diff --git a/build.zig.zon b/build.zig.zon index e36c945237..0b8f5d296e 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -22,8 +22,8 @@ .lazy = true, }, .mach_objc = .{ - .url = "https://pkg.machengine.org/mach-objc/eb1e1eee9c02039d582f5fd9814d32e48b736ba6.tar.gz", - .hash = "12209742f139402c34a8901bfb012a748c7101bef971f0a541338d659baa345b237d", + .url = "https://pkg.machengine.org/mach-objc/61206f68d907111ce3c1f068ef7d0b926ead5d62.tar.gz", + .hash = "12205e56037f3c3112c073bc833eea5f95b6ec3fb03a53cc7264595d2266304a5f7f", .lazy = true, }, .xcode_frameworks = .{ diff --git a/examples/core-transparent-window/App.zig b/examples/core-transparent-window/App.zig index fcb20b0787..9160d526ad 100644 --- a/examples/core-transparent-window/App.zig +++ b/examples/core-transparent-window/App.zig @@ -32,6 +32,7 @@ pub fn init( const window = try core.windows.new(.{ .title = "core-transparent-window", .vsync_mode = .double, + .transparent = true, }); // Store our render pipeline in our module's state, so we can access it later on. @@ -124,10 +125,10 @@ pub fn tick(app: *App, core: *mach.Core) void { defer encoder.release(); // Begin render pass - const sky_blue_background = gpu.Color{ .r = 0.0, .g = 0.0, .b = 0.0, .a = 0.0 }; + const transparent_background = gpu.Color{ .r = 0.0, .g = 0.0, .b = 0.0, .a = 0.0 }; const color_attachments = [_]gpu.RenderPassColorAttachment{.{ .view = back_buffer_view, - .clear_value = sky_blue_background, + .clear_value = transparent_background, .load_op = .clear, .store_op = .store, }}; @@ -172,7 +173,11 @@ pub fn tick(app: *App, core: *mach.Core) void { const green = mach.math.lerp(0.2, 0.6, mach.math.clamp(app.color_time - 2.0, 0.0, 1.0)); const alpha = mach.math.lerp(0.3, 1.0, app.color_time / 4.0); - core.windows.set(app.window, .color, .{ .transparent = .{ .color = .{ .r = red, .g = green, .b = blue, .a = alpha }, .titlebar = true } }); + core.windows.set( + app.window, + .decoration_color, + .{ .r = red, .g = green, .b = blue, .a = alpha }, + ); } pub fn deinit(app: *App) void { diff --git a/src/Core.zig b/src/Core.zig index 28a0c19d26..8fed0b4864 100644 --- a/src/Core.zig +++ b/src/Core.zig @@ -56,6 +56,7 @@ windows: mach.Objects( /// Vertical sync mode, prevents screen tearing. vsync_mode: VSyncMode = .none, + /// Window display mode: fullscreen, windowed or borderless fullscreen display_mode: DisplayMode = .windowed, /// Cursor @@ -74,8 +75,17 @@ windows: mach.Objects( /// Target frames per second refresh_rate: u32 = 0, - /// Color of the window background/titlebar - color: WindowColor = .system, + /// Titlebar/window decorations + decorated: bool = true, + + /// Color of the window decorations, i.e. titlebar + /// if null, decoration is the system-determined color + decoration_color: ?gpu.Color = null, + + /// Whether the window should be completely transparent + /// or not. On macOS, to achieve a fully transparent window + /// decoration_color must also be set fully transparent. + transparent: bool = false, // GPU // When `native` is not null, the rest of the fields have been @@ -86,7 +96,6 @@ windows: mach.Objects( queue: *gpu.Queue = undefined, swap_chain: *gpu.SwapChain = undefined, swap_chain_descriptor: gpu.SwapChain.Descriptor = undefined, - swap_chain_update: std.Thread.ResetEvent = .{}, surface: *gpu.Surface = undefined, surface_descriptor: gpu.Surface.Descriptor = undefined, @@ -99,6 +108,7 @@ windows: mach.Objects( .render_attachment = true, }, + /// Container for native platform-specific information native: ?Platform.Native = null, }, ), @@ -209,8 +219,8 @@ pub fn initWindow(core: *Core, window_id: mach.ObjectID) !void { .label = "main swap chain", .usage = core_window.swap_chain_usage, .format = .bgra8_unorm, - .width = core_window.width, - .height = core_window.height, + .width = core_window.framebuffer_width, + .height = core_window.framebuffer_height, .present_mode = switch (core_window.vsync_mode) { .none => .immediate, .double => .fifo, @@ -218,10 +228,6 @@ pub fn initWindow(core: *Core, window_id: mach.ObjectID) !void { }, }; core_window.swap_chain = core_window.device.createSwapChain(core_window.surface, &core_window.swap_chain_descriptor); - core_window.framebuffer_format = core_window.swap_chain_descriptor.format; - core_window.framebuffer_width = core_window.swap_chain_descriptor.width; - core_window.framebuffer_height = core_window.swap_chain_descriptor.height; - core.pushEvent(.{ .window_open = .{ .window_id = window_id } }); } @@ -236,6 +242,32 @@ pub fn tick(core: *Core, core_mod: mach.Mod(Core)) !void { core_mod.call(.presentFrame); } +pub fn presentFrame(core: *Core, core_mod: mach.Mod(Core)) !void { + var windows = core.windows.slice(); + while (windows.next()) |window_id| { + var core_window = core.windows.getValue(window_id); + defer core.windows.setValueRaw(window_id, core_window); + + mach.sysgpu.Impl.deviceTick(core_window.device); + + core_window.swap_chain.present(); + } + + // Record to frame rate frequency monitor that a frame was finished. + core.frame.tick(); + + switch (core.state) { + .running => {}, + .exiting => { + core.state = .deinitializing; + core_mod.run(core.on_exit.?); + core_mod.call(.deinit); + }, + .deinitializing => {}, + .exited => @panic("application not running"), + } +} + pub fn main(core: *Core, core_mod: mach.Mod(Core)) !void { if (core.on_tick == null) @panic("core.on_tick callback must be set"); if (core.on_exit == null) @panic("core.on_exit callback must be set"); @@ -286,11 +318,14 @@ fn platform_update_callback(core: *Core, core_mod: mach.Mod(Core)) !bool { core_mod.run(core.on_tick.?); core_mod.call(.presentFrame); - //core_mod.call(.processWindowUpdates); return core.state != .exited; } +pub fn exit(core: *Core) void { + core.state = .exiting; +} + pub fn deinit(core: *Core) !void { core.state = .exited; @@ -369,206 +404,6 @@ pub fn mousePosition(core: *@This()) Position { return core.input_state.mouse_position; } -// TODO(object) -// /// Set refresh rate synchronization mode. Default `.triple` -// /// -// /// Calling this function also implicitly calls setFrameRateLimit for you: -// /// ``` -// /// .none => setFrameRateLimit(0) // unlimited -// /// .double => setFrameRateLimit(0) // unlimited -// /// .triple => setFrameRateLimit(2 * max_monitor_refresh_rate) -// /// ``` -// pub inline fn setVSync(core: *@This(), mode: VSyncMode) void { -// return core.platform.setVSync(mode); -// } - -// TODO(object) -// /// Returns refresh rate synchronization mode. -// pub inline fn vsync(core: *@This()) VSyncMode { -// return core.platform.vsync_mode; -// } - -// TODO(object) -// /// Sets the frame rate limit. Default 0 (unlimited) -// /// -// /// This is applied *in addition* to the vsync mode. -// pub inline fn setFrameRateLimit(core: *@This(), limit: u32) void { -// core.frame.target = limit; -// } - -// TODO(object) -// /// Returns the frame rate limit, or zero if unlimited. -// pub inline fn frameRateLimit(core: *@This()) u32 { -// return core.frame.target; -// } - -// TODO(object) -// /// Set the window size, in subpixel units. -// pub inline fn setSize(core: *@This(), value: Size) void { -// return core.platform.setSize(value); -// } - -// TODO(object) -// /// Returns the window size, in subpixel units. -// pub inline fn size(core: *@This()) Size { -// return core.platform.size; -// } - -// TODO(object) -// pub inline fn setCursorMode(core: *@This(), mode: CursorMode) void { -// return core.platform.setCursorMode(mode); -// } - -// TODO(object) -// pub inline fn cursorMode(core: *@This()) CursorMode { -// return core.platform.cursorMode(); -// } - -// TODO(object) -// pub inline fn setCursorShape(core: *@This(), cursor: CursorShape) void { -// return core.platform.setCursorShape(cursor); -// } - -// TODO(object) -// pub inline fn cursorShape(core: *@This()) CursorShape { -// return core.platform.cursorShape(); -// } - -// TODO(object) -// /// Sets the minimum target frequency of the input handling thread. -// /// -// /// Input handling (the main thread) runs at a variable frequency. The thread blocks until there are -// /// input events available, or until it needs to unblock in order to achieve the minimum target -// /// frequency which is your collaboration point of opportunity with the main thread. -// /// -// /// For example, by default (`setInputFrequency(1)`) mach-core will aim to invoke `updateMainThread` -// /// at least once per second (but potentially much more, e.g. once per every mouse movement or -// /// keyboard button press.) If you were to increase the input frequency to say 60hz e.g. -// /// `setInputFrequency(60)` then mach-core will aim to invoke your `updateMainThread` 60 times per -// /// second. -// /// -// /// An input frequency of zero implies unlimited, in which case the main thread will busy-wait. -// /// -// /// # Multithreaded mach-core behavior -// /// -// /// On some platforms, mach-core is able to handle input and rendering independently for -// /// improved performance and responsiveness. -// /// -// /// | Platform | Threading | -// /// |----------|-----------------| -// /// | Desktop | Multi threaded | -// /// | Browser | Single threaded | -// /// | Mobile | TBD | -// /// -// /// On single-threaded platforms, `update` and the (optional) `updateMainThread` callback are -// /// invoked in sequence, one after the other, on the same thread. -// /// -// /// On multi-threaded platforms, `init` and `deinit` are called on the main thread, while `update` -// /// is called on a separate rendering thread. The (optional) `updateMainThread` callback can be -// /// used in cases where you must run a function on the main OS thread (such as to open a native -// /// file dialog on macOS, since many system GUI APIs must be run on the main OS thread.) It is -// /// advised you do not use this callback to run any code except when absolutely neccessary, as -// /// it is in direct contention with input handling. -// /// -// /// APIs which are not accessible from a specific thread are declared as such, otherwise can be -// /// called from any thread as they are internally synchronized. -// pub inline fn setInputFrequency(core: *@This(), input_frequency: u32) void { -// core.input.target = input_frequency; -// } - -// TODO(object) -// /// Returns the input frequency, or zero if unlimited (busy-waiting mode) -// pub inline fn inputFrequency(core: *@This()) u32 { -// return core.input.target; -// } - -// TODO(object) -// /// Returns the actual number of frames rendered (`update` calls that returned) in the last second. -// /// -// /// This is updated once per second. -// pub inline fn frameRate(core: *@This()) u32 { -// return core.frame.rate; -// } - -// TODO(object) -// /// Returns the actual number of input thread iterations in the last second. See setInputFrequency -// /// for what this means. -// /// -// /// This is updated once per second. -// pub inline fn inputRate(core: *@This()) u32 { -// return core.input.rate; -// } - -// TODO(object) -// /// Returns the underlying native NSWindow pointer -// /// -// /// May only be called on macOS. -// pub fn nativeWindowCocoa(core: *@This()) *anyopaque { -// return core.platform.nativeWindowCocoa(); -// } - -// TODO(object) -// /// Returns the underlying native Windows' HWND pointer -// /// -// /// May only be called on Windows. -// pub fn nativeWindowWin32(core: *@This()) std.os.windows.HWND { -// return core.platform.nativeWindowWin32(); -// } - -pub fn presentFrame(core: *Core, core_mod: mach.Mod(Core)) !void { - var windows = core.windows.slice(); - while (windows.next()) |window_id| { - var core_window = core.windows.getValue(window_id); - defer core.windows.setValueRaw(window_id, core_window); - - mach.sysgpu.Impl.deviceTick(core_window.device); - - core_window.swap_chain.present(); - - // Update swapchain for the next frame - if (core_window.swap_chain_update.isSet()) blk: { - core_window.swap_chain_update.reset(); - - switch (core_window.vsync_mode) { - .triple => core.frame.target = 2 * core_window.refresh_rate, - else => core.frame.target = 0, - } - - if (core_window.width == 0 or core_window.height == 0) break :blk; - - core_window.swap_chain_descriptor.present_mode = switch (core_window.vsync_mode) { - .none => .immediate, - .double => .fifo, - .triple => .mailbox, - }; - - core_window.swap_chain_descriptor.width = core_window.width; - core_window.swap_chain_descriptor.height = core_window.height; - core_window.swap_chain.release(); - - core_window.swap_chain = core_window.device.createSwapChain(core_window.surface, &core_window.swap_chain_descriptor); - } - } - - // Record to frame rate frequency monitor that a frame was finished. - core.frame.tick(); - - switch (core.state) { - .running => {}, - .exiting => { - core.state = .deinitializing; - core_mod.run(core.on_exit.?); - core_mod.call(.deinit); - }, - .deinitializing => {}, - .exited => @panic("application not running"), - } -} - -pub fn exit(core: *Core) void { - core.state = .exiting; -} - inline fn requestAdapterCallback( context: *RequestAdapterResponse, status: gpu.RequestAdapterStatus, @@ -697,6 +532,7 @@ pub const Event = union(enum) { window_open: struct { window_id: mach.ObjectID, }, + zoom_gesture: ZoomGestureEvent, focus_gained: struct { window_id: mach.ObjectID, }, @@ -726,6 +562,17 @@ pub const ResizeEvent = struct { size: Size, }; +pub const ZoomGestureEvent = struct { + window_id: mach.ObjectID, + phase: GesturePhase, + zoom: f32, +}; + +pub const GesturePhase = enum { + began, + ended, +}; + pub const MouseButton = enum { left, right, diff --git a/src/core/Darwin.zig b/src/core/Darwin.zig index f489e5ae6d..e46a9a30fe 100644 --- a/src/core/Darwin.zig +++ b/src/core/Darwin.zig @@ -70,36 +70,19 @@ pub fn tick(core: *Core) !void { if (core_window.native) |native| { const native_window: *objc.app_kit.Window = native.window; - const native_view: *objc.mach.View = native.view; - - if (core.windows.updated(window_id, .color)) { - switch (core_window.color) { - .transparent => |wc| { - const color = objc.app_kit.Color.colorWithRed_green_blue_alpha( - wc.color.r, - wc.color.g, - wc.color.b, - wc.color.a, - ); - native_window.setBackgroundColor(color); - native_window.setTitlebarAppearsTransparent(true); - native_view.layer().setOpaque(false); - }, - .solid => |wc| { - const color = objc.app_kit.Color.colorWithRed_green_blue_alpha( - wc.color.r, - wc.color.g, - wc.color.b, - wc.color.a, - ); - native_window.setBackgroundColor(color); - native_window.setTitlebarAppearsTransparent(false); - native_view.layer().setOpaque(true); - }, - .system => { - native_window.setTitlebarAppearsTransparent(false); - native_view.layer().setOpaque(true); - }, + + if (core.windows.updated(window_id, .decoration_color)) { + if (core_window.decoration_color) |decoration_color| { + const color = objc.app_kit.Color.colorWithRed_green_blue_alpha( + decoration_color.r, + decoration_color.g, + decoration_color.b, + decoration_color.a, + ); + native_window.setBackgroundColor(color); + native_window.setTitlebarAppearsTransparent(true); + } else { + native_window.setTitlebarAppearsTransparent(false); } } @@ -115,6 +98,31 @@ pub fn tick(core: *Core) !void { frame.size.height = @floatFromInt(core.windows.get(window_id, .height)); native_window.setFrame_display_animate(native_window.frameRectForContentRect(frame), true, true); } + + if (core.windows.updated(window_id, .cursor_mode)) { + switch (core_window.cursor_mode) { + .normal => objc.app_kit.Cursor.unhide(), + .disabled, .hidden => objc.app_kit.Cursor.hide(), + } + } + + if (core.windows.updated(window_id, .cursor_shape)) { + const Cursor = objc.app_kit.Cursor; + + Cursor.T_pop(); + + switch (core_window.cursor_shape) { + .arrow => Cursor.arrowCursor().push(), + .ibeam => Cursor.IBeamCursor().push(), + .crosshair => Cursor.crosshairCursor().push(), + .pointing_hand => Cursor.pointingHandCursor().push(), + .not_allowed => Cursor.operationNotAllowedCursor().push(), + .resize_ns => Cursor.resizeUpDownCursor().push(), + .resize_ew => Cursor.resizeLeftRightCursor().push(), + .resize_all => Cursor.closedHandCursor().push(), + else => std.log.warn("Unsupported cursor", .{}), + } + } } else { try initWindow(core, window_id); } @@ -126,16 +134,47 @@ fn initWindow( window_id: mach.ObjectID, ) !void { var core_window = core.windows.getValue(window_id); + + const context = try core.allocator.create(Context); + context.* = .{ .core = core, .window_id = window_id }; // If the application is not headless, we need to make the application a genuine UI application // by setting the activation policy, this moves the process to foreground // TODO: Only call this on the first window creation _ = objc.app_kit.Application.sharedApplication().setActivationPolicy(objc.app_kit.ApplicationActivationPolicyRegular); + { + // On macos, the command key in particular seems to be handled a bit differently and tends to block the `keyUp` event + // from firing. To remedy this, we borrow the same fix GLFW uses and add a monitor. + const commandFn = struct { + pub fn commandFn(block: *objc.foundation.BlockLiteral(*Context), event: *objc.app_kit.Event) callconv(.C) ?*objc.app_kit.Event { + const core_: *Core = block.context.core; + const window_id_ = block.context.window_id; + + if (core_.windows.get(window_id_, .native)) |native| { + const native_window: *objc.app_kit.Window = native.window; + + if (event.modifierFlags() & objc.app_kit.EventModifierFlagCommand != 0) + native_window.sendEvent(event); + } + return event; + } + }.commandFn; + + var commandBlock = objc.foundation.stackBlockLiteral( + commandFn, + context, + null, + null, + ); + + _ = objc.app_kit.Event.addLocalMonitorForEventsMatchingMask_handler(objc.app_kit.EventMaskKeyUp, commandBlock.asBlock().copy()); + } + const metal_descriptor = try core.allocator.create(gpu.Surface.DescriptorFromMetalLayer); const layer = objc.quartz_core.MetalLayer.new(); defer layer.release(); - if (core_window.color == .transparent) layer.setOpaque(false); + if (core_window.transparent) layer.setOpaque(false); metal_descriptor.* = .{ .layer = layer, @@ -151,11 +190,11 @@ fn initWindow( const window_style = (if (core_window.display_mode == .fullscreen) objc.app_kit.WindowStyleMaskFullScreen else 0) | - (if (core_window.display_mode == .windowed) objc.app_kit.WindowStyleMaskTitled else 0) | - (if (core_window.display_mode == .windowed) objc.app_kit.WindowStyleMaskClosable else 0) | - (if (core_window.display_mode == .windowed) objc.app_kit.WindowStyleMaskMiniaturizable else 0) | - (if (core_window.display_mode == .windowed) objc.app_kit.WindowStyleMaskResizable else 0); - // (if (core_window.display_mode == .windowed) objc.app_kit.WindowStyleMaskFullSizeContentView else 0); + (if (core_window.decorated) objc.app_kit.WindowStyleMaskTitled else 0) | + (if (core_window.decorated) objc.app_kit.WindowStyleMaskClosable else 0) | + (if (core_window.decorated) objc.app_kit.WindowStyleMaskMiniaturizable else 0) | + (if (core_window.decorated) objc.app_kit.WindowStyleMaskResizable else 0) | + (if (!core_window.decorated) objc.app_kit.WindowStyleMaskFullSizeContentView else 0); const native_window_opt: ?*objc.app_kit.Window = objc.app_kit.Window.alloc().initWithContentRect_styleMask_backing_defer_screen( rect, @@ -165,13 +204,21 @@ fn initWindow( screen, ); if (native_window_opt) |native_window| { + const framebuffer_scale: f32 = @floatCast(native_window.backingScaleFactor()); + const window_width: f32 = @floatFromInt(core_window.width); + const window_height: f32 = @floatFromInt(core_window.height); + + core_window.framebuffer_width = @intFromFloat(window_width * framebuffer_scale); + core_window.framebuffer_height = @intFromFloat(window_height * framebuffer_scale); + native_window.setReleasedWhenClosed(false); var view = objc.mach.View.allocInit(); + + // initWithFrame is overridden in our MACHView, which creates a tracking area for mouse tracking + view = view.initWithFrame(rect); view.setLayer(@ptrCast(layer)); - const context = try core.allocator.create(Context); - context.* = .{ .core = core, .window_id = window_id }; // TODO(core): free this allocation { @@ -183,6 +230,14 @@ fn initWindow( ); view.setBlock_keyDown(keyDown.asBlock().copy()); + var insertText = objc.foundation.stackBlockLiteral( + ViewCallbacks.insertText, + context, + null, + null, + ); + view.setBlock_insertText(insertText.asBlock().copy()); + var keyUp = objc.foundation.stackBlockLiteral( ViewCallbacks.keyUp, context, @@ -190,33 +245,69 @@ fn initWindow( null, ); view.setBlock_keyUp(keyUp.asBlock().copy()); + + var flagsChanged = objc.foundation.stackBlockLiteral( + ViewCallbacks.flagsChanged, + context, + null, + null, + ); + view.setBlock_flagsChanged(flagsChanged.asBlock().copy()); + + var magnify = objc.foundation.stackBlockLiteral( + ViewCallbacks.magnify, + context, + null, + null, + ); + view.setBlock_magnify(magnify.asBlock().copy()); + + var mouseMoved = objc.foundation.stackBlockLiteral( + ViewCallbacks.mouseMoved, + context, + null, + null, + ); + view.setBlock_mouseMoved(mouseMoved.asBlock().copy()); + + var mouseDown = objc.foundation.stackBlockLiteral( + ViewCallbacks.mouseDown, + context, + null, + null, + ); + view.setBlock_mouseDown(mouseDown.asBlock().copy()); + + var mouseUp = objc.foundation.stackBlockLiteral( + ViewCallbacks.mouseUp, + context, + null, + null, + ); + view.setBlock_mouseUp(mouseUp.asBlock().copy()); + + var scrollWheel = objc.foundation.stackBlockLiteral( + ViewCallbacks.scrollWheel, + context, + null, + null, + ); + view.setBlock_scrollWheel(scrollWheel.asBlock().copy()); } native_window.setContentView(@ptrCast(view)); native_window.center(); native_window.setIsVisible(true); native_window.makeKeyAndOrderFront(null); - switch (core_window.color) { - .transparent => |wc| { - const color = objc.app_kit.Color.colorWithRed_green_blue_alpha( - wc.color.r, - wc.color.g, - wc.color.b, - wc.color.a, - ); - native_window.setBackgroundColor(color); - native_window.setTitlebarAppearsTransparent(true); - }, - .solid => |wc| { - const color = objc.app_kit.Color.colorWithRed_green_blue_alpha( - wc.color.r, - wc.color.g, - wc.color.b, - wc.color.a, - ); - native_window.setBackgroundColor(color); - }, - .system => {}, + if (core_window.decoration_color) |decoration_color| { + const color = objc.app_kit.Color.colorWithRed_green_blue_alpha( + decoration_color.r, + decoration_color.g, + decoration_color.b, + decoration_color.a, + ); + native_window.setBackgroundColor(color); + native_window.setTitlebarAppearsTransparent(true); } const string = objc.foundation.String.allocInit(); @@ -261,20 +352,33 @@ const WindowDelegateCallbacks = struct { const native_window: *objc.app_kit.Window = native.window; const frame = native_window.frame(); - const content_rect = native_window.contentRectForFrameRect(frame); - core_window.width = @intFromFloat(content_rect.size.width); - core_window.height = @intFromFloat(content_rect.size.height); - core_window.swap_chain_update.set(); - } + if (core_window.width != @as(u32, @intFromFloat(content_rect.size.width)) or core_window.height != @as(u32, @intFromFloat(content_rect.size.height))) { + core_window.width = @intFromFloat(content_rect.size.width); + core_window.height = @intFromFloat(content_rect.size.height); - core.windows.setValueRaw(block.context.window_id, core_window); + const framebuffer_scale: f32 = @floatCast(native_window.backingScaleFactor()); + const window_width: f32 = @floatFromInt(core_window.width); + const window_height: f32 = @floatFromInt(core_window.height); - core.pushEvent(.{ .window_resize = .{ - .window_id = block.context.window_id, - .size = .{ .width = core_window.width, .height = core_window.height }, - } }); + core_window.framebuffer_width = @intFromFloat(window_width * framebuffer_scale); + core_window.framebuffer_height = @intFromFloat(window_height * framebuffer_scale); + + core_window.swap_chain_descriptor.width = core_window.framebuffer_width; + core_window.swap_chain_descriptor.height = core_window.framebuffer_height; + core_window.swap_chain.release(); + + core_window.swap_chain = core_window.device.createSwapChain(core_window.surface, &core_window.swap_chain_descriptor); + + core.windows.setValueRaw(block.context.window_id, core_window); + + core.pushEvent(.{ .window_resize = .{ + .window_id = block.context.window_id, + .size = .{ .width = core_window.width, .height = core_window.height }, + } }); + } + } } pub fn windowShouldClose(block: *objc.foundation.BlockLiteral(*Context)) callconv(.C) bool { @@ -288,6 +392,74 @@ const WindowDelegateCallbacks = struct { }; const ViewCallbacks = struct { + pub fn mouseMoved(block: *objc.foundation.BlockLiteral(*Context), event: *objc.app_kit.Event) callconv(.C) void { + const core: *Core = block.context.core; + const window_id = block.context.window_id; + + const mouse_location = event.locationInWindow(); + + const window_height: f32 = @floatFromInt(core.windows.get(window_id, .height)); + + core.pushEvent(.{ .mouse_motion = .{ + .window_id = window_id, + .pos = .{ .x = mouse_location.x, .y = window_height - mouse_location.y }, + } }); + } + + pub fn mouseDown(block: *objc.foundation.BlockLiteral(*Context), event: *objc.app_kit.Event) callconv(.C) void { + const core: *Core = block.context.core; + const window_id = block.context.window_id; + + core.pushEvent(.{ .mouse_press = .{ + .window_id = window_id, + .button = @enumFromInt(event.buttonNumber()), + .pos = .{ .x = event.locationInWindow().x, .y = event.locationInWindow().y }, + .mods = machModifierFromModifierFlag(event.modifierFlags()), + } }); + } + pub fn mouseUp(block: *objc.foundation.BlockLiteral(*Context), event: *objc.app_kit.Event) callconv(.C) void { + const core: *Core = block.context.core; + const window_id = block.context.window_id; + + core.pushEvent(.{ .mouse_release = .{ + .window_id = window_id, + .button = @enumFromInt(event.buttonNumber()), + .pos = .{ .x = event.locationInWindow().x, .y = event.locationInWindow().y }, + .mods = machModifierFromModifierFlag(event.modifierFlags()), + } }); + } + + pub fn scrollWheel(block: *objc.foundation.BlockLiteral(*Context), event: *objc.app_kit.Event) callconv(.C) void { + const core: *Core = block.context.core; + const window_id = block.context.window_id; + + var scroll_delta_x = event.scrollingDeltaX(); + var scroll_delta_y = event.scrollingDeltaY(); + + if (event.hasPreciseScrollingDeltas()) { + scroll_delta_x *= 0.1; + scroll_delta_y *= 0.1; + } + + core.pushEvent(.{ .mouse_scroll = .{ + .window_id = window_id, + .xoffset = @floatCast(scroll_delta_x), + .yoffset = @floatCast(scroll_delta_y), + } }); + } + + // This is currently only supported on macOS using a trackpad + pub fn magnify(block: *objc.foundation.BlockLiteral(*Context), event: *objc.app_kit.Event) callconv(.C) void { + const core: *Core = block.context.core; + const window_id = block.context.window_id; + + core.pushEvent(.{ .zoom_gesture = .{ + .window_id = window_id, + .zoom = @floatCast(event.magnification()), + .phase = machPhaseFromPhase(event.phase()), + } }); + } + pub fn keyDown(block: *objc.foundation.BlockLiteral(*Context), event: *objc.app_kit.Event) callconv(.C) void { const core: *Core = block.context.core; const window_id = block.context.window_id; @@ -306,6 +478,16 @@ const ViewCallbacks = struct { } } + pub fn insertText(block: *objc.foundation.BlockLiteral(*Context), event: *objc.app_kit.Event, codepoint: u32) callconv(.C) void { + _ = event; // autofix + const core: *Core = block.context.core; + const window_id = block.context.window_id; + core.pushEvent(.{ .char_input = .{ + .codepoint = @intCast(codepoint), + .window_id = window_id, + } }); + } + pub fn keyUp(block: *objc.foundation.BlockLiteral(*Context), event: *objc.app_kit.Event) callconv(.C) void { const core: *Core = block.context.core; const window_id = block.context.window_id; @@ -316,8 +498,43 @@ const ViewCallbacks = struct { .mods = machModifierFromModifierFlag(event.modifierFlags()), } }); } + + pub fn flagsChanged(block: *objc.foundation.BlockLiteral(*Context), event: *objc.app_kit.Event) callconv(.C) void { + const core: *Core = block.context.core; + const window_id = block.context.window_id; + + const key = machKeyFromKeycode(event.keyCode()); + const mods = machModifierFromModifierFlag(event.modifierFlags()); + + const key_flag = switch (key) { + .left_shift, .right_shift => objc.app_kit.EventModifierFlagShift, + .left_control, .right_control => objc.app_kit.EventModifierFlagControl, + .left_alt, .right_alt => objc.app_kit.EventModifierFlagOption, + .left_super, .right_super => objc.app_kit.EventModifierFlagCommand, + .caps_lock => objc.app_kit.EventModifierFlagCapsLock, + else => 0, + }; + + if (event.modifierFlags() & key_flag != 0) { + if (core.input_state.isKeyPressed(key)) { + core.pushEvent(.{ .key_release = .{ .window_id = window_id, .key = key, .mods = mods } }); + } else { + core.pushEvent(.{ .key_press = .{ .window_id = window_id, .key = key, .mods = mods } }); + } + } else { + core.pushEvent(.{ .key_release = .{ .window_id = window_id, .key = key, .mods = mods } }); + } + } }; +fn machPhaseFromPhase(phase: objc.app_kit.EventPhase) Core.GesturePhase { + return switch (phase) { + objc.app_kit.EventPhaseBegan => .began, + objc.app_kit.EventPhaseEnded => .ended, + else => .began, + }; +} + fn machModifierFromModifierFlag(modifier_flag: usize) Core.KeyMods { var modifier: Core.KeyMods = .{ .alt = false, diff --git a/src/core/Windows.zig b/src/core/Windows.zig index 3c375c1921..b600d57e27 100644 --- a/src/core/Windows.zig +++ b/src/core/Windows.zig @@ -40,7 +40,6 @@ pub fn tick(core: *Core) !void { if (native_opt) |native| { _ = native; // autofix - var msg: w.MSG = undefined; while (w.PeekMessageW(&msg, null, 0, 0, w.PM_REMOVE) != 0) { _ = w.TranslateMessage(&msg); @@ -193,8 +192,7 @@ fn wndProc(wnd: w.HWND, msg: u32, wParam: w.WPARAM, lParam: w.LPARAM) callconv(w const core = context.core; const window_id = context.window_id; - var window = core.windows.getValue(window_id); - defer core.windows.setValueRaw(window_id, window); + var core_window = core.windows.getValue(window_id); switch (msg) { w.WM_CLOSE => { @@ -205,18 +203,28 @@ fn wndProc(wnd: w.HWND, msg: u32, wParam: w.WPARAM, lParam: w.LPARAM) callconv(w const width: u32 = @as(u32, @intCast(lParam & 0xFFFF)); const height: u32 = @as(u32, @intCast((lParam >> 16) & 0xFFFF)); - window.width = width; - window.height = height; + if (core_window.width != width or core_window.height != height) { + // Recreate the swap_chain + core_window.swap_chain.release(); + core_window.swap_chain_descriptor.width = width; + core_window.swap_chain_descriptor.height = height; + core_window.swap_chain = core_window.device.createSwapChain(core_window.surface, &core_window.swap_chain_descriptor); - core.pushEvent(.{ .window_resize = .{ .window_id = window_id, .size = .{ .width = width, .height = height } } }); + core_window.width = width; + core_window.height = height; + core_window.framebuffer_width = width; + core_window.framebuffer_height = height; + + core.pushEvent(.{ .window_resize = .{ .window_id = window_id, .size = .{ .width = width, .height = height } } }); + + core.windows.setValueRaw(window_id, core_window); + } // TODO (win32): only send resize event when sizing is done. // the main mach loops does not run while resizing. // Which means if events are pushed here they will // queue up until resize is done. - window.swap_chain_update.set(); - return 0; }, w.WM_KEYDOWN, w.WM_KEYUP, w.WM_SYSKEYDOWN, w.WM_SYSKEYUP => { @@ -225,6 +233,7 @@ fn wndProc(wnd: w.HWND, msg: u32, wParam: w.WPARAM, lParam: w.LPARAM) callconv(w if (msg == w.WM_SYSKEYDOWN and vkey == w.VK_F4) { core.pushEvent(.{ .close = .{ .window_id = window_id } }); + return 0; } @@ -235,7 +244,7 @@ fn wndProc(wnd: w.HWND, msg: u32, wParam: w.WPARAM, lParam: w.LPARAM) callconv(w // right alt sends left control first var next: w.MSG = undefined; const time = w.GetMessageTime(); - if (window.native) |native| { + if (core_window.native) |native| { if (w.PeekMessageW(&next, native.window, 0, 0, w.PM_NOREMOVE) != 0 and next.time == time and (next.message == msg or (msg == w.WM_SYSKEYDOWN and next.message == w.WM_KEYUP)) and @@ -276,7 +285,7 @@ fn wndProc(wnd: w.HWND, msg: u32, wParam: w.WPARAM, lParam: w.LPARAM) callconv(w return 0; }, w.WM_CHAR => { - if (window.native) |*native| { + if (core_window.native) |*native| { const char: u16 = @truncate(wParam); var chars: []const u16 = undefined; if (native.surrogate != 0) {