use std::cell::RefCell;
use std::ffi::c_void;
use std::path::PathBuf;
use objc2::rc::Retained;
use objc2::runtime::{AnyObject, ProtocolObject, Sel};
use objc2::{define_class, msg_send, sel, AllocAnyThread, DefinedClass, MainThreadOnly};
use objc2_app_kit::{
NSApplication, NSApplicationActivationPolicy, NSBackingStoreType, NSCursor, NSDragOperation,
NSDraggingDestination, NSDraggingInfo, NSEvent, NSGraphicsContext, NSPasteboardType, NSScreen,
NSTextInputClient, NSTrackingArea, NSTrackingAreaOptions, NSView, NSWindow, NSWindowButton,
NSWindowDelegate, NSWindowStyleMask, NSWindowTitleVisibility,
};
#[allow(deprecated)]
use objc2_app_kit::NSFilenamesPboardType;
use objc2_core_foundation::{CFRetained, CGPoint, CGRect, CGSize};
use objc2_core_graphics::{
CGBitmapInfo, CGColorRenderingIntent, CGColorSpace, CGContext, CGDataProvider, CGImage,
CGImageAlphaInfo,
};
use objc2_foundation::{
MainThreadMarker, NSArray, NSAttributedString, NSAttributedStringKey, NSNotFound,
NSNotification, NSObjectProtocol, NSPoint, NSRange, NSRangePointer, NSRect, NSSize, NSString,
NSTimer, NSUInteger,
};
use tiny_skia::Pixmap;
use super::{AppHandler, WindowConfig};
use crate::event::{Key, KeyEvent, MouseButton, PointerEvent, PointerKind, WindowOp};
use crate::geometry::{Color, Point, Size};
use crate::platform::to_skia_color;
struct ViewState {
handler: Box<dyn AppHandler>,
bg: Color,
pixmap: Option<Pixmap>,
buf_w: i32,
buf_h: i32,
scale: f32,
frameless: bool,
composing: bool,
frame_timer: Option<Retained<NSTimer>>,
interval_timers: Vec<Retained<NSTimer>>,
color_space: CFRetained<CGColorSpace>,
}
impl ViewState {
fn ensure_pixmap(&mut self, w: i32, h: i32) {
let w = w.max(1);
let h = h.max(1);
if self.buf_w == w && self.buf_h == h && self.pixmap.is_some() {
return;
}
self.pixmap = Some(Pixmap::new(w as u32, h as u32).expect("分配 pixmap 失败"));
self.buf_w = w;
self.buf_h = h;
}
}
define_class!(
#[unsafe(super(NSView))]
#[thread_kind = MainThreadOnly]
#[name = "WindUiContentView"]
#[ivars = RefCell<ViewState>]
struct ContentView;
impl ContentView {
#[unsafe(method(isFlipped))]
fn is_flipped(&self) -> bool {
true
}
#[unsafe(method(acceptsFirstResponder))]
fn accepts_first_responder(&self) -> bool {
true
}
#[unsafe(method(drawRect:))]
fn draw_rect(&self, _dirty: NSRect) {
self.do_draw();
}
#[unsafe(method(mouseDown:))]
fn mouse_down(&self, ev: &NSEvent) {
if self.ivars().borrow().frameless {
let pos = self.loc_phys(ev);
let (drag, interactive) = {
let st = self.ivars().borrow();
(st.handler.window_drag_at(pos), st.handler.interactive_at(pos))
};
if drag && !interactive {
if let Some(win) = self.window() {
win.performWindowDragWithEvent(ev);
}
return;
}
}
self.on_pointer(ev, PointerKind::Down, MouseButton::Left);
}
#[unsafe(method(mouseUp:))]
fn mouse_up(&self, ev: &NSEvent) {
self.on_pointer(ev, PointerKind::Up, MouseButton::Left);
}
#[unsafe(method(mouseDragged:))]
fn mouse_dragged(&self, ev: &NSEvent) {
self.on_pointer(ev, PointerKind::Move, MouseButton::Left);
}
#[unsafe(method(mouseMoved:))]
fn mouse_moved(&self, ev: &NSEvent) {
self.on_pointer(ev, PointerKind::Move, MouseButton::Left);
}
#[unsafe(method(rightMouseDown:))]
fn right_mouse_down(&self, ev: &NSEvent) {
self.on_pointer(ev, PointerKind::Down, MouseButton::Right);
}
#[unsafe(method(rightMouseUp:))]
fn right_mouse_up(&self, ev: &NSEvent) {
self.on_pointer(ev, PointerKind::Up, MouseButton::Right);
}
#[unsafe(method(mouseExited:))]
fn mouse_exited(&self, _ev: &NSEvent) {
self.dispatch_pointer(PointerEvent::single(
PointerKind::Move,
Point::new(-1, -1),
MouseButton::Left,
));
}
#[unsafe(method(scrollWheel:))]
fn scroll_wheel(&self, ev: &NSEvent) {
self.on_wheel(ev);
}
#[unsafe(method(keyDown:))]
fn key_down(&self, ev: &NSEvent) {
self.on_key(ev);
}
#[unsafe(method(updateTrackingAreas))]
fn update_tracking_areas(&self) {
self.refresh_tracking_area();
let _: () = unsafe { msg_send![super(self), updateTrackingAreas] };
}
#[unsafe(method(frameTick:))]
fn frame_tick(&self, _timer: &NSTimer) {
self.setNeedsDisplay(true);
}
#[unsafe(method(intervalTick:))]
fn interval_tick(&self, timer: &NSTimer) {
let idx = {
let st = self.ivars().borrow();
st.interval_timers
.iter()
.position(|t| std::ptr::eq(Retained::as_ptr(t), timer))
};
let Some(idx) = idx else { return };
let need = self.ivars().borrow_mut().handler.on_interval_fired(idx);
if need {
self.setNeedsDisplay(true);
}
}
}
unsafe impl NSObjectProtocol for ContentView {}
unsafe impl NSDraggingDestination for ContentView {
#[unsafe(method(draggingEntered:))]
fn dragging_entered(&self, _sender: &ProtocolObject<dyn NSDraggingInfo>) -> NSDragOperation {
NSDragOperation::Copy
}
#[unsafe(method(performDragOperation:))]
fn perform_drag_operation(&self, sender: &ProtocolObject<dyn NSDraggingInfo>) -> bool {
self.on_drop(sender)
}
}
unsafe impl NSTextInputClient for ContentView {
#[unsafe(method(insertText:replacementRange:))]
fn insert_text(&self, string: &AnyObject, _replacement: NSRange) {
self.ime_insert(string);
}
#[unsafe(method(doCommandBySelector:))]
fn do_command_by_selector(&self, _selector: Sel) {}
#[unsafe(method(setMarkedText:selectedRange:replacementRange:))]
fn set_marked_text(&self, string: &AnyObject, _selected: NSRange, _replacement: NSRange) {
let composing = !anyobject_to_string(string).is_empty();
self.ivars().borrow_mut().composing = composing;
self.dispatch_composing(composing);
}
#[unsafe(method(unmarkText))]
fn unmark_text(&self) {
self.ivars().borrow_mut().composing = false;
self.dispatch_composing(false);
}
#[unsafe(method(selectedRange))]
fn selected_range(&self) -> NSRange {
NSRange { location: 0, length: 0 }
}
#[unsafe(method(markedRange))]
fn marked_range(&self) -> NSRange {
if self.ivars().borrow().composing {
NSRange { location: 0, length: 0 }
} else {
NSRange { location: NSNotFound as NSUInteger, length: 0 }
}
}
#[unsafe(method(hasMarkedText))]
fn has_marked_text(&self) -> bool {
self.ivars().borrow().composing
}
#[unsafe(method_id(attributedSubstringForProposedRange:actualRange:))]
fn attributed_substring(
&self,
_range: NSRange,
_actual: NSRangePointer,
) -> Option<Retained<NSAttributedString>> {
None
}
#[unsafe(method_id(validAttributesForMarkedText))]
fn valid_attributes(&self) -> Retained<NSArray<NSAttributedStringKey>> {
NSArray::new()
}
#[unsafe(method(firstRectForCharacterRange:actualRange:))]
fn first_rect(&self, _range: NSRange, _actual: NSRangePointer) -> NSRect {
self.ime_caret_rect()
}
#[unsafe(method(characterIndexForPoint:))]
fn character_index(&self, _point: NSPoint) -> NSUInteger {
0
}
}
unsafe impl NSWindowDelegate for ContentView {
#[unsafe(method(windowShouldClose:))]
fn window_should_close(&self, _sender: &AnyObject) -> bool {
let allow = self.ivars().borrow_mut().handler.on_close_request();
if !allow {
self.setNeedsDisplay(true);
}
allow
}
#[unsafe(method(windowWillClose:))]
fn window_will_close(&self, _notification: &NSNotification) {
let mtm = MainThreadMarker::from(self);
NSApplication::sharedApplication(mtm).terminate(None);
}
}
);
fn anyobject_to_string(obj: &AnyObject) -> String {
if let Some(s) = obj.downcast_ref::<NSString>() {
s.to_string()
} else if let Some(a) = obj.downcast_ref::<NSAttributedString>() {
a.string().to_string()
} else {
String::new()
}
}
impl ContentView {
#[allow(deprecated)]
fn new(
mtm: MainThreadMarker,
frame: NSRect,
handler: Box<dyn AppHandler>,
bg: Color,
frameless: bool,
) -> Retained<Self> {
let color_space = CGColorSpace::new_device_rgb().expect("CGColorSpaceCreateDeviceRGB 失败");
let state = ViewState {
handler,
bg,
pixmap: None,
buf_w: 0,
buf_h: 0,
scale: 1.0,
frameless,
composing: false,
frame_timer: None,
interval_timers: Vec::new(),
color_space,
};
let this = Self::alloc(mtm).set_ivars(RefCell::new(state));
let this: Retained<Self> = unsafe { msg_send![super(this), initWithFrame: frame] };
let ty: &NSPasteboardType = unsafe { NSFilenamesPboardType };
let types = NSArray::from_slice(&[ty]);
this.registerForDraggedTypes(&types);
this
}
#[allow(deprecated)]
fn on_drop(&self, sender: &ProtocolObject<dyn NSDraggingInfo>) -> bool {
let pb = sender.draggingPasteboard();
let ty: &NSPasteboardType = unsafe { NSFilenamesPboardType };
let Some(plist) = pb.propertyListForType(ty) else {
return false;
};
let Ok(arr) = plist.downcast::<NSArray>() else {
return false;
};
let mut paths: Vec<PathBuf> = Vec::new();
for i in 0..arr.count() {
if let Ok(s) = arr.objectAtIndex(i).downcast::<NSString>() {
paths.push(PathBuf::from(s.to_string()));
}
}
if paths.is_empty() {
return false;
}
let view_pt = self.convertPoint_fromView(sender.draggingLocation(), None);
let scale = self.ivars().borrow().scale;
let pos = Point::new(
(view_pt.x as f32 * scale).round() as i32,
(view_pt.y as f32 * scale).round() as i32,
);
let repaint = {
let _guard = crate::platform::EventDispatchGuard::enter();
self.ivars().borrow_mut().handler.on_drop_files(pos, paths)
};
if repaint {
self.setNeedsDisplay(true);
}
self.after_event();
true
}
fn refresh_tracking_area(&self) {
let areas = self.trackingAreas();
for area in areas.iter() {
self.removeTrackingArea(&area);
}
let opts = NSTrackingAreaOptions::MouseEnteredAndExited
| NSTrackingAreaOptions::MouseMoved
| NSTrackingAreaOptions::ActiveInKeyWindow
| NSTrackingAreaOptions::InVisibleRect;
let area = unsafe {
NSTrackingArea::initWithRect_options_owner_userInfo(
NSTrackingArea::alloc(),
self.bounds(),
opts,
Some(self),
None,
)
};
self.addTrackingArea(&area);
}
fn do_draw(&self) {
let bounds = self.bounds();
let scale = self
.window()
.map(|w| w.backingScaleFactor() as f32)
.unwrap_or(1.0)
.max(0.1);
let pw = (bounds.size.width as f32 * scale).round().max(1.0) as i32;
let ph = (bounds.size.height as f32 * scale).round().max(1.0) as i32;
let image = {
let mut st = self.ivars().borrow_mut();
if (st.scale - scale).abs() > 0.001 {
st.scale = scale;
st.handler.set_scale(scale);
}
st.ensure_pixmap(pw, ph);
let bg = st.bg;
let pixmap = st.pixmap.as_mut().unwrap();
pixmap.fill(to_skia_color(bg));
let size = Size::new(pw, ph);
let ptr = pixmap as *mut Pixmap;
let mut tgt = crate::render::PixmapTarget {
pixmap: unsafe { &mut *ptr },
};
st.handler.render(&mut tgt, size);
let bytes_per_row = pw as usize * 4;
let pixmap = st.pixmap.as_ref().unwrap();
let data = pixmap.data().as_ptr() as *const c_void;
let size = bytes_per_row * ph as usize;
let cs = st.color_space.clone();
let provider =
unsafe { CGDataProvider::with_data(std::ptr::null_mut(), data, size, None) };
provider.and_then(|p| unsafe {
CGImage::new(
pw as usize,
ph as usize,
8,
32,
bytes_per_row,
Some(&cs),
CGBitmapInfo(CGImageAlphaInfo::PremultipliedLast.0),
Some(&p),
std::ptr::null(),
false,
CGColorRenderingIntent::RenderingIntentDefault,
)
})
};
let Some(image) = image else { return };
let Some(gctx) = NSGraphicsContext::currentContext() else {
return;
};
let cg = gctx.CGContext();
let h = bounds.size.height;
CGContext::save_g_state(Some(&cg));
CGContext::translate_ctm(Some(&cg), 0.0, h);
CGContext::scale_ctm(Some(&cg), 1.0, -1.0);
CGContext::draw_image(
Some(&cg),
CGRect {
origin: CGPoint { x: 0.0, y: 0.0 },
size: CGSize {
width: bounds.size.width,
height: bounds.size.height,
},
},
Some(&image),
);
CGContext::restore_g_state(Some(&cg));
self.schedule_next_frame();
}
fn install_interval_timers(&self) {
let durs = self.ivars().borrow().handler.intervals();
let mut timers = Vec::with_capacity(durs.len());
for d in durs {
let secs = d.as_secs_f64().max(0.001);
let timer = unsafe {
NSTimer::scheduledTimerWithTimeInterval_target_selector_userInfo_repeats(
secs,
self,
sel!(intervalTick:),
None,
true, )
};
timers.push(timer);
}
self.ivars().borrow_mut().interval_timers = timers;
}
fn schedule_next_frame(&self) {
if let Some(t) = self.ivars().borrow_mut().frame_timer.take() {
t.invalidate();
}
if !self.ivars().borrow().handler.wants_animation() {
return;
}
let interval = self.display_frame_interval();
let timer = unsafe {
NSTimer::scheduledTimerWithTimeInterval_target_selector_userInfo_repeats(
interval,
self,
sel!(frameTick:),
None,
false,
)
};
self.ivars().borrow_mut().frame_timer = Some(timer);
}
fn display_frame_interval(&self) -> f64 {
let mtm = MainThreadMarker::from(self);
let fps = self
.window()
.and_then(|w| w.screen())
.or_else(|| NSScreen::mainScreen(mtm))
.map(|s| s.maximumFramesPerSecond())
.unwrap_or(60)
.clamp(60, 240);
1.0 / fps as f64
}
fn loc_phys(&self, ev: &NSEvent) -> Point {
let win_pt = ev.locationInWindow();
let view_pt = self.convertPoint_fromView(win_pt, None);
let scale = self.ivars().borrow().scale;
Point::new(
(view_pt.x as f32 * scale).round() as i32,
(view_pt.y as f32 * scale).round() as i32,
)
}
fn on_pointer(&self, ev: &NSEvent, kind: PointerKind, button: MouseButton) {
let pos = self.loc_phys(ev);
let click_count = if matches!(kind, PointerKind::Down) {
(ev.clickCount().max(1) as u8).min(3)
} else {
1
};
self.dispatch_pointer(PointerEvent {
kind,
pos,
button,
click_count,
});
}
fn on_wheel(&self, ev: &NSEvent) {
let dy = ev.scrollingDeltaY();
let delta = if ev.hasPreciseScrollingDeltas() {
(dy * 3.0) as i32
} else {
(dy * 40.0) as i32
};
if delta == 0 {
return;
}
let pos = self.loc_phys(ev);
self.dispatch_pointer(PointerEvent::single(
PointerKind::Wheel(delta),
pos,
MouseButton::Left,
));
}
fn on_key(&self, ev: &NSEvent) {
if self.ivars().borrow().composing {
self.route_ime(ev);
return;
}
let key_code = ev.keyCode();
let flags = ev.modifierFlags();
let shift = flags.contains(objc2_app_kit::NSEventModifierFlags::Shift);
let modk = flags.contains(objc2_app_kit::NSEventModifierFlags::Command)
|| flags.contains(objc2_app_kit::NSEventModifierFlags::Control);
let special = map_special(key_code);
if let Some(k) = special {
self.dispatch_key(KeyEvent {
key: k,
pressed: true,
shift,
ctrl: modk,
});
if k != Key::Space {
return;
}
}
if modk {
if special.is_none() {
if let Some(s) = ev.charactersIgnoringModifiers() {
if let Some(c) = s.to_string().chars().next() {
let up = c.to_ascii_uppercase();
self.dispatch_key(KeyEvent {
key: Key::Other(up as u32),
pressed: true,
shift,
ctrl: true,
});
}
}
}
return;
}
self.route_ime(ev);
}
fn route_ime(&self, ev: &NSEvent) {
if let Some(ic) = self.inputContext() {
let _ = ic.handleEvent(ev);
}
}
fn ime_insert(&self, string: &AnyObject) {
let text = anyobject_to_string(string);
self.ivars().borrow_mut().composing = false;
self.dispatch_composing(false);
for c in text.chars() {
if c.is_control() {
continue;
}
self.dispatch_key(KeyEvent {
key: Key::Char(c),
pressed: true,
shift: false,
ctrl: false,
});
}
}
fn ime_caret_rect(&self) -> NSRect {
let (caret, scale) = {
let st = self.ivars().borrow();
(st.handler.ime_caret(), st.scale)
};
let Some((x, y, h)) = caret else {
return NSRect {
origin: NSPoint { x: 0.0, y: 0.0 },
size: NSSize {
width: 0.0,
height: 0.0,
},
};
};
let s = scale as f64;
let view_rect = NSRect {
origin: NSPoint {
x: x as f64 / s,
y: y as f64 / s,
},
size: NSSize {
width: 1.0,
height: (h as f64 / s).max(1.0),
},
};
let win_rect = self.convertRect_toView(view_rect, None);
match self.window() {
Some(w) => w.convertRectToScreen(win_rect),
None => win_rect,
}
}
fn dispatch_pointer(&self, ev: PointerEvent) {
let repaint = {
let _guard = crate::platform::EventDispatchGuard::enter();
self.ivars().borrow_mut().handler.on_pointer(ev)
};
if repaint {
self.setNeedsDisplay(true);
}
self.after_event();
}
fn dispatch_key(&self, ev: KeyEvent) {
let repaint = {
let _guard = crate::platform::EventDispatchGuard::enter();
self.ivars().borrow_mut().handler.on_key(ev)
};
if repaint {
self.setNeedsDisplay(true);
}
self.after_event();
}
fn dispatch_composing(&self, composing: bool) {
let repaint = {
let _guard = crate::platform::EventDispatchGuard::enter();
self.ivars()
.borrow_mut()
.handler
.set_ime_composing(composing)
};
if repaint {
self.setNeedsDisplay(true);
}
}
fn after_event(&self) {
let (op, dialog, close) = {
let mut st = self.ivars().borrow_mut();
(
st.handler.take_window_op(),
st.handler.take_dialog_request(),
st.handler.wants_close(),
)
};
if let Some(op) = op {
if let Some(win) = self.window() {
match op {
WindowOp::Minimize => win.miniaturize(None),
WindowOp::ToggleMaximize => win.zoom(None),
}
}
}
if let Some(req) = dialog {
req.run();
self.setNeedsDisplay(true);
}
self.apply_cursor();
if close {
if let Some(win) = self.window() {
win.close();
}
}
}
fn apply_cursor(&self) {
let shape = self.ivars().borrow().handler.cursor();
let cursor = match shape {
crate::event::CursorShape::Hand => NSCursor::pointingHandCursor(),
crate::event::CursorShape::Text => NSCursor::IBeamCursor(),
crate::event::CursorShape::Arrow => NSCursor::arrowCursor(),
};
cursor.set();
}
}
fn map_special(key_code: u16) -> Option<Key> {
Some(match key_code {
0x30 => Key::Tab, 0x24 => Key::Enter, 0x4C => Key::Enter, 0x35 => Key::Escape, 0x31 => Key::Space, 0x33 => Key::Backspace, 0x75 => Key::Delete, 0x7B => Key::Left, 0x7C => Key::Right, 0x7D => Key::Down, 0x7E => Key::Up, 0x73 => Key::Home, 0x77 => Key::End, _ => return None,
})
}
extern "C" {
static _dispatch_main_q: c_void;
fn dispatch_async_f(
queue: *const c_void,
context: *mut c_void,
work: extern "C" fn(*mut c_void),
);
}
extern "C" fn wake_on_main(ctx: *mut c_void) {
let view = ctx as *const ContentView;
unsafe { (*view).setNeedsDisplay(true) };
}
struct MacWake {
view: usize,
}
unsafe impl Send for MacWake {}
impl crate::sync::RawWakeSignal for MacWake {
fn signal(&self) {
unsafe {
dispatch_async_f(
std::ptr::addr_of!(_dispatch_main_q),
self.view as *mut c_void,
wake_on_main,
);
}
}
}
pub(crate) fn run_windowed(
mut cfg: WindowConfig,
handler: Box<dyn AppHandler>,
waker: Option<std::sync::Arc<crate::sync::WakerShared>>,
) {
let mtm = MainThreadMarker::new().expect("macOS GUI 必须在主线程运行");
let app = NSApplication::sharedApplication(mtm);
app.setActivationPolicy(NSApplicationActivationPolicy::Regular);
let content_rect = NSRect {
origin: NSPoint { x: 0.0, y: 0.0 },
size: NSSize {
width: cfg.width as f64,
height: cfg.height as f64,
},
};
let mut style =
NSWindowStyleMask::Titled | NSWindowStyleMask::Closable | NSWindowStyleMask::Miniaturizable;
if cfg.resizable {
style |= NSWindowStyleMask::Resizable;
}
let window = unsafe {
NSWindow::initWithContentRect_styleMask_backing_defer(
NSWindow::alloc(mtm),
content_rect,
style,
NSBackingStoreType::Buffered,
false,
)
};
window.setTitle(&NSString::from_str(&cfg.title));
if cfg.min_width > 0 || cfg.min_height > 0 {
window.setContentMinSize(NSSize {
width: cfg.min_width.max(0) as f64,
height: cfg.min_height.max(0) as f64,
});
}
if cfg.frameless {
window.setStyleMask(style | NSWindowStyleMask::FullSizeContentView);
window.setTitlebarAppearsTransparent(true);
window.setTitleVisibility(NSWindowTitleVisibility::Hidden);
for b in [
NSWindowButton::CloseButton,
NSWindowButton::MiniaturizeButton,
NSWindowButton::ZoomButton,
] {
if let Some(btn) = window.standardWindowButton(b) {
btn.setHidden(true);
}
}
}
let view = ContentView::new(mtm, content_rect, handler, cfg.bg, cfg.frameless);
let scale = window.backingScaleFactor() as f32;
{
let mut st = view.ivars().borrow_mut();
st.scale = scale;
st.handler.set_scale(scale);
}
window.setContentView(Some(&view));
window.setAcceptsMouseMovedEvents(true);
window.setDelegate(Some(ProtocolObject::from_ref(&*view)));
view.refresh_tracking_area();
let _ = window.makeFirstResponder(Some(&view));
if cfg.centered {
window.center();
}
if let Some(w) = &waker {
w.bind(Box::new(MacWake {
view: Retained::as_ptr(&view) as usize,
}));
}
view.install_interval_timers();
let _tray = cfg
.tray
.take()
.and_then(|t| super::tray::install(mtm, window.clone(), t));
window.makeKeyAndOrderFront(None);
app.activate();
app.run();
drop(_tray);
}