Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ objc2-foundation = { version = "0.3.2", default-features = false, features = ["s
objc2-app-kit = { version = "0.3.2", default-features = false, features = [
"NSApplication",
"NSCursor",
"NSImage",
"NSDragging",
"NSEvent",
"NSGraphics",
Expand All @@ -92,7 +93,7 @@ objc2-app-kit = { version = "0.3.2", default-features = false, features = [
] }

[workspace]
members = ["examples/open_parented", "examples/open_window", "examples/plugin_clack", "examples/render_femtovg", "examples/render_wgpu"]
members = ["examples/cursors", "examples/open_parented", "examples/open_window", "examples/plugin_clack", "examples/render_femtovg", "examples/render_wgpu"]

[lints.clippy]
missing-safety-doc = "allow"
Expand Down
9 changes: 9 additions & 0 deletions examples/cursors/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[package]
name = "cursors"
version = "0.1.0"
edition = "2021"

[dependencies]
baseview = { path = "../..", features = ["opengl", "tracing"] }
femtovg = "0.26"
tracing-subscriber = { workspace = true }
111 changes: 111 additions & 0 deletions examples/cursors/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
use baseview::dpi::{LogicalSize, PhysicalPosition};
use baseview::gl::{GlConfig, GlContext};
use baseview::{
Event, EventStatus, HandlerError, MouseCursor, MouseEvent, Window, WindowContext,
WindowHandler, WindowSettings, WindowSize,
};
use femtovg::renderer::OpenGl;
use femtovg::{Canvas, Color};
use std::cell::{Cell, RefCell};

struct CursorsExample {
window_context: WindowContext,
gl_context: GlContext,
canvas: RefCell<Canvas<OpenGl>>,
damaged: Cell<bool>,
}

impl CursorsExample {
fn new(window_context: WindowContext) -> Result<Self, HandlerError> {
let Some(gl_context) = window_context.gl_context() else { unreachable!() };
unsafe { gl_context.make_current()? };

let renderer =
unsafe { OpenGl::new_from_function_cstr(|s| gl_context.get_proc_address(s)) }?;

let mut canvas = Canvas::new(renderer)?;
let size = window_context.size();

canvas.set_size(size.physical.width, size.physical.height, size.scale_factor as f32);

unsafe { gl_context.make_not_current()? };
Ok(Self { gl_context, window_context, canvas: canvas.into(), damaged: true.into() })
}

fn in_blue_area(&self, position: PhysicalPosition<f64>) -> bool {
let window_size = self.window_context.size().physical.cast::<f64>();
let x = position.x / window_size.width;
let y = position.y / window_size.height;

let is_outside = x < 0.1 || y < 0.1 || x > 0.9 || y > 0.9;
!is_outside
}
}

impl WindowHandler for CursorsExample {
fn on_frame(&self) -> Result<(), HandlerError> {
if !self.damaged.get() {
return Ok(());
}

let context = &self.gl_context;
unsafe { context.make_current()? };

let mut canvas = self.canvas.borrow_mut();

let screen_height = canvas.height();
let screen_width = canvas.width();

// Clear
canvas.clear_rect(0, 0, screen_width, screen_height, Color::rgb(0xAA, 0xAA, 0xAA));

// Make big blue rectangle
canvas.clear_rect(
(screen_width as f32 * 0.1).floor() as u32,
(screen_height as f32 * 0.1).floor() as u32,
(screen_width as f32 * 0.8).floor() as u32,
(screen_height as f32 * 0.8).floor() as u32,
Color::rgbf(0., 0.3, 0.9),
);

// Tell renderer to execute all drawing commands
canvas.flush();
context.swap_buffers()?;
unsafe { context.make_not_current()? };
self.damaged.set(false);

Ok(())
}

fn resized(&self, new_size: WindowSize) -> Result<(), HandlerError> {
let size = new_size.physical;
self.canvas.borrow_mut().set_size(size.width, size.height, new_size.scale_factor as f32);
self.damaged.set(true);

Ok(())
}

fn on_event(&self, event: Event) -> EventStatus {
if let Event::Mouse(MouseEvent::CursorMoved { position, .. }) = event {
if self.in_blue_area(position) {
self.window_context.set_mouse_cursor(MouseCursor::Working).unwrap();
} else {
self.window_context.set_mouse_cursor(MouseCursor::Hand).unwrap();
}
};

EventStatus::Captured
}
}

fn main() -> Result<(), baseview::Error> {
tracing_subscriber::fmt::init();

let window_open_options = WindowSettings::new()
.with_title("Baseview cursors")
.with_size(LogicalSize::new(512, 512))
.with_gl_config(GlConfig::default());

Window::create(window_open_options, CursorsExample::new)?.run_until_closed()?;
Ok(())
}
12 changes: 4 additions & 8 deletions src/platform/macos/context.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
use crate::dpi::Size;
use crate::platform::macos::cursor::Cursor;
use crate::platform::macos::view::BaseviewView;
use crate::platform::Result;
use crate::platform::{PlatformHandle, WindowSharedState};
Expand All @@ -9,7 +8,6 @@ use dispatch2::MainThreadBound;
use objc2::rc::Weak;
use objc2::runtime::NSObjectProtocol;
use objc2::{MainThreadMarker, Message};
use objc2_app_kit::NSCursor;
use raw_window_handle::DisplayHandle;
use std::rc::Rc;

Expand Down Expand Up @@ -75,12 +73,10 @@ impl WindowContext {

pub fn set_mouse_cursor(&self, cursor: MouseCursor) -> Result<()> {
let Some(view) = self.view.load() else { return Ok(()) };
let native_cursor = Cursor::from(cursor);
if let Some(cursor) = native_cursor.load() {
view.addCursorRect_cursor(view.bounds(), &cursor);
} else {
NSCursor::hide()
}
let Some(view) = view.inner_ref() else { return Ok(()) };

view.inner.cursor_manager.set_cursor(cursor);

Ok(())
}

Expand Down
100 changes: 72 additions & 28 deletions src/platform/macos/cursor.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,80 @@
use crate::MouseCursor;
use objc2::__framework_prelude::Retained;
use objc2::runtime::{MessageReceiver, Sel};
use objc2::{msg_send, sel, ClassType};
use objc2_app_kit::NSCursor;
use objc2::{msg_send, sel, AnyThread, ClassType, Message};
use objc2_app_kit::{NSCursor, NSImage};
use objc2_foundation::{NSPoint, NSSize};
use std::cell::{Cell, LazyCell, RefCell};

use crate::MouseCursor;
pub struct CursorManager {
is_inside: Cell<bool>,
current: Cell<MouseCursor>,
current_cursor: RefCell<Retained<NSCursor>>,
empty: LazyCell<Retained<NSCursor>>,
}

impl CursorManager {
pub fn new() -> Self {
Self {
current: MouseCursor::Default.into(),
current_cursor: NSCursor::arrowCursor().into(),
empty: LazyCell::new(Self::create_empty_cursor),
is_inside: Cell::new(false),
}
}

fn create_empty_cursor() -> Retained<NSCursor> {
let image = NSImage::initWithSize(NSImage::alloc(), NSSize::new(0.0, 0.0));
NSCursor::initWithImage_hotSpot(NSCursor::alloc(), &image, NSPoint::ZERO)
}

pub fn set_is_inside(&self, is_inside: bool) {
self.is_inside.set(is_inside);
}

pub fn set_cursor(&self, cursor: MouseCursor) {
if self.current.get() == cursor {
return;
}

self.current_cursor.replace(self.load(cursor.into()));
self.current.set(cursor);

if self.is_inside.get() {
self.update_to_current_cursor();
}
}

pub fn update_to_current_cursor(&self) {
//NSCursor::crosshairCursor().set();
self.current_cursor.borrow().set();
}

fn load(&self, cursor: Cursor) -> Retained<NSCursor> {
match cursor {
Cursor::Native(loader) => loader(),
Cursor::Undocumented(sel) => {
let class = NSCursor::class();

// NOTE: class.responds_to does not yield the same result (probably because NSCursor overrides respondsToSelector)
let responds_to: bool = unsafe { msg_send![class, respondsToSelector: sel] };

if !responds_to {
return NSCursor::arrowCursor();
}

let raw: *mut NSCursor = unsafe { class.send_message(sel, ()) };
let cursor = unsafe { Retained::retain(raw) };

cursor.unwrap_or_else(NSCursor::arrowCursor)
}
Cursor::Hidden => self.empty.retain(),
}
}
}

#[derive(Debug)]
pub enum Cursor {
enum Cursor {
Native(fn() -> Retained<NSCursor>),
Undocumented(Sel),
Hidden,
Expand Down Expand Up @@ -63,27 +131,3 @@ impl From<MouseCursor> for Cursor {
}
}
}

impl Cursor {
pub fn load(&self) -> Option<Retained<NSCursor>> {
match self {
Cursor::Native(loader) => Some(loader()),
Cursor::Undocumented(sel) => {
let class = NSCursor::class();

// NOTE: class.responds_to does not yield the same result (probably because NSCursor overrides respondsToSelector)
let responds_to: bool = unsafe { msg_send![class, respondsToSelector: *sel] };

if !responds_to {
return Some(NSCursor::arrowCursor());
}

let raw: *mut NSCursor = unsafe { class.send_message(*sel, ()) };
let cursor = unsafe { Retained::retain(raw) };

Some(cursor.unwrap_or_else(NSCursor::arrowCursor))
}
Cursor::Hidden => None,
}
}
}
37 changes: 26 additions & 11 deletions src/platform/macos/view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use super::keyboard::{make_modifiers, KeyboardState};
use super::window::WindowSharedState;
use crate::dpi::{LogicalPosition, LogicalSize, Size};
use crate::host::Host;
use crate::platform::macos::cursor::CursorManager;
use crate::platform::*;
use crate::tracing::warn;
use crate::utils::SizingStrategy;
Expand All @@ -19,10 +20,10 @@ use objc2::rc::Weak;
use objc2::runtime::{NSObjectProtocol, ProtocolObject};
use objc2::{msg_send, AllocAnyThread, ClassType, MainThreadMarker};
use objc2_app_kit::{
NSApplication, NSCursor, NSDragOperation, NSDraggingInfo, NSEvent, NSFilenamesPboardType,
NSTrackingArea, NSTrackingAreaOptions, NSView, NSWindow,
NSApplication, NSDragOperation, NSDraggingInfo, NSEvent, NSFilenamesPboardType, NSTrackingArea,
NSTrackingAreaOptions, NSView, NSWindow,
};
use objc2_foundation::{NSArray, NSNotification, NSPoint, NSRect, NSSize, NSString};
use objc2_foundation::{NSArray, NSNotification, NSPoint, NSPointInRect, NSRect, NSSize, NSString};
use std::cell::{Cell, RefCell};
use std::rc::Rc;

Expand Down Expand Up @@ -73,6 +74,7 @@ pub(crate) struct BaseviewView {
pub(crate) lifetime_tied_to_app: Cell<Option<Weak<NSApplication>>>,

host: Host,
pub(crate) cursor_manager: CursorManager,

#[cfg(feature = "opengl")]
pub(crate) gl_context: std::cell::OnceCell<super::gl::GlContext>,
Expand Down Expand Up @@ -103,6 +105,7 @@ impl BaseviewView {
parenting: ViewParentingType::Uninitialized.into(),
host: init.host,
lifetime_tied_to_app: None.into(),
cursor_manager: CursorManager::new(),

#[cfg(feature = "opengl")]
gl_context: std::cell::OnceCell::new(),
Expand Down Expand Up @@ -274,9 +277,6 @@ impl BaseviewView {
impl Drop for BaseviewView {
fn drop(&mut self) {
self.state.closed.set(true);
if self.state.cursor_hidden.get() {
NSCursor::unhide();
}
}
}

Expand Down Expand Up @@ -416,9 +416,7 @@ impl ViewImpl for BaseviewView {
}

unsafe {
let superclass = msg_send![this.view, superclass];

let () = msg_send![super(this.view, superclass), viewWillMoveToWindow: new_window];
let () = msg_send![super(this.view, NSView::class()), viewWillMoveToWindow: new_window];
}
}

Expand Down Expand Up @@ -446,6 +444,9 @@ impl ViewImpl for BaseviewView {
modifiers: make_modifiers(event.modifierFlags()),
}),
);

// SAFETY: Our superclass is NSView
let _: () = unsafe { msg_send![super(this.view, NSView::class()), mouseMoved: event] };
}

fn scroll_wheel(this: ViewRef<Self>, event: &NSEvent) {
Expand Down Expand Up @@ -620,13 +621,26 @@ impl ViewImpl for BaseviewView {
}

fn mouse_entered(this: ViewRef<Self>) {
this.cursor_manager.set_is_inside(true);
Self::trigger_event(this, Event::Mouse(MouseEvent::CursorEntered));
}

fn mouse_exited(this: ViewRef<Self>) {
this.cursor_manager.set_is_inside(false);
Self::trigger_event(this, Event::Mouse(MouseEvent::CursorLeft));
}

fn cursor_update(this: ViewRef<Self>, event: Option<&NSEvent>) -> bool {
let Some(event) = event else { return false };
let point = this.view.convertPoint_fromView(event.locationInWindow(), None);
if NSPointInRect(point, this.view.frame()) {
this.cursor_manager.update_to_current_cursor();
true
} else {
false
}
}

fn key_down(this: ViewRef<Self>, event: &NSEvent) {
if let Some(key_event) = this.keyboard_state.process_native_event(event) {
let status = Self::trigger_event(this, Event::Keyboard(key_event));
Expand Down Expand Up @@ -678,15 +692,16 @@ fn new_tracking_area(this: &NSView) -> Retained<NSTrackingArea> {
let options = NSTrackingAreaOptions::MouseEnteredAndExited
| NSTrackingAreaOptions::MouseMoved
| NSTrackingAreaOptions::CursorUpdate
| NSTrackingAreaOptions::ActiveInActiveApp
//| NSTrackingAreaOptions::ActiveInActiveApp
| NSTrackingAreaOptions::ActiveInKeyWindow
| NSTrackingAreaOptions::InVisibleRect
| NSTrackingAreaOptions::EnabledDuringMouseDrag;

// SAFETY: `this` is of the correct type (NSView)
unsafe {
NSTrackingArea::initWithRect_options_owner_userInfo(
NSTrackingArea::alloc(),
this.bounds(),
NSRect::new(NSPoint::new(0.0, 0.0), NSSize::new(0.0, 0.0)),
options,
Some(this),
None,
Expand Down
Loading