diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0d478867..8e06e73a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -3,8 +3,8 @@ on: workflow_dispatch: push: pull_request: - branches: - - main + types: [opened, reopened] + branches: [main] jobs: run-tests: @@ -16,7 +16,7 @@ jobs: os: - macos-latest - windows-latest - - ubuntu-latest + - ubuntu-22.04 node: - 16 - 22 diff --git a/Cargo.lock b/Cargo.lock index 92dfc02a..3774e090 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1504,6 +1504,17 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" +[[package]] +name = "raw-window-metal" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2000e45d7daa9b6d946e88dfa1d7ae330424a81918a6545741821c989eb80a9" +dependencies = [ + "objc2", + "objc2-foundation", + "objc2-quartz-core", +] + [[package]] name = "rayon" version = "1.10.0" @@ -1731,10 +1742,12 @@ dependencies = [ "objc", "once_cell", "raw-window-handle 0.6.2", + "raw-window-metal", "rayon", "serde", "serde_json", "skia-safe", + "spin_sleep", "vulkano", "winit", ] @@ -1813,6 +1826,15 @@ dependencies = [ "serde", ] +[[package]] +name = "spin_sleep" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64bd7227d85bfd1b8df51e0d83da36d9baaee85eb75730386ef8e3ab6f2a2ea3" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "static_assertions" version = "1.1.0" @@ -2555,9 +2577,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winit" -version = "0.30.5" +version = "0.30.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0be9e76a1f1077e04a411f0b989cbd3c93339e1771cb41e71ac4aee95bfd2c67" +checksum = "f5d74280aabb958072864bff6cfbcf9025cf8bfacdde5e32b5e12920ef703b0f" dependencies = [ "ahash", "android-activity", diff --git a/Cargo.toml b/Cargo.toml index 97e86834..9c96340e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,9 +14,9 @@ crate-type = ["cdylib"] lto = "fat" [features] -metal = ["skia-safe/metal", "dep:metal", "dep:raw-window-handle", "dep:core-graphics-types", "dep:cocoa", "dep:objc"] +metal = ["skia-safe/metal", "dep:metal", "dep:raw-window-handle", "dep:raw-window-metal", "dep:core-graphics-types", "dep:cocoa", "dep:objc"] vulkan = ["skia-safe/vulkan", "winit/rwh_05", "dep:ash", "dep:vulkano"] -window = ["dep:winit"] +window = ["dep:winit", "dep:spin_sleep"] [dependencies] neon = "1.0" @@ -37,9 +37,11 @@ vulkano = { version = "0.34.1", optional = true } # metal metal = { version = "0.29", optional = true } raw-window-handle = { version = "0.6", optional = true } +raw-window-metal = { version = "1.0.0", optional = true } core-graphics-types = { version = "0.1.1", optional = true } cocoa = { version = "0.26.0", optional = true } objc = { version = "0.2.7", optional = true } # window -winit = { version = '0.30.5', features = ["serde"], optional = true } +winit = { version = '0.30.8', features = ["serde"], optional = true } +spin_sleep = {version = "1.2.1", optional = true } \ No newline at end of file diff --git a/lib/classes/css.js b/lib/classes/css.js index 0cc7ecf1..b31793fe 100644 --- a/lib/classes/css.js +++ b/lib/classes/css.js @@ -203,12 +203,15 @@ function parseTextDecoration(str){ // -- Window Types ----------------------------------------------------------------------- + let cursorTypes = [ - "default", "crosshair", "hand", "arrow", "move", "text", "wait", "help", "progress", "not-allowed", "context-menu", - "cell", "vertical-text", "alias", "copy", "no-drop", "grab", "grabbing", "all-scroll", "zoom-in", "zoom-out", - "e-resize", "n-resize", "ne-resize", "nw-resize", "s-resize", "se-resize", "sw-resize", "w-resize", "ew-resize", - "ns-resize", "nesw-resize", "nwse-resize", "col-resize", "row-resize", "none" + "default", "none", "context-menu", "help", "pointer", "progress", "wait", "cell", "crosshair", + "text", "vertical-text", "alias", "copy", "move", "no-drop", "not-allowed", "grab", "grabbing", + "e-resize", "n-resize", "ne-resize", "nw-resize", "s-resize", "se-resize", "sw-resize", "w-resize", + "ew-resize", "ns-resize", "nesw-resize", "nwse-resize", "col-resize", "row-resize", "all-scroll", + "zoom-in", "zoom-out", ] + function parseCursor(str){ return cursorTypes.includes(str) } diff --git a/lib/classes/gui.js b/lib/classes/gui.js index 47358a01..ca8be122 100644 --- a/lib/classes/gui.js +++ b/lib/classes/gui.js @@ -15,17 +15,47 @@ const checkSupport = () => { class App extends RustClass{ static #locale = process.env.LC_ALL || process.env.LC_MESSAGES || process.env.LANG || process.env.LANGUAGE - #running - #fps + #events = 'native' // `native` for an OS event loop or `node` to poll for ui-events from node + #started = false // whether the `eventLoop` property is permanently set + #active = false // whether launch() can be called to start/resume the event loop + #launcher // timer set by opening windows to ensure app is launched soon after + + #windows = [] + #frames = {} + #fps = 60 constructor(){ super(App) - this.#running = false - this.#fps = 60 + + // set the callback to use for event dispatch & rendering + if (neon.App) this.ƒ("register", this.#dispatch.bind(this)) + + // track new windows and schedule launch if needed + Window.events.on('open', win => { + this.#windows.push(win) + this.#frames[win.id] = 0 + if (!this.#launcher) this.#launcher = setImmediate( () => this.launch() ) + this.ƒ("openWindow", JSON.stringify(win.state), core(win.canvas.pages[win.state.page-1])) + this.emit("open", {type:"open", target:win}) + }) + + // drop closed windows + Window.events.on('close', win => { + this.#windows = this.#windows.filter(w => w!==win) + this.ƒ("closeWindow", win.id) + this.emit("close", {type:"close", target:win}) + }) } - get windows(){ return [...GUI.windows] } - get running(){ return this.#running } + get windows(){ return [...this.#windows] } + get running(){ return this.#started } + get eventLoop(){ return this.#events } + set eventLoop(mode){ + if (this.#started) throw new Error("Cannot alter event loop after it has begun") + if (['native', 'node'].includes(mode) && mode != this.#events){ + this.#events = this.ƒ("setMode", mode) + } + } get fps(){ return this.#fps } set fps(rate){ checkSupport() @@ -36,131 +66,152 @@ class App extends RustClass{ launch(){ checkSupport() + clearImmediate(this.#launcher) + this.#started = true + if (this.#active){ + return console.error('Application is already running') + } + this.#active = true - if (this.#running) return console.error('Application is already running') - this.#running = true - clearTimeout(GUI.launcher) + return this.ƒ('activate').finally(() => { + this.#active = false + this.#launcher = null + }) - // begin event loop (and never return) - this.ƒ("launch", args => { - let {ui, state, geom} = JSON.parse(args) + } + + #eachWindow(updates, callback){ + for (const [id, payload] of Object.entries(updates || {})){ + let win = this.#windows.find(win => win.id == id) + if (win) callback(win, payload) + } + } + + #dispatch(isFrame, payload){ + let {geom, state, ui} = JSON.parse(payload) - // in the initial roundtrip only, merge the autogenerated window locations with the specs - for (const [id, {top, left}] of Object.entries(geom || {})){ - GUI.getWindow(id, win => { - win.left = win.left || left - win.top = win.top || top - }) + // merge autogenerated window locations into newly opened windows + if (geom) this.#eachWindow(geom, (win, {top, left}) => { + win.left = win.left || left + win.top = win.top || top + }) + + // update state of windows that are still active and mark others as closed + if (state) this.#windows = this.#windows.filter(win => { + // keep active windows and new ones still waiting for a `geom` roundtrip to set their initial position + if (win.id in state || win.top === undefined){ + Object.assign(win, state[win.id]) + return true } - // update local state based on ui modifications (and evict GUI.windows that have been closed) - if (state) GUI.windows = GUI.windows.filter(win => { - return win.state.id in (state || {}) && Object.assign(win, state[win.state.id]) - }) - - // deliver ui events to corresponding windows - for (const [id, events] of Object.entries(ui || {})){ - GUI.getWindow(id, (win, frame) => { - let modifiers = {} - for (const [[type, e]] of events.map(o => Object.entries(o))){ - switch(type){ - case 'modifiers': - var {control_key:ctrlKey, alt_key:altKey, super_key:metaKey, shift_key:shiftKey} = e - modifiers = {ctrlKey, altKey, metaKey, shiftKey} - break - - case 'mouse': - var {button, x, y, pageX, pageY} = e - e.events.forEach(type => win.emit(type, {x, y, pageX, pageY, button, ...modifiers})) - break - - case 'input': - win.emit(type, {data:e, inputType:'insertText'}) - break - - case 'composition': - win.emit(e.event, {data:e.data, locale:App.#locale}) - break - - case 'keyboard': - var {event, key, code, location, repeat} = e, - defaults = true; - - win.emit(event, {key, code, location, repeat, ...modifiers, - preventDefault:() => defaults = false - }) - - // apply default keybindings unless e.preventDefault() was run - if (defaults && event=='keydown' && !repeat){ - let {ctrlKey, altKey, metaKey} = modifiers - if ( (metaKey && key=='w') || (ctrlKey && key=='c') || (altKey && key=='F4') ){ - win.close() - }else if ( (metaKey && key=='f') || (altKey && key=='F8') ){ - win.fullscreen = !win.fullscreen - } - } - break - - case 'focus': - if (e) win.emit('focus') - else win.emit('blur') - break - - case 'resize': - if (win.fit == 'resize'){ - win.ctx.prop('size', e.width, e.height) - win.canvas.prop('width', e.width) - win.canvas.prop('height', e.height) - } - win.emit(type, e) - break - - case 'move': - case 'wheel': - win.emit(type, e) - break - - case 'fullscreen': - win.emit(type, {enabled: e}) - break - - default: - console.log(type, e); + // but otherwise evict all windows that have been closed + this.emit('close', win) + }) + + // deliver ui events to corresponding windows + if (ui) this.#eachWindow(ui, (win, events) => { + for (const [[type, e]] of events.map(o => Object.entries(o))){ + switch(type){ + case 'mouse': + var {button, buttons, point, page_point:{x:pageX, y:pageY}, modifiers} = e + win.emit(e.event, {button, buttons, ...point, pageX, pageY, ...modifiers}) + break + + case 'input': + let [data, inputType] = e + win.emit(type, {data, inputType}) + break + + case 'composition': + win.emit(e.event, {data:e.data, locale:App.#locale}) + break + + case 'keyboard': + var {event, key, code, location, repeat, modifiers} = e, + defaults = true; + + win.emit(event, {key, code, location, repeat, ...modifiers, + preventDefault:() => defaults = false + }) + + // apply default keybindings unless e.preventDefault() was run + if (defaults && event=='keydown' && !repeat){ + let {ctrlKey, altKey, metaKey} = modifiers + if ( (metaKey && key=='w') || (ctrlKey && key=='c') || (altKey && key=='F4') ){ + win.close() + }else if ( (metaKey && key=='f') || (altKey && key=='F8') ){ + win.fullscreen = !win.fullscreen + } } - } - }) - } + break + + case 'focus': + if (e) win.emit('focus') + else win.emit('blur') + break + + case 'resize': + if (win.fit == 'resize'){ + win.ctx.prop('size', e.width, e.height) + win.canvas.prop('width', e.width) + win.canvas.prop('height', e.height) + } + win.emit(type, e) + break - // provide frame updates to prompt redraws - GUI.nextFrame((win, frame) => { - if (frame==0) win.emit("setup") - win.emit("frame", {frame}) - if (win.listenerCount('draw')){ - win.canvas.width = win.canvas.width - win.emit("draw", {frame}) - } - }) + case 'move': + case 'wheel': + win.emit(type, e) + break - // refresh lazily if not doing a flipbook animation - this.ƒ('setRate', GUI.needsFrameUpdates() ? this.#fps : 0) + case 'fullscreen': + win.emit(type, {enabled: e}) + break - // update the display - return [ - JSON.stringify( GUI.windows.map(win => win.state) ), - GUI.windows.map(win => core(win.canvas.pages[win.page-1]) ) - ] + default: + console.log(type, e); + } + } }) - GUI.windows = [] // if the launch call exited, the last window was closed + // provide frame updates to prompt redraws + if (isFrame) for (let win of this.#windows){ + let frame = ++this.#frames[win.id] + + if (frame==0) win.emit("setup") + win.emit("frame", {frame}) + if (win.listenerCount('draw')){ + win.canvas.getContext("2d").reset() + win.emit("draw", {frame}) + } + } + + // if this is a full roundtrip, return window state & content + return isFrame && [ + JSON.stringify( this.#windows.map(win => win.state) ), + this.#windows.map(win => core(win.canvas.pages[win.page-1]) ) + ] } quit(){ this.ƒ("quit") } + + [REPR](depth, options) { + let {eventLoop, fps, windows} = this + return `App ${inspect({eventLoop, fps, windows}, Object.assign(options, { + depth:1, customInspect:false + }))}` + } } +// Mix the EventEmitter properties into App +Object.assign(App.prototype, EventEmitter.prototype) + class Window extends EventEmitter{ - static #kwargs = "left,top,width,height,title,page,background,fullscreen,cursor,fit,visible,resizable".split(/,/) + static events = new EventEmitter() + static #kwargs = "id,left,top,width,height,title,page,background,fullscreen,cursor,fit,visible,resizable".split(/,/) + static #nextID = 1 #canvas #state @@ -189,21 +240,23 @@ class Window extends EventEmitter{ width, height, cursor: "default", - cursorHidden: false, fit: "contain", - id: Math.random().toString(16) + id: Window.#nextID++ } Object.assign(this, {canvas}, Object.fromEntries( Object.entries(opts).filter(([k, v]) => Window.#kwargs.includes(k) && v!==undefined) )) - GUI.openWindow(this) + Window.events.emit('open', this) } - get state(){ return this.#state } + get state(){ return {...this.#state} } get ctx(){ return this.#canvas.pages[this.page-1] } + get id(){ return this.#state.id } + set id(id){ if (id!=this.id) throw new Error("Window IDs are immutable") } + get canvas(){ return this.#canvas } set canvas(canvas){ if (canvas instanceof Canvas){ @@ -225,11 +278,10 @@ class Window extends EventEmitter{ get title(){ return this.#state.title } set title(txt){ this.#state.title = (txt != null ? txt : '').toString() } - get cursor(){ return this.#state.cursorHidden ? 'none' : this.#state.cursor } + get cursor(){ return this.#state.cursor } set cursor(icon){ if (css.cursor(icon)){ - this.#state.cursorHidden = icon == 'none' - if (icon != 'none') this.#state.cursor = icon + this.#state.cursor = icon } } @@ -269,7 +321,7 @@ class Window extends EventEmitter{ catch(err){ console.error(err) } } - close(){ GUI.closeWindow(this) } + close(){ Window.events.emit('close', this) } [REPR](depth, options) { let info = Object.fromEntries(Window.#kwargs.map(k => [k, this.#state[k]])) @@ -277,39 +329,4 @@ class Window extends EventEmitter{ } } -const GUI = { - App: new App(), - windows: [], - frames: new WeakMap(), - launcher: null, - - nextFrame(callback){ - GUI.windows.forEach(win => { - let frame = GUI.frames.get(win) || 0 - GUI.frames.set(win, frame + 1) - callback(win, frame) - }) - }, - - needsFrameUpdates(){ - let names = GUI.windows.map(win => win.eventNames()).flat() - return (names.includes('frame') || names.includes('draw')) - }, - - getWindow(id, callback){ - GUI.windows.filter(w => w.state.id==id).forEach(win => callback(win)) - }, - - openWindow(win){ - GUI.windows.push(win) - if (!GUI.launcher) GUI.launcher = setTimeout( () => GUI.App.launch() ) - neon.App.openWindow(JSON.stringify(win.state), core(win.canvas.pages[win.state.page-1])) - }, - - closeWindow(win){ - GUI.windows = GUI.windows.filter(w => w !== win) - neon.App.closeWindow(win.state.id) - } -} - -module.exports = {App:GUI.App, Window} +module.exports = {App:new App(), Window} diff --git a/lib/index.d.ts b/lib/index.d.ts index b8c254b9..17c60595 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -120,7 +120,7 @@ declare var DOMMatrix: { // Canvas // -export type ExportFormat = "png" | "jpg" | "jpeg" | "webp" | "pdf" | "svg"; +export type ExportFormat = "png" | "jpg" | "jpeg" | "webp" | "raw" | "pdf" | "svg"; export interface RenderOptions { /** Page to export: Defaults to 1 (i.e., first page) */ @@ -137,6 +137,12 @@ export interface RenderOptions { /** Convert text to paths for SVG exports */ outline?: boolean + + /** Number of samples used for antialising each pixel */ + msaa?: number | false + + /** Color type to use when exporting in "raw" format */ + colorType?: ColorType } export interface SaveOptions extends RenderOptions { @@ -144,6 +150,15 @@ export interface SaveOptions extends RenderOptions { format?: ExportFormat } +export interface EngineDetails { + renderer: "CPU" | "GPU" + api: "Vulkan" | "Metal" + device: string + driver?: string + threads: number + error?: string +} + export class Canvas { /** @internal */ constructor(width?: number, height?: number) @@ -158,6 +173,7 @@ export class Canvas { get gpu(): boolean set gpu(enabled: boolean) + readonly engine: EngineDetails saveAs(filename: string, options?: SaveOptions): Promise toBuffer(format: ExportFormat, options?: RenderOptions): Promise @@ -388,6 +404,8 @@ export const FontLibrary: FontLibrary // import { EventEmitter } from "stream"; +export type EventLoopMode = "node" | "native" +export type TextInputType = "insertText" | "deleteContentBackward" | "deleteContentForward" | "insertLineBreak" | "insertCompositionText" export type FitStyle = "none" | "contain-x" | "contain-y" | "contain" | "cover" | "fill" | "scale-down" | "resize" export type CursorStyle = "default" | "crosshair" | "hand" | "arrow" | "move" | "text" | "wait" | "help" | "progress" | "not-allowed" | "context-menu" | "cell" | "vertical-text" | "alias" | "copy" | "no-drop" | "grab" | "grabbing" | "all-scroll" | "zoom-in" | "zoom-out" | @@ -415,6 +433,7 @@ type MouseEventProps = { pageX: number; pageY: number; button: number; + buttons: number, ctrlKey: boolean; altKey: boolean; metaKey: boolean; @@ -440,7 +459,7 @@ type WindowEvents = { keyup: KeyboardEventProps input: { data: string - inputType: 'insertText' + inputType: TextInputType }; wheel: { deltaX: number; deltaY: number } fullscreen: { enabled: boolean } @@ -481,12 +500,16 @@ export class Window extends EventEmitter<{ close(): void } -export interface App{ +export interface App extends EventEmitter<{ + "open": {type: "open", target: Window}, + "close": {type: "close", target: Window}, +}>{ readonly windows: Window[] readonly running: boolean + eventLoop: EventLoopMode fps: number - launch(): void + launch(): Promise quit(): void } diff --git a/lib/index.mjs b/lib/index.mjs index 21f67e8d..66db7184 100644 --- a/lib/index.mjs +++ b/lib/index.mjs @@ -2,7 +2,7 @@ // Skia Canvas — ES Module version // -import skia_canvas from './classes/index.js' +import skia_canvas from './index.js' const { Canvas, CanvasGradient, CanvasPattern, CanvasTexture, @@ -14,6 +14,7 @@ const { } = skia_canvas export { + skia_canvas as default, Canvas, CanvasGradient, CanvasPattern, CanvasTexture, Image, ImageData, loadImage, loadImageData, Path2D, DOMPoint, DOMMatrix, DOMRect, diff --git a/src/context/page.rs b/src/context/page.rs index ceb72fc5..78d06399 100644 --- a/src/context/page.rs +++ b/src/context/page.rs @@ -29,6 +29,7 @@ pub struct PageRecorder{ matrix: Matrix, clip: Option, changed: bool, + rev: usize, } impl PageRecorder{ @@ -36,7 +37,7 @@ impl PageRecorder{ let mut rec = PictureRecorder::new(); rec.begin_recording(bounds, None); rec.recording_canvas().unwrap().save(); // start at depth 2 - PageRecorder{ current:rec, changed:false, layers:vec![], cache:None, matrix:Matrix::default(), clip:None, bounds } + PageRecorder{ current:rec, changed:false, layers:vec![], cache:None, matrix:Matrix::default(), clip:None, bounds, rev:0 } } pub fn append(&mut self, f:F) @@ -49,7 +50,9 @@ impl PageRecorder{ } pub fn set_bounds(&mut self, bounds:Rect){ + let rev = self.rev; *self = PageRecorder::new(bounds); + self.rev = rev + 1; } pub fn update_bounds(&mut self, bounds:Rect){ @@ -112,6 +115,7 @@ impl PageRecorder{ Page{ layers: self.layers.clone(), bounds: self.bounds, + rev: self.rev, } } @@ -135,6 +139,13 @@ impl PageRecorder{ pub struct Page{ pub layers: Vec, pub bounds: Rect, + pub rev: usize +} + +impl PartialEq for Page { + fn eq(&self, other: &Self) -> bool { + self.rev == other.rev && self.layers.len() == other.layers.len() + } } impl Page{ diff --git a/src/gpu/metal.rs b/src/gpu/metal.rs index e7f0a668..acca5dab 100644 --- a/src/gpu/metal.rs +++ b/src/gpu/metal.rs @@ -1,31 +1,17 @@ -#![allow(dead_code)] -#![allow(unused_imports)] use std::cell::RefCell; use std::sync::{Arc, OnceLock}; use std::time::{Instant, Duration}; -use cocoa::{appkit::NSView, base::id as cocoa_id}; -use core_graphics_types::geometry::CGSize; use metal::{ + foreign_types::{ForeignType, ForeignTypeRef}, CommandQueue, Device, MTLPixelFormat, MetalLayer, MTLDeviceLocation, - foreign_types::{ForeignType, ForeignTypeRef} }; -use skia_safe::{scalar, ImageInfo, ColorType, Size, Surface, Data}; +use skia_safe::{scalar, ImageInfo, ColorType, Size, Surface}; use skia_safe::gpu::{ mtl, direct_contexts, backend_render_targets, surfaces, Budgeted, DirectContext, SurfaceOrigin }; -use objc::runtime::YES; -pub use objc::rc::autoreleasepool; +use objc::rc::autoreleasepool; use serde_json::{json, Value}; -#[cfg(feature = "window")] -use winit::{ - dpi::{LogicalSize, PhysicalSize}, - platform::macos::WindowExtMacOS, - window::Window, - raw_window_handle::HasWindowHandle, - event_loop::ActiveEventLoop, -}; - thread_local!( static MTL_CONTEXT: RefCell> = const { RefCell::new(None) }; ); static MTL_CONTEXT_LIFESPAN:Duration = Duration::from_secs(5); static MTL_STATUS: OnceLock = OnceLock::new(); @@ -117,7 +103,6 @@ impl MetalEngine { } pub struct MetalContext { device: Device, - queue: CommandQueue, context: DirectContext, msaa: Vec, last_use: Instant, @@ -139,7 +124,7 @@ impl MetalContext{ *s==0 || device.supports_texture_sample_count(*s as _) }).collect(); direct_contexts::make_metal(&backend, None) - .map(|context| MetalContext{device, queue, context, msaa, last_use}) + .map(|context| MetalContext{device, context, msaa, last_use}) }) }) } @@ -178,110 +163,119 @@ impl MetalContext{ // Windowed rendering // +#[cfg(feature = "window")] +use { + skia_safe::{Matrix, Color}, + raw_window_metal::Layer, + core_graphics_types::geometry::CGSize, + objc::{msg_send, sel, sel_impl, runtime::{YES, NO, Object}}, + winit::{ + dpi::PhysicalSize, + window::Window, + raw_window_handle::{RawWindowHandle, HasWindowHandle}, + event_loop::ActiveEventLoop, + }, + crate::context::page::Page, +}; + +#[allow(non_upper_case_globals)] +#[link(name = "QuartzCore", kind = "framework")] +extern "C" { + static kCAGravityTopLeft: *mut Object; + static kCAGravityBottomLeft: *mut Object; +} + +#[cfg(feature = "window")] pub struct MetalRenderer { - layer: Arc, - device: Arc, + window: Arc, + backend: MetalBackend, + layer: MetalLayer, + resized: bool, } -// The windowed renderer -impl MetalRenderer { +#[cfg(feature = "window")] +impl MetalRenderer{ pub fn for_window(_event_loop: &ActiveEventLoop, window:Arc) -> Self { - let device = Device::system_default().expect("no device found"); + let device = Device::system_default().expect("Metal device not found"); - let raw_window_handle = window + let raw_window = window .window_handle() .expect("Failed to retrieve a window handle") .as_raw(); - let layer = { - let draw_size = window.inner_size(); - let layer = MetalLayer::new(); - layer.set_device(&device); - layer.set_pixel_format(MTLPixelFormat::BGRA8Unorm); - layer.set_presents_with_transaction(false); - layer.set_opaque(false); - layer.set_framebuffer_only(false); // to enable blend modes - - unsafe { - let view = match raw_window_handle { - raw_window_handle::RawWindowHandle::AppKit(appkit) => { - appkit.ns_view.as_ptr() - } - _ => panic!("Wrong window handle type"), - } as cocoa_id; - view.setWantsLayer(YES); - view.setLayer(layer.as_ref() as *const _ as _); - } - layer.set_drawable_size(CGSize::new(draw_size.width as f64, draw_size.height as f64)); - layer + let raw_layer = match raw_window { + RawWindowHandle::AppKit(handle) => unsafe { Layer::from_ns_view(handle.ns_view) }, + RawWindowHandle::UiKit(handle) => unsafe { Layer::from_ui_view(handle.ui_view) }, + _ => panic!("Unsupported window handle type"), }; - Self { layer: Arc::new(layer), device: Arc::new(device) } - } + let layer = unsafe{ + let mtl_layer = MetalLayer::from_ptr(raw_layer.into_raw().as_ptr().cast()); + let gravity = match msg_send![mtl_layer.as_ptr(), contentsAreFlipped] { + YES => kCAGravityBottomLeft, + NO => kCAGravityTopLeft, + }; + let _: () = msg_send![mtl_layer.as_ptr(), setContentsGravity: gravity]; + mtl_layer + }; + layer.set_device(&device); + layer.set_pixel_format(MTLPixelFormat::BGRA8Unorm); + layer.set_presents_with_transaction(false); + layer.set_display_sync_enabled(true); + layer.set_opaque(false); + layer.set_framebuffer_only(false); // to enable blend modes - pub fn resize(&self, size: PhysicalSize) { - autoreleasepool(|| { - let cg_size = CGSize::new(size.width as f64, size.height as f64); - self.layer.set_drawable_size(cg_size); - }) - } + let draw_size = window.inner_size(); + layer.set_drawable_size(CGSize::new(draw_size.width as f64, draw_size.height as f64)); - pub fn draw( - &mut self, - window: &Arc, - f: F, - ) -> Result<(), String> - where F:FnOnce(&skia_safe::Canvas, LogicalSize) - { - autoreleasepool(||{ - let dpr = window.scale_factor(); - let size = window.inner_size(); - BACKEND.with_borrow_mut(|cell| { - let backend = cell.get_or_insert_with(|| MetalBackend::for_renderer(self)); - - backend.render_to_layer(&self.layer, |canvas|{ - canvas.reset_matrix(); - canvas.scale((dpr as f32, dpr as f32)); - f(canvas, LogicalSize::from_physical(size, dpr)); - }) - }) - }) + let backend = MetalBackend::for_layer(&layer); + + Self{window, layer, backend, resized:false} } -} -impl Drop for MetalRenderer { - fn drop(&mut self) { - BACKEND.with_borrow_mut(|cell| *cell = None ); + pub fn resize(&mut self, size: PhysicalSize) { + let cg_size = CGSize::new(size.width as f64, size.height as f64); + self.layer.set_drawable_size(cg_size); + self.resized = true; } -} + pub fn draw(&mut self, page:Page, matrix:Matrix, matte:Color){ + let (clip, _) = matrix.map_rect(page.bounds); + self.backend.render_to_layer(&self.layer, &self.window, self.resized, |canvas|{ + canvas.clear(matte) + .clip_rect(clip, None, Some(true)) + .draw_picture(page.get_picture(None).unwrap(), Some(&matrix), None); + }).unwrap(); -thread_local!(static BACKEND: RefCell> = const { RefCell::new(None) } ); + self.resized = false; // only the first render after a resize needs to be synchronous + } +} pub struct MetalBackend { - // each renderer's non-Send references need to be lazily allocated on the window's thread skia_ctx: DirectContext, queue: CommandQueue, } -#[cfg(feature = "window")] -impl MetalBackend { - pub fn for_renderer(renderer:&MetalRenderer) -> Self { - let queue = renderer.device.new_command_queue(); +impl Drop for MetalBackend{ + fn drop(&mut self) { + self.skia_ctx.abandon(); + } +} +impl MetalBackend { + pub fn for_layer(layer:&MetalLayer) -> Self{ + let queue = layer.device().new_command_queue(); let backend_ctx = unsafe { mtl::BackendContext::new( - renderer.device.as_ptr() as mtl::Handle, + layer.device().as_ptr() as mtl::Handle, queue.as_ptr() as mtl::Handle, ) }; - let skia_ctx = direct_contexts::make_metal(&backend_ctx, None).unwrap(); - Self { skia_ctx, queue } } - fn render_to_layer(&mut self, layer:&MetalLayer, f:F) -> Result<(), String> + fn render_to_layer(&mut self, layer:&MetalLayer, window:&Window, sync:bool, f:F) -> Result<(), String> where F:FnOnce(&skia_safe::Canvas) { let drawable = layer @@ -311,14 +305,21 @@ impl MetalBackend { None, ).ok_or("MetalBackend: could not create render target")?; - f(surface.canvas()); + let dpr = window.scale_factor(); + let scale = Matrix::scale((dpr as f32, dpr as f32)); + f(surface.canvas().set_matrix(&scale.into())); self.skia_ctx.flush_and_submit(); self.skia_ctx.free_gpu_resources(); + window.pre_present_notify(); let command_buffer = self.queue.new_command_buffer(); command_buffer.present_drawable(drawable); command_buffer.commit(); + + // during resizes, ensure drawing is complete before returning + if sync{ command_buffer.wait_until_completed(); } + Ok(()) } diff --git a/src/gpu/mod.rs b/src/gpu/mod.rs index e1453209..00ed41c1 100644 --- a/src/gpu/mod.rs +++ b/src/gpu/mod.rs @@ -13,9 +13,9 @@ pub use crate::gpu::metal::MetalRenderer as Renderer; #[cfg(feature = "vulkan")] mod vulkan; #[cfg(feature = "vulkan")] -use crate::gpu::vulkan::VulkanEngine as Engine; +use crate::gpu::vulkan::engine::VulkanEngine as Engine; #[cfg(all(feature = "vulkan", feature = "window"))] -pub use crate::gpu::vulkan::VulkanRenderer as Renderer; +pub use crate::gpu::vulkan::renderer::VulkanRenderer as Renderer; #[cfg(not(any(feature = "vulkan", feature = "metal")))] struct Engine { } diff --git a/src/gpu/vulkan/engine.rs b/src/gpu/vulkan/engine.rs index 3e820da8..f124b7f9 100644 --- a/src/gpu/vulkan/engine.rs +++ b/src/gpu/vulkan/engine.rs @@ -1,7 +1,5 @@ -#![allow(unused_imports)] use std::{cell::RefCell, sync::{Arc, OnceLock}, time::{Instant, Duration}, ptr}; use serde_json::{json, Value}; - use vulkano::{ device::{ physical::{PhysicalDevice, PhysicalDeviceType}, @@ -10,11 +8,13 @@ use vulkano::{ instance::{Instance, InstanceCreateFlags, InstanceCreateInfo}, Handle, VulkanLibrary, VulkanObject, }; - -use skia_safe::gpu::vk::{BackendContext, GetProcOf}; -use skia_safe::gpu::{direct_contexts, surfaces, Budgeted, DirectContext, SurfaceOrigin}; -use skia_safe::{ColorSpace, ISize, ImageInfo, Surface, Data}; - +use skia_safe::{ + gpu::{ + vk::{BackendContext, GetProcOf}, + direct_contexts, surfaces, Budgeted, DirectContext, SurfaceOrigin + }, + ColorSpace, ISize, ImageInfo, Surface, +}; thread_local!( static VK_CONTEXT: RefCell> = const { RefCell::new(None) }; ); static VK_STATUS: OnceLock = OnceLock::new(); @@ -51,15 +51,15 @@ impl VulkanEngine { Self::spawn_idle_watcher(); // watch for inactive contexts and deallocate them let device_props = context.physical_device.properties(); - let (mode, gpu_type) = match device_props.device_type { - PhysicalDeviceType::IntegratedGpu => ("GPU", Some("Integrated GPU")), - PhysicalDeviceType::DiscreteGpu => ("GPU", Some("Discrete GPU")), - PhysicalDeviceType::VirtualGpu => ("GPU", Some("Virtual GPU")), - _ => ("CPU", Some("Software Rasterizer")) + let gpu_type = match device_props.device_type { + PhysicalDeviceType::IntegratedGpu => Some("Integrated GPU"), + PhysicalDeviceType::DiscreteGpu => Some("Discrete GPU"), + PhysicalDeviceType::VirtualGpu => Some("Virtual GPU"), + _ => Some("Software Rasterizer") }; json!({ - "renderer": mode, + "renderer": "GPU", "api": "Vulkan", "device": gpu_type.map(|t| format!("{} ({})", t, device_props.device_name) @@ -219,12 +219,17 @@ impl VulkanContext{ } .ok_or("Failed to create Vulkan backend context")?; - let sample_counts = physical_device.properties().framebuffer_color_sample_counts; - let mut msaa:Vec = [2,4,8,16,32].into_iter() - .filter_map(|s| vulkano::image::SampleCount::try_from(s).ok() ) - .filter(|s| sample_counts.contains_enum(*s) ) - .map(|s| s as usize) - .collect(); + let vk_sample_counts = physical_device.properties().framebuffer_color_sample_counts; + let max_sample_count = context.max_surface_sample_count_for_color_type( + // even if the device claims it supports >1 samples, let skia overrule it + ImageInfo::new_n32_premul((0,0), None).color_type() + ); + let mut msaa:Vec = [1,2,4,8,16,32].into_iter() + .filter(|s| s <= &max_sample_count) + .filter_map(|s| vulkano::image::SampleCount::try_from(s as u32).ok() ) + .filter(|s| vk_sample_counts.contains_enum(*s) ) + .map(|s| s as usize) + .collect(); msaa.insert(0, 0); // also include the shader-based AA option diff --git a/src/gpu/vulkan/mod.rs b/src/gpu/vulkan/mod.rs index e21f1c5a..7236117a 100644 --- a/src/gpu/vulkan/mod.rs +++ b/src/gpu/vulkan/mod.rs @@ -1,5 +1,70 @@ +use vulkano::format::Format as VkFormat; +use skia_safe::{ gpu::vk, ColorType }; + pub mod engine; -pub use engine::VulkanEngine; +#[cfg(feature = "window")] pub mod renderer; -pub use renderer::VulkanRenderer; \ No newline at end of file + +static VK_FORMATS: &'static [VkFormat] = &[ + VkFormat::R8G8B8A8_UNORM, + VkFormat::R8G8B8A8_SRGB, + VkFormat::R8_UNORM, + VkFormat::B8G8R8A8_UNORM, + VkFormat::R5G6B5_UNORM_PACK16, + VkFormat::B5G6R5_UNORM_PACK16, + VkFormat::R16G16B16A16_SFLOAT, + VkFormat::R16_SFLOAT, + VkFormat::R8G8B8_UNORM, + VkFormat::R8G8_UNORM, + VkFormat::A2B10G10R10_UNORM_PACK32, + VkFormat::A2R10G10B10_UNORM_PACK32, + VkFormat::R10X6G10X6B10X6A10X6_UNORM_4PACK16, + VkFormat::B4G4R4A4_UNORM_PACK16, + VkFormat::R4G4B4A4_UNORM_PACK16, + VkFormat::R16_UNORM, + VkFormat::R16G16_UNORM, + VkFormat::G8_B8_R8_3PLANE_420_UNORM, + VkFormat::G8_B8R8_2PLANE_420_UNORM, + VkFormat::G10X6_B10X6R10X6_2PLANE_420_UNORM_3PACK16, + VkFormat::R16G16B16A16_UNORM, + VkFormat::R16G16_SFLOAT, +]; + +fn to_sk_format(vulkano_format:&VkFormat) -> Option<(vk::Format, ColorType)>{ + // Format / ColorType pairs + // https://github.com/google/skia/blob/4f24819404272433687a76e407bcd7877384f512/src/gpu/ganesh/vk/GrVkCaps.cpp#L880 + // + // GrColorType -> SkColorType mappings + // https://github.com/google/skia/blob/4f24819404272433687a76e407bcd7877384f512/include/private/gpu/ganesh/GrTypesPriv.h#L590 + // + // Present in the GrVkCaps 'supported' list but lacking supported GrColorTypes so omitted: + // - VkFormat::ETC2_R8G8B8_UNORM_BLOCK + // - VkFormat::BC1_RGB_UNORM_BLOCK + // - VkFormat::BC1_RGBA_UNORM_BLOCK + match vulkano_format { + VkFormat::R8G8B8A8_UNORM => Some(( vk::Format::R8G8B8A8_UNORM, ColorType::RGBA8888 )), + VkFormat::R8G8B8A8_SRGB => Some(( vk::Format::R8G8B8A8_SRGB, ColorType::SRGBA8888 )), + VkFormat::R8_UNORM => Some(( vk::Format::R8_UNORM, ColorType::R8UNorm )), + VkFormat::B8G8R8A8_UNORM => Some(( vk::Format::B8G8R8A8_UNORM, ColorType::BGRA8888 )), + VkFormat::R5G6B5_UNORM_PACK16 => Some(( vk::Format::R5G6B5_UNORM_PACK16, ColorType::RGB565 )), + VkFormat::B5G6R5_UNORM_PACK16 => Some(( vk::Format::B5G6R5_UNORM_PACK16, ColorType::RGB565 )), + VkFormat::R16G16B16A16_SFLOAT => Some(( vk::Format::R16G16B16A16_SFLOAT, ColorType::RGBAF16 )), + VkFormat::R16_SFLOAT => Some(( vk::Format::R16_SFLOAT, ColorType::A16Float )), + VkFormat::R8G8B8_UNORM => Some(( vk::Format::R8G8B8_UNORM, ColorType::RGB888x )), + VkFormat::R8G8_UNORM => Some(( vk::Format::R8G8_UNORM, ColorType::R8G8UNorm )), + VkFormat::A2B10G10R10_UNORM_PACK32 => Some(( vk::Format::A2B10G10R10_UNORM_PACK32, ColorType::RGBA1010102 )), + VkFormat::A2R10G10B10_UNORM_PACK32 => Some(( vk::Format::A2R10G10B10_UNORM_PACK32, ColorType::BGRA1010102 )), + VkFormat::R10X6G10X6B10X6A10X6_UNORM_4PACK16 => Some(( vk::Format::R10X6G10X6B10X6A10X6_UNORM_4PACK16, ColorType::RGBA10x6 )), + VkFormat::B4G4R4A4_UNORM_PACK16 => Some(( vk::Format::B4G4R4A4_UNORM_PACK16, ColorType::ARGB4444 )), + VkFormat::R4G4B4A4_UNORM_PACK16 => Some(( vk::Format::R4G4B4A4_UNORM_PACK16, ColorType::ARGB4444 )), + VkFormat::R16_UNORM => Some(( vk::Format::R16_UNORM, ColorType::A16UNorm )), + VkFormat::R16G16_UNORM => Some(( vk::Format::R16G16_UNORM, ColorType::R16G16UNorm )), + VkFormat::G8_B8_R8_3PLANE_420_UNORM => Some(( vk::Format::G8_B8_R8_3PLANE_420_UNORM, ColorType::RGB888x )), + VkFormat::G8_B8R8_2PLANE_420_UNORM => Some(( vk::Format::G8_B8R8_2PLANE_420_UNORM, ColorType::RGB888x )), + VkFormat::G10X6_B10X6R10X6_2PLANE_420_UNORM_3PACK16 => Some(( vk::Format::G10X6_B10X6R10X6_2PLANE_420_UNORM_3PACK16, ColorType::RGBA1010102 )), + VkFormat::R16G16B16A16_UNORM => Some(( vk::Format::R16G16B16A16_UNORM, ColorType::R16G16B16A16UNorm )), + VkFormat::R16G16_SFLOAT => Some(( vk::Format::R16G16_SFLOAT, ColorType::R16G16Float )), + _ => None + } +} diff --git a/src/gpu/vulkan/renderer.rs b/src/gpu/vulkan/renderer.rs index 83f56e9f..06f0e6dd 100644 --- a/src/gpu/vulkan/renderer.rs +++ b/src/gpu/vulkan/renderer.rs @@ -1,50 +1,37 @@ -#![allow(dead_code)] -#![allow(unused_imports)] use ash::vk::Handle; -use std::{ - cell::RefCell, collections::HashMap, ptr, sync::Arc -}; +use std::{ptr, sync::Arc}; use vulkano::{ device::{ - physical::PhysicalDeviceType, Device, DeviceCreateInfo, DeviceExtensions, Queue, - QueueCreateInfo, QueueFlags, + physical::PhysicalDeviceType, Device, DeviceCreateInfo, DeviceExtensions, DeviceOwned, Queue, QueueCreateInfo, QueueFlags }, image::{view::ImageView, ImageUsage}, instance::{Instance, InstanceCreateFlags, InstanceCreateInfo}, render_pass::{Framebuffer, FramebufferCreateInfo, RenderPass}, swapchain::{ - acquire_next_image, CompositeAlpha, Surface, Swapchain, - SwapchainAcquireFuture, SwapchainCreateInfo, SwapchainPresentInfo, + acquire_next_image, CompositeAlpha, Surface, Swapchain, SwapchainAcquireFuture, SwapchainCreateInfo, SwapchainPresentInfo }, sync::{self, GpuFuture}, Validated, VulkanError, VulkanLibrary, VulkanObject, }; - use skia_safe::{ gpu::{self, backend_render_targets, direct_contexts, surfaces, vk}, - ColorType, + Color, Matrix, }; - -#[cfg(feature = "window")] use winit::{ - dpi::{LogicalSize, PhysicalSize}, + dpi::PhysicalSize, event_loop::ActiveEventLoop, - window::{Window, WindowId}, + window::Window, }; -thread_local!( - static BACKEND: RefCell> = const { RefCell::new(None) }; -); +use crate::context::page::Page; +use super::{VK_FORMATS, to_sk_format}; -pub struct VulkanRenderer { - queue: Arc, - swapchain: Arc, - framebuffers: Vec>, - render_pass: Arc, - swapchain_is_valid: bool, + +pub struct VulkanRenderer{ + window: Arc, + backend: VulkanBackend, } -#[cfg(feature = "window")] impl VulkanRenderer { pub fn for_window(event_loop: &ActiveEventLoop, window: Arc) -> Self { let instance = { @@ -126,9 +113,21 @@ impl VulkanRenderer { let surface_capabilities = physical_device .surface_capabilities(&surface, Default::default()) .unwrap(); - let (image_format, _) = physical_device + + // choose the first device format that is on the supported list + let device_formats = physical_device .surface_formats(&surface, Default::default()) - .unwrap()[0]; + .unwrap(); + let (image_format, _) = device_formats.clone() + .into_iter() + .find(|(fmt, _)| VK_FORMATS.contains(fmt)) + .unwrap_or_else(|| + panic!( + "Vulkan: no format supported by Skia was found on device.\nSupported formats: {:?}\nDevice formats: {:?}", + VK_FORMATS, + device_formats + ) + ); Swapchain::new( device.clone(), @@ -157,6 +156,47 @@ impl VulkanRenderer { .unwrap() }; + Self{window, backend:VulkanBackend::new(queue, swapchain)} + } + + pub fn resize(&mut self, size: PhysicalSize) { + self.backend.swapchain_is_valid = false; + self.backend.prepare_swapchain(size.into()); + } + + pub fn draw(&mut self, page:Page, matrix:Matrix, matte:Color){ + let (clip, _) = matrix.map_rect(page.bounds); + self.backend.render_frame(&self.window, |canvas|{ + canvas.clear(matte) + .clip_rect(clip, None, Some(true)) + .draw_picture(page.get_picture(None).unwrap(), Some(&matrix), None); + }).unwrap(); + } +} + + +struct VulkanBackend{ + queue: Arc, + framebuffers: Vec>, + render_pass: Arc, + swapchain: Arc, + swapchain_is_valid: bool, + last_render: Option>, + skia_ctx: gpu::DirectContext, +} + +impl Drop for VulkanBackend{ + fn drop(&mut self) { + self.skia_ctx.abandon(); + } +} + +impl VulkanBackend{ + fn new(queue:Arc, swapchain:Arc) -> Self{ + let device = queue.device(); + let instance = device.instance(); + let library = instance.library(); + // Define the layout of the framebuffers and their role in the graphics pipeline let render_pass = vulkano::single_pass_renderpass!( device.clone(), @@ -180,30 +220,59 @@ impl VulkanRenderer { let framebuffers = vec![]; let swapchain_is_valid = false; - Self { - queue, - swapchain, - swapchain_is_valid, - render_pass, - framebuffers, - } + // Hold onto the previous GpuFuture so we can wait on its completion before the next frame + let last_render = Some(sync::now(device.clone()).boxed()); - } + // Create a DirectContext that will let us use a surface & canvas to draw into framebuffers + let skia_ctx = unsafe { + let get_proc = |gpo| { + let get_device_proc_addr = instance.fns().v1_0.get_device_proc_addr; - pub fn resize(&mut self, _size: PhysicalSize) { - // we can get the dimensions from the window when reallocating framebuffers - self.swapchain_is_valid = false; - } + match gpo { + vk::GetProcOf::Instance(instance, name) => { + let vk_instance = ash::vk::Instance::from_raw(instance as _); + library.get_instance_proc_addr(vk_instance, name) + } + vk::GetProcOf::Device(device, name) => { + let vk_device = ash::vk::Device::from_raw(device as _); + get_device_proc_addr(vk_device, name) + } + } + .map(|f| f as _) + .unwrap_or_else(|| { + println!("Vulkan: failed to resolve {}", gpo.name().to_str().unwrap()); + ptr::null() + }) + }; + + let direct_context = direct_contexts::make_vulkan( + &vk::BackendContext::new( + instance.handle().as_raw() as _, + device.physical_device().handle().as_raw() as _, + device.handle().as_raw() as _, + ( + queue.handle().as_raw() as _, + queue.queue_family_index() as usize, + ), + &get_proc, + ), + None, + ) + .expect("Vulkan: Failed to create Skia direct context"); - fn prepare_swapchain(&mut self, window: &Window) { - let window_size: PhysicalSize = window.inner_size(); + direct_context + }; + Self{queue, framebuffers, render_pass, swapchain, swapchain_is_valid, last_render, skia_ctx} + } + + fn prepare_swapchain(&mut self, size: PhysicalSize) { // Only regenerate the swapchain/framebuffers if we've flagged that it's necessary - if window_size.width > 0 && window_size.height > 0 && !self.swapchain_is_valid { + if size.width > 0 && size.height > 0 && !self.swapchain_is_valid { let (new_swapchain, new_images) = self .swapchain .recreate(SwapchainCreateInfo { - image_extent: window_size.into(), + image_extent: size.into(), ..self.swapchain.create_info() }) .expect("failed to recreate swapchain"); @@ -226,6 +295,29 @@ impl VulkanRenderer { } } + fn render_frame(&mut self, window:&Window, f:F) -> Result<(), String> + where F:FnOnce(&skia_safe::Canvas) + { + // make sure the framebuffers match the current window size + self.prepare_swapchain(self.swapchain.image_extent().into()); + + if let Some((image_index, acquire_future)) = self.get_next_frame() { + // pull the appropriate framebuffer and create a skia Surface that renders to it + let framebuffer = self.framebuffers[image_index as usize].clone(); + let mut surface = self.surface_for_framebuffer(framebuffer.clone()); + + // pass the suface's canvas to the user-provided callback + let dpr = window.scale_factor(); + let scale = Matrix::scale((dpr as f32, dpr as f32)); + f(surface.canvas().set_matrix(&scale.into())); + + // display the result + self.flush_framebuffer(window, image_index, acquire_future); + } + + Ok(()) + } + fn get_next_frame(&mut self) -> Option<(u32, SwapchainAcquireFuture)> { // Request the next framebuffer and a GpuFuture for the render pass let (image_index, suboptimal, acquire_future) = @@ -246,105 +338,6 @@ impl VulkanRenderer { Some((image_index, acquire_future)) } - pub fn draw)>( - &mut self, - window: &Window, - f: F, - ) -> Result<(), String> { - // make sure the framebuffers match the current window size - self.prepare_swapchain(window); - - if let Some((image_index, acquire_future)) = self.get_next_frame() { - BACKEND.with_borrow_mut(|cell| { - let backend = cell.get_or_insert_with(||{ VulkanBackend::for_renderer(self) }); - - // pull the appropriate framebuffer and create a skia Surface that renders to it - let framebuffer = self.framebuffers[image_index as usize].clone(); - let mut surface = backend.surface_for_framebuffer(framebuffer.clone()); - - // convert the window size to logical coords and pre-scale the canvas's matrix to match - let dpr = window.scale_factor(); - let size = window.inner_size(); - let canvas = surface.canvas(); - canvas.reset_matrix(); - canvas.scale((dpr as f32, dpr as f32)); - - // pass the suface's canvas and dimensions to the user-provided callback - f(canvas, LogicalSize::from_physical(size, dpr)); - - // display the result - backend.flush_framebuffer(self, image_index, acquire_future); - }); - } - Ok(()) - } -} - -impl Drop for VulkanRenderer { - fn drop(&mut self) { - BACKEND.with_borrow_mut(|cell| *cell = None ); - } -} - -struct VulkanBackend{ - // each renderer's non-Send references need to be lazily allocated on the window's thread - last_render: Option>, - skia_ctx: gpu::DirectContext, -} - -impl VulkanBackend{ - fn for_renderer(renderer:&VulkanRenderer) -> Self{ - let queue = renderer.queue.clone(); - let device = queue.device(); - let instance = device.instance(); - let library = instance.library(); - - Self{ - // Hold onto the previous GpuFuture so we can wait on its completion before the next frame - last_render: Some(sync::now(device.clone()).boxed()), - - // Create a DirectContext that will let us use a surface & canvas to draw into framebuffers - skia_ctx: unsafe { - let get_proc = |gpo| { - let get_device_proc_addr = instance.fns().v1_0.get_device_proc_addr; - - match gpo { - vk::GetProcOf::Instance(instance, name) => { - let vk_instance = ash::vk::Instance::from_raw(instance as _); - library.get_instance_proc_addr(vk_instance, name) - } - vk::GetProcOf::Device(device, name) => { - let vk_device = ash::vk::Device::from_raw(device as _); - get_device_proc_addr(vk_device, name) - } - } - .map(|f| f as _) - .unwrap_or_else(|| { - println!("Vulkan: failed to resolve {}", gpo.name().to_str().unwrap()); - ptr::null() - }) - }; - - let direct_context = direct_contexts::make_vulkan( - &vk::BackendContext::new( - instance.handle().as_raw() as _, - device.physical_device().handle().as_raw() as _, - device.handle().as_raw() as _, - ( - queue.handle().as_raw() as _, - queue.queue_family_index() as usize, - ), - &get_proc, - ), - None, - ) - .expect("Vulkan: Failed to create Skia direct context"); - - direct_context - }, - } - } - fn surface_for_framebuffer( &mut self, framebuffer: Arc, @@ -354,13 +347,8 @@ impl VulkanBackend{ let image_object = image_access.image().handle().as_raw(); let format = image_access.format(); - let (vk_format, color_type) = match format { - vulkano::format::Format::B8G8R8A8_UNORM => ( - skia_safe::gpu::vk::Format::B8G8R8A8_UNORM, - ColorType::BGRA8888, - ), - _ => panic!("Vulkan: unsupported color format {:?}", format), - }; + let (vk_format, color_type) = to_sk_format(&format) + .unwrap_or_else(|| panic!("Vulkan: unsupported color format {:?}", format)); let image_info = &unsafe { vk::ImageInfo::new( @@ -393,7 +381,7 @@ impl VulkanBackend{ .unwrap() } - fn flush_framebuffer(&mut self, renderer:&mut VulkanRenderer, image_index:u32, acquire_future:SwapchainAcquireFuture){ + fn flush_framebuffer(&mut self, window:&Window, image_index:u32, acquire_future:SwapchainAcquireFuture){ // flush the canvas's contents to the framebuffer self.skia_ctx.flush_and_submit(); self.skia_ctx.free_gpu_resources(); @@ -401,6 +389,9 @@ impl VulkanBackend{ // reclaim leftover resources from the last frame self.last_render.as_mut().unwrap().cleanup_finished(); + // let winit know that rendering is complete + window.pre_present_notify(); + // send the framebuffer to the gpu and display it on screen let future = self .last_render @@ -408,9 +399,9 @@ impl VulkanBackend{ .unwrap() .join(acquire_future) .then_swapchain_present( - renderer.queue.clone(), + self.queue.clone(), SwapchainPresentInfo::swapchain_image_index( - renderer.swapchain.clone(), + self.swapchain.clone(), image_index, ), ) @@ -421,14 +412,14 @@ impl VulkanBackend{ self.last_render = Some(future.boxed()); } Err(VulkanError::OutOfDate) => { - let device = renderer.queue.device(); + let device = self.queue.device(); self.last_render = Some(sync::now(device.clone()).boxed()); - renderer.swapchain_is_valid = false; + self.swapchain_is_valid = false; } Err(e) => { panic!("Vulkan: swapchain flush failed: {e}"); } }; - } + } diff --git a/src/gui/app.rs b/src/gui/app.rs index 947918f6..d10eb814 100644 --- a/src/gui/app.rs +++ b/src/gui/app.rs @@ -1,198 +1,325 @@ use neon::prelude::*; -use std::time::{Duration, Instant}; use serde_json::Value; +use std::{ + sync::{Arc, OnceLock}, + iter::zip, + cell::RefCell, + time::{Duration, Instant}, +}; use winit::{ - application::ApplicationHandler, - event::{ElementState, KeyEvent, StartCause, WindowEvent}, - event_loop::{ActiveEventLoop, ControlFlow}, + platform::pump_events::EventLoopExtPumpEvents, + platform::run_on_demand::EventLoopExtRunOnDemand, + event::{ElementState, KeyEvent, Event, WindowEvent}, + event_loop::{EventLoop, EventLoopProxy, ActiveEventLoop, ControlFlow}, keyboard::{PhysicalKey, KeyCode}, - window::WindowId }; -use super::event::CanvasEvent; -use super::window_mgr::WindowManager; -use super::{add_event, new_proxy}; +use crate::context::{page::Page, BoxedContext2D}; +use super::{ + event::AppEvent, + window_mgr::WindowManager, + window::WindowSpec, +}; + +thread_local!( + static APP: RefCell = RefCell::new(App::default()); + static EVENT_LOOP: RefCell> = RefCell::new(EventLoop::with_user_event().build().unwrap()); + static PROXY: RefCell> = RefCell::new(EVENT_LOOP.with_borrow(|event_loop| + event_loop.create_proxy() + )); +); -pub trait Roundtrip: FnMut(Value, &mut WindowManager) -> NeonResult<()>{} -impl NeonResult<()>> Roundtrip for T {} +static RENDER_CALLBACK: OnceLock>> = OnceLock::new(); -pub struct App{ +#[derive(Copy, Clone)] +pub enum LoopMode{ + Native, Node +} + +pub struct App{ + pub mode: LoopMode, windows: WindowManager, cadence: Cadence, - callback: F, } -impl App{ - pub fn with_callback(callback:F) -> Self{ - let windows = WindowManager::default(); - let cadence = Cadence::default(); - Self{windows, cadence, callback} +impl Default for App{ + fn default() -> Self { + Self{ + windows: WindowManager::default(), + cadence: Cadence::default(), + mode: LoopMode::Native, + } + } +} + +fn add_event(event: AppEvent){ + PROXY.with_borrow_mut(|proxy| proxy.send_event(event).ok() ); +} + +impl App{ + pub fn register(callback:Root){ + RENDER_CALLBACK.get_or_init(|| Arc::new(callback)); } - fn initial_sync(&mut self){ - let payload = self.windows.get_geometry(); - let _ = (self.callback)(payload, &mut self.windows); + pub fn set_mode(mode:LoopMode){ + APP.with_borrow_mut(|app| app.mode = mode ); } - fn roundtrip(&mut self){ - let payload = self.windows.get_ui_changes(); - let _ = (self.callback)(payload, &mut self.windows); + pub fn set_fps(fps:f32){ + add_event(AppEvent::FrameRate(fps as u64)); } -} -impl ApplicationHandler for App { - fn resumed(&mut self, event_loop:&ActiveEventLoop){ + pub fn open_window(spec:WindowSpec, page:Page){ + add_event(AppEvent::Open(spec, page)); + } + pub fn close_window(token:u32){ + add_event(AppEvent::Close(token)); } - fn new_events(&mut self, event_loop:&ActiveEventLoop, cause:StartCause) { - if cause == StartCause::Init{ - // on initial pass, do a roundtrip to sync up the Window object's state attrs: - // send just the initial window positions then read back all state - self.initial_sync(); - } + pub fn quit(){ + APP.with_borrow_mut(|app| app.windows.remove_all() ); + add_event(AppEvent::Quit); } - fn window_event( &mut self, event_loop:&ActiveEventLoop, window_id:WindowId, event:WindowEvent){ - // route UI events to the relevant window - self.windows.capture_ui_event(&window_id, &event); + #[allow(deprecated)] + pub fn activate(channel:Channel, deferred:neon::types::Deferred){ + std::thread::spawn(move || { + loop{ + // schedule a callback on the node event loop + let keep_running = channel.send(move |mut cx| { - // handle window lifecycle events - match event { - WindowEvent::Destroyed | WindowEvent::CloseRequested => { - self.windows.remove(&window_id); - if self.windows.is_empty() { - // quit after the last window is closed - event_loop.exit(); + // define closure to relay events to js and receive canvas updates in return + let dispatch = |payload:Value, windows:Option<&mut WindowManager>| -> NeonResult<()>{ + App::dispatch_events(&mut cx, payload, windows) + }; + + // run the winit event loop (either once or until all windows are closed depending on mode) + APP.with_borrow_mut(|app| { + EVENT_LOOP.with_borrow_mut(|event_loop|{ + match app.mode{ + LoopMode::Native => { + let handler = app.event_handler(dispatch); + event_loop.set_control_flow(ControlFlow::Wait); + event_loop.run_on_demand(handler).ok(); + Ok(false) // final window was closed + } + LoopMode::Node => { + let poll_time = app.cadence.next_wakeup() - Instant::now(); + let handler = app.event_handler(dispatch); + event_loop.pump_events(Some(poll_time), handler); + Ok(app.cadence.should_continue() || !app.windows.is_empty()) + } + } + }) + }) + }).join(); + + match keep_running{ + Ok(true) => continue, + _ => break } } - WindowEvent::KeyboardInput { - event: - KeyEvent { - physical_key: PhysicalKey::Code(KeyCode::Escape), - state: ElementState::Pressed, - repeat: false, - .. - }, - .. - } => { - self.windows.set_fullscreen_state(&window_id, false); - } - #[cfg(target_os = "macos")] - WindowEvent::Occluded(is_hidden) => { - self.windows.send_event(&window_id, CanvasEvent::RedrawingSuspended(is_hidden)); - } + // resolve the promise + deferred.settle_with(&channel, move |mut cx| Ok(cx.undefined()) ); + }); + } - WindowEvent::RedrawRequested => { - self.windows.send_event(&window_id, CanvasEvent::RedrawRequested); - } + fn dispatch_events(cx:&mut TaskContext, events:Value, window_mgr:Option<&mut WindowManager>) -> NeonResult<()>{ + // window_mgr is only present if it's time to collect updated canvas contents from js + let is_render = window_mgr.is_some(); + + // js callback is passed render flag & json-encoded event queue + let mut call = match RENDER_CALLBACK.get(){ + None => return Ok(()), + Some(callback)=> callback.to_inner(cx).call_with(cx), + }; + call.arg(cx.boolean(is_render)) + .arg(cx.string(events.to_string())); + + match window_mgr{ + None => call.exec(cx)?, // if this is just a UI-event delivery, fire & forget + + Some(window_mgr) => { + // for a full roundtrip, first pass events to js + let response = call.apply::(cx)? + .downcast::(cx).or_throw(cx)? + .to_vec(cx)?; + + // then unpack the returned window specs & contexts + let specs_json = response[0].downcast::(cx).or_throw(cx)?.value(cx); + let specs:Vec = serde_json::from_str(&specs_json) + .or_else(|err| cx.throw_error(format!("Malformed response from window event handler: {}", err)) )?; - WindowEvent::Resized(size) => { - self.windows.send_event(&window_id, CanvasEvent::WindowResized(size)); + let contexts = response[1].downcast::(cx).or_throw(cx)?.to_vec(cx)?; + let pages = contexts.iter().map(|boxed| + boxed.downcast::(cx).ok() + .map(|ctx| ctx.borrow().get_page()) + ); + + // update each window with its new state & content + zip(specs, pages) + .filter_map(|(spec, page)| page.map(|page| (spec, page) )) + .for_each(|(spec, page)| window_mgr.update_window(spec, page) ); } - _ => {} - } + }; + + Ok(()) } - fn user_event(&mut self, event_loop:&ActiveEventLoop, event:CanvasEvent) { - match event{ - CanvasEvent::Open(spec, page) => { - self.windows.add(event_loop, new_proxy(), spec, page); - } - CanvasEvent::Close(token) => { - self.windows.remove_by_token(&token); - } - CanvasEvent::Quit => { - event_loop.exit(); - } - CanvasEvent::Render => { - // relay UI-driven state changes to js and render the next frame in the (active) cadence - self.roundtrip(); - } - CanvasEvent::Transform(window_id, matrix) => { - self.windows.use_ui_transform(&window_id, &matrix); + fn event_handler(&mut self, mut dispatch:F) -> impl FnMut(Event, &ActiveEventLoop) + use<'_, F> + where F:FnMut(Value, Option<&mut WindowManager>) -> NeonResult<()> + { + move |event, event_loop| match event { + Event::WindowEvent { event:ref win_event, window_id } => { + self.windows.find(&window_id, |win| win.sieve.capture(win_event) ); + + match win_event { + WindowEvent::Destroyed | WindowEvent::CloseRequested => { + self.windows.remove(&window_id); + + // after the last window is closed, either exit (in run_on_demand mode) + // or wait for the window destructor to run (in pump_events mode) + if self.windows.is_empty(){ match self.mode{ + LoopMode::Native => event_loop.exit(), + LoopMode::Node => self.cadence.loop_again(), + }} + } + + WindowEvent::KeyboardInput { + event: + KeyEvent { + physical_key: PhysicalKey::Code(KeyCode::Escape), + state: ElementState::Pressed, + repeat: false, + .. + }, + .. + } => { + self.windows.find(&window_id, |win| win.set_fullscreen(false) ); + } + + WindowEvent::Moved(loc) => { + self.windows.find(&window_id, |win| win.did_move(*loc) ); + } + + WindowEvent::Resized(size) => { + self.windows.find(&window_id, |win| win.did_resize(*size) ); + } + + #[cfg(target_os = "macos")] + WindowEvent::Occluded(is_hidden) => { + self.windows.find(&window_id, |win| win.set_redrawing_suspended(*is_hidden) ); + } + + WindowEvent::RedrawRequested => { + self.windows.find(&window_id, |win| win.redraw() ); + } + + _ => {} + } + }, + + + Event::UserEvent(app_event) => match app_event{ + AppEvent::Open(spec, page) => { + self.windows.add(event_loop, spec, page); + dispatch(self.windows.get_geometry(), Some(&mut self.windows)).ok(); + } + AppEvent::Close(token) => { + self.windows.remove_by_token(token); + } + AppEvent::FrameRate(fps) => { + self.cadence.set_frame_rate(fps) + } + AppEvent::Quit => { + event_loop.exit(); + } }, - CanvasEvent::InFullscreen(window_id, is_fullscreen) => { - self.windows.use_fullscreen_state(&window_id, is_fullscreen); - } - CanvasEvent::FrameRate(fps) => { - self.cadence.set_frame_rate(fps) - } - _ => {} - } - } - fn about_to_wait(&mut self, event_loop:&ActiveEventLoop) { - // when no windows have frame/draw handlers, the (inactive) cadence will never trigger - // a Render event, so only do a roundtrip if there are new UI events to be relayed - if !self.cadence.active() && self.windows.has_ui_changes() { - self.roundtrip(); - } - // delegate timing to the cadence if active, otherwise wait for ui events - event_loop.set_control_flow( - match self.cadence.active(){ - true => self.cadence.on_next_frame(|| add_event(CanvasEvent::Render) ), - false => ControlFlow::Wait + Event::AboutToWait => { + event_loop.set_control_flow( + // let the cadence decide when to switch to poll-mode or sleep the thread + self.cadence.on_next_frame(self.mode, || { + // relay UI-driven state changes to js and render the next frame in the (active) cadence + dispatch(self.windows.get_ui_changes(), Some(&mut self.windows)).ok(); + }) + ); } - ); + _ => {} + } } - } struct Cadence{ rate: u64, last: Instant, - interval: Duration, - begun: bool, + needs_cleanup: Option, } impl Default for Cadence { fn default() -> Self { Self{ - rate: 0, + rate: 60, last: Instant::now(), - interval: Duration::new(0, 0), - begun: false, + needs_cleanup: Some(true), // ensure at least one post-Init loop } } } impl Cadence{ - fn at_startup(&mut self) -> bool{ - if self.begun{ false } - else{ - self.begun = true; - true // only return true on first call - } + fn loop_again(&mut self){ + // flag that a clean-up event-loop pass is necessary (e.g., for reflecting window closures) + self.needs_cleanup = Some(true) + } + + fn should_continue(&mut self) -> bool{ + self.needs_cleanup.take().is_some() } fn set_frame_rate(&mut self, rate:u64){ - if rate == self.rate{ return } - let frame_time = 1_000_000_000/rate.max(1); - self.interval = Duration::from_nanos(frame_time); self.rate = rate; } - fn on_next_frame(&mut self, draw:F) -> ControlFlow{ - match self.active() { - true => { - if self.last.elapsed() >= self.interval{ - while self.last < Instant::now() - self.interval{ - self.last += self.interval - } - draw(); - } - ControlFlow::WaitUntil(self.last + self.interval) - }, - false => ControlFlow::Wait, - } + pub fn next_wakeup(&self) -> Instant{ + let frame_time = 1_000_000_000/self.rate.max(1); + let watch_interval = 1_500_000.min(frame_time/10); + let wakeup = Duration::from_nanos(frame_time - watch_interval); + self.last + wakeup } - fn active(&self) -> bool{ - self.rate > 0 + pub fn on_next_frame(&mut self, mode:LoopMode, mut draw:F) -> ControlFlow{ + // determine the upcoming deadlines for actually rendering and for spinning in preparation + let frame_time = 1_000_000_000/self.rate.max(1); + let watch_interval = 1_500_000.min(frame_time/10); + let render = Duration::from_nanos(frame_time); + let wakeup = Duration::from_nanos(frame_time - watch_interval); + + // if node is handling the event loop, we can't use polling to wait for the render + // deadline. so instead we'll pause the thread for the last 10% of the inter-frame + // time (up to 1.5ms), making sure we can then draw immediately after + let dt = self.last.elapsed(); + if matches!(mode, LoopMode::Node) && dt >= wakeup && dt < render{ + if let Some(sleep_time) = render.checked_sub(self.last.elapsed()){ + spin_sleep::sleep(sleep_time); + } + } + + // call the draw callback if it's time & make sure the next deadline is in the future + if self.last.elapsed() >= render{ + draw(); + while self.last < Instant::now() - render{ + self.last += render + } + } + + // if winit is in control, we can use waiting & polling to hit the deadline + match self.last.elapsed() < wakeup { + true => ControlFlow::WaitUntil(self.last + wakeup), + false => ControlFlow::Poll, + } } } - diff --git a/src/gui/event.rs b/src/gui/event.rs index f5be1ccc..964081bb 100644 --- a/src/gui/event.rs +++ b/src/gui/event.rs @@ -1,51 +1,21 @@ -use skia_safe::{Matrix, Color}; +use skia_safe::Matrix; use serde::Serialize; use serde_json::json; -use std::collections::HashSet; use winit::{ - dpi::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize}, + dpi::{LogicalPosition, LogicalSize, PhysicalPosition}, event::{ElementState, KeyEvent, Ime, Modifiers, MouseButton, MouseScrollDelta, WindowEvent}, keyboard::{ModifiersState, KeyCode, KeyLocation, NamedKey, PhysicalKey::Code, Key::{Character, Named}}, - platform::scancode::PhysicalKeyExtScancode, - window::{CursorIcon, WindowId} }; use crate::context::page::Page; -use super::window::{WindowSpec, Fit}; +use super::window::WindowSpec; #[derive(Debug, Clone)] -pub enum CanvasEvent{ - // app api +pub enum AppEvent{ Open(WindowSpec, Page), - Close(String), + Close(u32), FrameRate(u64), Quit, - - // app -> window - Page(Page), - - // window -> app - Transform(WindowId, Option), - InFullscreen(WindowId, bool), - - // cadence triggers - Render, - - // script -> window - Title(String), - Fullscreen(bool), - Visible(bool), - Resizable(bool), - Cursor(Option), - Background(Color), - Fit(Fit), - Position(LogicalPosition), - Size(LogicalSize), - - // encapsulated WindowEvents - WindowResized(PhysicalSize), - RedrawRequested, - RedrawingSuspended(bool), } #[derive(Debug, Serialize)] @@ -54,23 +24,43 @@ pub enum UiEvent{ #[allow(non_snake_case)] Wheel{deltaX:f32, deltaY:f32}, Move{left:f32, top:f32}, - Keyboard{event:String, key:String, code:KeyCode, location:u32, repeat:bool}, + Keyboard{event:String, key:String, code:KeyCode, location:u32, modifiers:ModifierKeys, repeat:bool}, Composition{event:String, data:String}, - Input(Option), - Mouse(String), + Mouse{event:String, button:Option, buttons:u16, point:LogicalPosition::, page_point:LogicalPosition::, modifiers:ModifierKeys}, + Input(Option, String), Focus(bool), Resize(LogicalSize), Fullscreen(bool), } +#[derive(Debug, Clone, Copy, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ModifierKeys{ + shift_key: bool, + ctrl_key: bool, + alt_key: bool, + meta_key: bool, +} + +impl From for ModifierKeys{ + fn from(state:ModifiersState) -> Self{ + ModifierKeys{ + shift_key: state.shift_key(), + ctrl_key: state.control_key(), + alt_key: state.alt_key(), + meta_key: state.super_key(), + } + } +} #[derive(Debug)] pub struct Sieve{ dpr: f64, queue: Vec, - key_modifiers: ModifiersState, + key_modifiers: ModifierKeys, mouse_point: PhysicalPosition::, mouse_button: Option, + mouse_buttons: u16, mouse_transform: Matrix, compose_begun: bool, compose_ongoing: bool, @@ -81,9 +71,10 @@ impl Sieve{ Sieve{ dpr, queue: vec![], - key_modifiers: Modifiers::default().state(), + key_modifiers: Modifiers::default().state().into(), mouse_point: PhysicalPosition::default(), mouse_button: None, + mouse_buttons: 0, mouse_transform: Matrix::new_identity(), compose_begun: false, compose_ongoing: false, @@ -98,6 +89,22 @@ impl Sieve{ self.queue.push(UiEvent::Fullscreen(is_full)); } + fn add_mouse_event(&mut self, event:&str){ + // helper to attach positions & keyboard modifiers for each type of mouse event + let raw_position = LogicalPosition::::from_physical(self.mouse_point, self.dpr); + let canvas_point = self.mouse_transform.map_point((raw_position.x, raw_position.y)); + let canvas_position = LogicalPosition::::new(canvas_point.x, canvas_point.y); + + self.queue.push(UiEvent::Mouse{ + event: event.to_string(), + point: canvas_position, + page_point: raw_position, + button: self.mouse_button, + buttons: self.mouse_buttons, + modifiers: self.key_modifiers, + }) + } + pub fn capture(&mut self, event:&WindowEvent){ match event{ WindowEvent::Moved(physical_pt) => { @@ -115,24 +122,20 @@ impl Sieve{ } WindowEvent::ModifiersChanged(modifiers) => { - self.key_modifiers = modifiers.state(); + self.key_modifiers = modifiers.state().into(); } WindowEvent::CursorEntered{..} => { - let mouse_event = "mouseenter".to_string(); - self.queue.push(UiEvent::Mouse(mouse_event)); + self.add_mouse_event("mouseenter"); } WindowEvent::CursorLeft{..} => { - let mouse_event = "mouseleave".to_string(); - self.queue.push(UiEvent::Mouse(mouse_event)); + self.add_mouse_event("mouseleave"); } WindowEvent::CursorMoved{position, ..} => { - if *position != self.mouse_point{ - self.mouse_point = *position; - self.queue.push(UiEvent::Mouse("mousemove".to_string())); - } + self.mouse_point = *position; + self.add_mouse_event("mousemove"); } WindowEvent::MouseWheel{delta, ..} => { @@ -148,20 +151,27 @@ impl Sieve{ } WindowEvent::MouseInput{state, button, ..} => { - let mouse_event = match state { - ElementState::Pressed => "mousedown", - ElementState::Released => "mouseup" - }.to_string(); - - self.mouse_button = match button { - MouseButton::Left => Some(0), - MouseButton::Middle => Some(1), - MouseButton::Right => Some(2), - MouseButton::Back => Some(3), - MouseButton::Forward => Some(4), - MouseButton::Other(num) => Some(*num) + let (button_id, button_bits) = match button { + MouseButton::Left => (0, 1), + MouseButton::Middle => (1, 4), + MouseButton::Right => (2, 2), + MouseButton::Back => (3, 8), + MouseButton::Forward => (4, 16), + MouseButton::Other(num) => (*num, 0), }; - self.queue.push(UiEvent::Mouse(mouse_event)); + + self.mouse_button = Some(button_id); + match state { + ElementState::Pressed => { + self.mouse_buttons |= button_bits; + self.add_mouse_event("mousedown"); + }, + ElementState::Released => { + self.mouse_buttons &= !button_bits; + self.add_mouse_event("mouseup"); + self.mouse_button = None; + }, + } } WindowEvent::KeyboardInput { event: KeyEvent { @@ -194,6 +204,7 @@ impl Sieve{ key: key_text.clone(), code: key_code.clone(), location: key_location, + modifiers: self.key_modifiers, repeat: *repeat }); @@ -211,19 +222,28 @@ impl Sieve{ match state{ // ignore keyups, just report presses & repeats ElementState::Pressed => { - // in addition to printable characters, report space & delete as input + // in addition to printable characters, report spacing & deletion as input let key_char = match &logical_key{ Character(c) => Some(c.to_string()), + Named(NamedKey::Tab) => Some("\t".to_string()), Named(NamedKey::Space) => Some(" ".to_string()), - Named(NamedKey::Backspace | NamedKey::Delete) => Some("".to_string()), + Named(NamedKey::Backspace | NamedKey::Delete | NamedKey::Enter) => Some("".to_string()), _ => None }; + let input_type = match &logical_key{ + Named(NamedKey::Backspace) => "deleteContentBackward", + Named(NamedKey::Delete) => "deleteContentForward", + Named(NamedKey::Enter) => "insertLineBreak", + _ => "insertText" + }.to_string(); + if let Some(string) = key_char{ - self.queue.push(UiEvent::Input(match !string.is_empty(){ + let data = match !string.is_empty(){ true => Some(string), false => None, - })); + }; + self.queue.push(UiEvent::Input(data, input_type)); }; }, _ => {}, @@ -233,7 +253,7 @@ impl Sieve{ WindowEvent::Ime( event, ..) => { match &event { - Ime::Preedit(string, Some(range)) => { + Ime::Preedit(string, Some(_range)) => { if !self.compose_begun{ self.queue.push(UiEvent::Composition{ event:"compositionstart".to_string(), data:"".to_string() @@ -249,7 +269,7 @@ impl Sieve{ self.queue.push(UiEvent::Composition { event:"compositionend".to_string(), data:string.clone() }); - self.queue.push(UiEvent::Input(Some(string.clone()))); // emit the composed character + self.queue.push(UiEvent::Input(Some(string.clone()), "insertCompositionText".to_string())); // emit the composed character self.compose_begun = false; }, _ => {} @@ -260,62 +280,10 @@ impl Sieve{ } } - pub fn serialize(&mut self) -> Option{ - if self.queue.is_empty(){ return None } - - let mut payload: Vec = vec![]; - let mut mouse_events: HashSet = HashSet::new(); - let mut modifiers:Option = None; - let mut last_wheel:Option<&UiEvent> = None; - - for change in &self.queue { - match change{ - UiEvent::Mouse(event_type) => { - modifiers = Some(self.key_modifiers); - mouse_events.insert(event_type.clone()); - } - UiEvent::Wheel{..} => { - modifiers = Some(self.key_modifiers); - last_wheel = Some(&change); - } - UiEvent::Input(..) | UiEvent::Keyboard{..} => { - modifiers = Some(self.key_modifiers); - payload.push(json!(change)); - } - _ => payload.push(json!(change)) - } - } - - if let Some(modfiers) = modifiers { - payload.insert(0, json!({"modifiers": modifiers})); - } - - if !mouse_events.is_empty() { - let viewport_point = LogicalPosition::::from_physical(self.mouse_point, self.dpr); - let canvas_point = self.mouse_transform.map_point((viewport_point.x, viewport_point.y)); - - payload.push(json!({ - "mouse": { - "events": mouse_events, - "button": self.mouse_button, - "x": canvas_point.x, - "y": canvas_point.y, - "pageX": viewport_point.x, - "pageY": viewport_point.y, - } - })); - - if mouse_events.contains("mouseup"){ - self.mouse_button = None; - } - } - - if let Some(wheel) = last_wheel{ - payload.push(json!(wheel)); - } - + pub fn collect(&mut self) -> serde_json::Value{ + let payload = json!(self.queue); self.queue.clear(); - Some(json!(payload)) + payload } pub fn is_empty(&self) -> bool { diff --git a/src/gui/mod.rs b/src/gui/mod.rs index b111d71d..0da33bc9 100644 --- a/src/gui/mod.rs +++ b/src/gui/mod.rs @@ -1,50 +1,23 @@ -#![allow(unused_mut)] #![allow(unused_imports)] #![allow(unused_variables)] #![allow(dead_code)] -use neon::{prelude::*, result::Throw}; -use std::iter::zip; -use serde_json::Value; -use std::cell::RefCell; -use winit::{ - event_loop::{ControlFlow, EventLoop, EventLoopProxy}, - platform::run_on_demand::EventLoopExtRunOnDemand, -}; +use neon::prelude::*; use crate::utils::*; use crate::context::BoxedContext2D; +use crate::gpu::RenderingEngine; pub mod app; -use app::App; - -pub mod event; -use event::CanvasEvent; +use app::{App, LoopMode}; pub mod window; use window::WindowSpec; -pub mod window_mgr; -use window_mgr::WindowManager; - -use crate::gpu::RenderingEngine; - -thread_local!( - // the event loop can only be run from the main thread - static EVENT_LOOP: RefCell> = RefCell::new(EventLoop::with_user_event().build().unwrap()); - static PROXY: RefCell> = RefCell::new(EVENT_LOOP.with(|event_loop| - event_loop.borrow().create_proxy() - )); -); - -pub(crate) fn new_proxy() -> EventLoopProxy{ - PROXY.with(|cell| cell.borrow().clone() ) -} +pub mod event; -pub(crate) fn add_event(event: CanvasEvent){ - PROXY.with(|cell| cell.borrow().send_event(event).ok() ); -} +pub mod window_mgr; -fn validate_gpu(cx:&mut FunctionContext) -> Result<(), Throw>{ +fn validate_gpu(cx:&mut FunctionContext) -> NeonResult<()>{ // bail out if we can't draw to the screen if let Some(reason) = RenderingEngine::default().lacks_gpu_support(){ cx.throw_error(reason)? @@ -52,74 +25,60 @@ fn validate_gpu(cx:&mut FunctionContext) -> Result<(), Throw>{ Ok(()) } -pub fn launch(mut cx: FunctionContext) -> JsResult { - let callback = cx.argument::(1)?; +pub fn register(mut cx: FunctionContext) -> JsResult { + let callback = cx.argument::(1)?.root(&mut cx); + App::register(callback); + Ok(cx.undefined()) +} + +pub fn activate(mut cx: FunctionContext) -> JsResult { validate_gpu(&mut cx)?; - // closure for using the callback to relay events to js and receive updates in return - let roundtrip = |payload:Value, windows:&mut WindowManager| -> NeonResult<()>{ - let cx = &mut cx; - let null = cx.null(); - - // send payload to js for event dispatch and canvas drawing then read back new state & page data - let events = cx.string(payload.to_string()).upcast::(); - let response = callback.call(cx, null, vec![events])? - .downcast::(cx).or_throw(cx)? - .to_vec(cx)?; - - // unpack boxed contexts & window state objects - let contexts:Vec> = response[1].downcast::(cx).or_throw(cx)?.to_vec(cx)?; - let specs:Vec = serde_json::from_str( - &response[0].downcast::(cx).or_throw(cx)?.value(cx) - ).expect("Malformed response from window event handler"); - - // pass each window's new state & page data to the window manager - zip(contexts, specs).for_each(|(boxed_ctx, spec)| { - if let Ok(ctx) = boxed_ctx.downcast::(cx){ - windows.update_window( - spec.clone(), - ctx.borrow().get_page() - ) - } - }); - Ok(()) - }; - - EVENT_LOOP.with(|event_loop| { - let mut app = App::with_callback(roundtrip); - let mut event_loop = event_loop.borrow_mut(); - event_loop.set_control_flow(ControlFlow::Wait); - event_loop.run_app_on_demand(&mut app) - }).ok(); + let (deferred, promise) = cx.promise(); + let channel = cx.channel(); - Ok(cx.undefined()) + App::activate(channel, deferred); + + Ok(promise) } pub fn set_rate(mut cx: FunctionContext) -> JsResult { - let fps = float_arg(&mut cx, 1, "framesPerSecond")? as u64; - add_event(CanvasEvent::FrameRate(fps)); + let fps = float_arg(&mut cx, 1, "framesPerSecond")?; + App::set_fps(fps); Ok(cx.number(fps as f64)) } +pub fn set_mode(mut cx: FunctionContext) -> JsResult { + let mode = string_arg(&mut cx, 1, "eventLoopMode")?; + let loop_mode = match mode.as_str(){ + "node" => Ok(LoopMode::Node), + "native" => Ok(LoopMode::Native), + _ => cx.throw_error(format!("Invalid event loop mode: {}", mode)) + }?; + + App::set_mode(loop_mode); + Ok(cx.string(mode)) +} + pub fn open(mut cx: FunctionContext) -> JsResult { - let win_config = string_arg(&mut cx, 0, "Window configuration")?; - let context = cx.argument::(1)?; + let win_config = string_arg(&mut cx, 1, "Window configuration")?; + let context = cx.argument::(2)?; let spec = serde_json::from_str::(&win_config).expect("Invalid window state"); validate_gpu(&mut cx)?; - add_event(CanvasEvent::Open(spec, context.borrow().get_page())); + App::open_window(spec, context.borrow().get_page()); Ok(cx.undefined()) } pub fn close(mut cx: FunctionContext) -> JsResult { - let token = string_arg(&mut cx, 0, "windowID")?; - add_event(CanvasEvent::Close(token)); + let token = float_arg(&mut cx, 1, "windowID")? as u32; + App::close_window(token); Ok(cx.undefined()) } pub fn quit(mut cx: FunctionContext) -> JsResult { - add_event(CanvasEvent::Quit); + App::quit(); Ok(cx.undefined()) } diff --git a/src/gui/window.rs b/src/gui/window.rs index 8adce81e..d8fba10e 100644 --- a/src/gui/window.rs +++ b/src/gui/window.rs @@ -1,23 +1,21 @@ -use std::sync::Arc; -use skia_safe::{Matrix, Color, Paint}; +use std::{str::FromStr, sync::Arc}; +use skia_safe::{Matrix, Color}; use serde::{Serialize, Deserialize}; use winit::{ - dpi::{LogicalSize, LogicalPosition, PhysicalSize}, - event_loop::{ActiveEventLoop, EventLoopProxy}, - window::{Window as WinitWindow, CursorIcon, Fullscreen}, + dpi::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize}, + window::{CursorIcon, Fullscreen, Window as WinitWindow, WindowId}, + event_loop::ActiveEventLoop, }; -#[cfg(target_os = "macos" )] -use winit::platform::macos::WindowExtMacOS; use crate::utils::css_to_color; use crate::gpu::Renderer; use crate::context::page::Page; -use super::event::CanvasEvent; +use super::event::Sieve; #[derive(Deserialize, Serialize, Debug, Clone)] #[serde(rename_all = "camelCase")] pub struct WindowSpec { - pub id: String, + pub id: u32, pub left: Option, pub top: Option, pub title: String, @@ -28,9 +26,7 @@ pub struct WindowSpec { pub page: u32, pub width: f32, pub height: f32, - #[serde(with = "Cursor")] - pub cursor: CursorIcon, - pub cursor_hidden: bool, + pub cursor: String, pub fit: Fit, } @@ -49,18 +45,19 @@ pub enum Cursor { NResize, NsResize, NwResize, NwseResize, Pointer, Progress, RowResize, SeResize, SResize, SwResize, Text, VerticalText, Wait, WResize, ZoomIn, ZoomOut, } + pub struct Window { pub handle: Arc, - proxy: EventLoopProxy, + pub spec: WindowSpec, + pub sieve: Sieve, renderer: Renderer, - fit: Fit, background: Color, page: Page, suspended: bool, } impl Window { - pub fn new(event_loop:&ActiveEventLoop, proxy:EventLoopProxy, spec: &mut WindowSpec, page: &Page) -> Self { + pub fn new(event_loop:&ActiveEventLoop, mut spec:WindowSpec, page:&Page) -> Self { let size:LogicalSize = LogicalSize::new(spec.width as i32, spec.height as i32); let background = match css_to_color(&spec.background){ Some(color) => color, @@ -77,32 +74,58 @@ impl Window { .with_title(spec.title.clone()) .with_visible(false) .with_resizable(spec.resizable); + let handle = Arc::new(event_loop.create_window(window_attributes).unwrap()); + let renderer = Renderer::for_window(&event_loop, handle.clone()); + let sieve = Sieve::new(handle.scale_factor()); + + let cursor_icon = CursorIcon::from_str(&spec.cursor).ok(); + handle.set_cursor(cursor_icon.unwrap_or_default()); + handle.set_cursor_visible(cursor_icon.is_some()); if let (Some(left), Some(top)) = (spec.left, spec.top){ handle.set_outer_position(LogicalPosition::new(left, top)); } - let renderer = Renderer::for_window(&event_loop, handle.clone()); + Self{ spec, handle, sieve, renderer, page:page.clone(), suspended:false, background} + } - Self{ handle, proxy, renderer, page:page.clone(), fit:spec.fit, suspended:false, background } + pub fn id(&self) -> WindowId { + self.handle.id() } pub fn resize(&mut self, size: PhysicalSize){ - if let Some(monitor) = self.handle.current_monitor(){ - self.renderer.resize(size); - self.reposition_ime(size); + self.renderer.resize(size); + self.reposition_ime(size); + self.update_fit(); + + let LogicalSize{width, height} = self.handle.inner_size().to_logical::(self.handle.scale_factor()); + let is_fullscreen = self.handle.fullscreen().is_some() + && width >= self.spec.width + && height >= self.spec.height; + + self.spec = WindowSpec{width, height, ..self.spec.clone()}; + if self.spec.fullscreen != is_fullscreen{ + self.sieve.go_fullscreen(is_fullscreen); + self.spec.fullscreen = is_fullscreen; + } + } - let id = self.handle.id(); - self.proxy.send_event(CanvasEvent::Transform(id, self.fitting_matrix().invert() )).ok(); - self.proxy.send_event(CanvasEvent::InFullscreen(id, monitor.size() == size )).ok(); + pub fn reposition(&mut self, loc:LogicalPosition){ + self.spec.left = Some(loc.x as _); + self.spec.top = Some(loc.y as _); + } + + pub fn update_fit(&mut self){ + if let Some(fit) = self.fitting_matrix().invert(){ + self.sieve.use_transform(fit); } } pub fn reposition_ime(&mut self, size:PhysicalSize){ // place the input region in the bottom left corner so the UI doesn't cover the window let dpr = self.handle.scale_factor(); - let window_height = size.to_logical::(dpr).height; + let window_height = size.to_logical::(dpr).height; self.handle.set_ime_allowed(true); self.handle.set_ime_cursor_area( LogicalPosition::new(15, window_height-20), LogicalSize::new(100, 15) @@ -116,7 +139,7 @@ impl Window { let fit_x = size.width / dims.width; let fit_y = size.height / dims.height; - let sf = match self.fit{ + let sf = match self.spec.fit{ Fit::Cover => fit_x.max(fit_y), Fit::ScaleDown => fit_x.min(fit_y).min(1.0), Fit::Contain => fit_x.min(fit_y), @@ -125,12 +148,12 @@ impl Window { _ => 1.0 }; - let (x_scale, y_scale) = match self.fit{ + let (x_scale, y_scale) = match self.spec.fit{ Fit::Fill => (fit_x, fit_y), _ => (sf, sf) }; - let (x_shift, y_shift) = match self.fit{ + let (x_shift, y_shift) = match self.spec.fit{ Fit::Resize => (0.0, 0.0), _ => ( (size.width - dims.width * x_scale) / 2.0, (size.height - dims.height * y_scale) / 2.0 ) @@ -142,80 +165,84 @@ impl Window { (x_shift, y_shift) ); matrix - } - + } pub fn redraw(&mut self){ - let paint = Paint::default(); - let matrix = self.fitting_matrix(); - let (clip, _) = matrix.map_rect(self.page.bounds); - - self.renderer.draw(&self.handle, |canvas, _size| { - canvas.clear(self.background); - canvas.clip_rect(clip, None, Some(true)); - canvas.draw_picture(self.page.get_picture(None).unwrap(), Some(&matrix), Some(&paint)); - }).unwrap(); - } - - pub fn handle_event(&mut self, event:CanvasEvent){ - match event { - CanvasEvent::Page(page) => { - self.page = page; - self.handle.request_redraw(); - } - CanvasEvent::Visible(flag) => { - self.handle.set_visible(flag); - } - CanvasEvent::Resizable(flag) => { - self.handle.set_resizable(flag); - } - CanvasEvent::Title(title) => { - self.handle.set_title(&title); - } - CanvasEvent::Cursor(icon) => { - if let Some(icon) = icon{ - self.handle.set_cursor(icon); - } - self.handle.set_cursor_visible(icon.is_some()); - } - CanvasEvent::Fit(mode) => { - self.fit = mode; - } - CanvasEvent::Background(color) => { - self.background = color; - } - CanvasEvent::Size(size) => { - let size:PhysicalSize = size.to_physical(self.handle.scale_factor()); - if let Some(to_size) = self.handle.request_inner_size(size){ - self.resize(to_size); - } - } - CanvasEvent::Position(loc) => { - self.handle.set_outer_position(loc); - } - CanvasEvent::Fullscreen(to_fullscreen) => { - match to_fullscreen{ - true => self.handle.set_fullscreen( Some(Fullscreen::Borderless(None)) ), - false => self.handle.set_fullscreen( None ) - } - } - CanvasEvent::WindowResized(size) => { - self.resize(size); - } - CanvasEvent::RedrawingSuspended(suspended) => { - self.suspended = suspended; - if !suspended{ - self.redraw(); - } - } - CanvasEvent::RedrawRequested => { - if !self.suspended{ - self.redraw() - } - } + if !self.suspended{ + self.renderer.draw(self.page.clone(), self.fitting_matrix(), self.background); + } + } - _ => {} + pub fn set_page(&mut self, page:Page){ + if self.page != page{ + self.handle.request_redraw(); } + self.page = page; + } + + pub fn set_visible(&mut self, flag:bool){ + self.handle.set_visible(flag); } + + pub fn set_resizable(&mut self, flag:bool){ + self.handle.set_resizable(flag); + } + + pub fn set_title(&mut self, title:&str){ + self.handle.set_title(title); + } + + pub fn set_cursor(&mut self, icon:&str){ + let cursor_icon = CursorIcon::from_str(icon).ok(); + self.handle.set_cursor(cursor_icon.unwrap_or_default()); + self.handle.set_cursor_visible(cursor_icon.is_some()); + } + + pub fn set_fit(&mut self, mode:Fit){ + self.spec.fit = mode; + } + + pub fn set_background(&mut self, color:Color){ + if self.background != color{ + self.background = color; + self.handle.request_redraw(); + } + } + + pub fn set_size(&mut self, size:LogicalSize){ + let size:PhysicalSize = size.to_physical(self.handle.scale_factor()); + if let Some(to_size) = self.handle.request_inner_size(size){ + self.resize(to_size); + } + } + + pub fn set_position(&mut self, loc:LogicalPosition){ + self.handle.set_outer_position(loc); + self.reposition(loc); + } + + pub fn set_fullscreen(&mut self, to_fullscreen:bool){ + match to_fullscreen{ + true => self.handle.set_fullscreen( Some(Fullscreen::Borderless(None)) ), + false => self.handle.set_fullscreen( None ) + } + } + + pub fn did_move(&mut self, size:PhysicalPosition){ + self.reposition(size.to_logical(self.handle.scale_factor())); + } + + pub fn did_resize(&mut self, size:PhysicalSize){ + self.resize(size); + } + + pub fn set_redrawing_suspended(&mut self, suspended:bool){ + self.suspended = suspended; + if !suspended{ + self.handle.request_redraw(); + } + } + + } diff --git a/src/gui/window_mgr.rs b/src/gui/window_mgr.rs index 21983e09..080e1d97 100644 --- a/src/gui/window_mgr.rs +++ b/src/gui/window_mgr.rs @@ -1,38 +1,29 @@ -use std::thread; use serde_json::json; -use skia_safe::Matrix; -use crossbeam::channel::{self, Sender}; use serde_json::{Map, Value}; use winit::{ dpi::{LogicalSize, LogicalPosition}, + event_loop::ActiveEventLoop, event::WindowEvent, - event_loop::{ActiveEventLoop, EventLoopProxy}, window::WindowId, }; use crate::utils::css_to_color; use crate::context::page::Page; -use super::event::{CanvasEvent, Sieve}; use super::window::{Window, WindowSpec}; -struct WindowRef { tx: Sender, id: WindowId, spec: WindowSpec, sieve:Sieve } - #[derive(Default)] pub struct WindowManager { - windows: Vec, + windows: Vec, last: Option>, } impl WindowManager { - pub fn add(&mut self, event_loop:&ActiveEventLoop, proxy:EventLoopProxy, mut spec: WindowSpec, page: Page) { - let mut window = Window::new(event_loop, proxy, &mut spec, &page); - let id = window.handle.id(); - let (tx, rx) = channel::bounded(50); - let mut sieve = Sieve::new(window.handle.scale_factor()); - if let Some(fit) = window.fitting_matrix().invert(){ - sieve.use_transform(fit); - } + pub fn add(&mut self, event_loop:&ActiveEventLoop, spec:WindowSpec, page:Page) { + let mut window = Window::new(event_loop, spec, &page); + + // make sure mouse events use canvas-relative coordinates (in case win size doesn't match) + window.update_fit(); // cascade the windows based on the position of the most recently opened let dpr = window.handle.scale_factor(); @@ -40,142 +31,84 @@ impl WindowManager { if let Ok(inset) = window.handle.inner_position().map(|pt| pt.to_logical::(dpr)){ let delta = inset.y - auto_loc.y; let reference = self.last.unwrap_or(auto_loc); - let (left, top) = ( spec.left.unwrap_or(reference.x), spec.top.unwrap_or(reference.y) ); + let (left, top) = ( window.spec.left.unwrap_or(reference.x), window.spec.top.unwrap_or(reference.y) ); window.handle.set_outer_position(LogicalPosition::new(left, top)); window.handle.set_visible(true); - spec.left = Some(left); - spec.top = Some(top); + window.spec.left = Some(left); + window.spec.top = Some(top); self.last = Some(LogicalPosition::new(left + delta, top + delta)); } } - // hold a reference to the window on the main thread… - self.windows.push( WindowRef{ id, spec, tx, sieve } ); - - // …but let the window's event handler & renderer run on a background thread - thread::spawn(move || { - while let Ok(event) = rx.recv() { - let mut queue = vec![event]; - while !rx.is_empty(){ - queue.push(rx.recv().unwrap()); - } - - let mut needs_redraw = None; - queue.drain(..).for_each(|event|{ - match event { - CanvasEvent::RedrawRequested => needs_redraw = Some(event), - _ => window.handle_event(event) - } - }); - - // deduplicate and defer redraw requests until all other events were handled - if let Some(event) = needs_redraw { - window.handle_event(event) - } - } - }); - + self.windows.push( window ); } pub fn remove(&mut self, window_id:&WindowId){ - self.windows.retain(|win| win.id != *window_id); + self.windows.retain(|win| win.id() != *window_id); } - pub fn remove_by_token(&mut self, token:&str){ + pub fn remove_by_token(&mut self, token:u32){ self.windows.retain(|win| win.spec.id != token); } - pub fn send_event(&self, window_id:&WindowId, event:CanvasEvent){ - if let Some(tx) = self.windows.iter().find(|win| win.id == *window_id).map(|win| &win.tx){ - tx.send(event).ok(); - } + pub fn remove_all(&mut self){ + self.windows.clear(); } - pub fn update_window(&mut self, mut spec:WindowSpec, page:Page){ - let mut updates:Vec = vec![]; - - if let Some(mut win) = self.windows.iter_mut().find(|win| win.spec.id == spec.id){ + if let Some(win) = self.windows.iter_mut().find(|win| win.spec.id == spec.id){ if spec.width != win.spec.width || spec.height != win.spec.height { - updates.push(CanvasEvent::Size(LogicalSize::new(spec.width as u32, spec.height as u32))); + win.set_size(LogicalSize::new(spec.width as u32, spec.height as u32)); } if let (Some(left), Some(top)) = (spec.left, spec.top){ if spec.left != win.spec.left || spec.top != win.spec.top { - updates.push(CanvasEvent::Position(LogicalPosition::new(left as i32, top as i32))); + win.set_position(LogicalPosition::new(left as i32, top as i32)); } } if spec.title != win.spec.title { - updates.push(CanvasEvent::Title(spec.title.clone())); + win.set_title(&spec.title); } if spec.visible != win.spec.visible { - updates.push(CanvasEvent::Visible(spec.visible)); + win.set_visible(spec.visible); } if spec.fullscreen != win.spec.fullscreen { - updates.push(CanvasEvent::Fullscreen(spec.fullscreen)); + win.set_fullscreen(spec.fullscreen); + win.sieve.go_fullscreen(spec.fullscreen); + } + + if spec.resizable != win.spec.resizable { + win.set_resizable(spec.resizable); } - if spec.cursor != win.spec.cursor || spec.cursor_hidden != win.spec.cursor_hidden { - let icon = if spec.cursor_hidden{ None }else{ Some(spec.cursor) }; - updates.push(CanvasEvent::Cursor(icon)); + if spec.cursor != win.spec.cursor { + win.set_cursor(&spec.cursor); } if spec.fit != win.spec.fit { - updates.push(CanvasEvent::Fit(spec.fit)); + win.set_fit(spec.fit); } if spec.background != win.spec.background { if let Some(color) = css_to_color(&spec.background) { - updates.push(CanvasEvent::Background(color)); + win.set_background(color); }else{ spec.background = win.spec.background.clone(); } } - updates.push(CanvasEvent::Page(page)); - - updates.drain(..).for_each(|event| { - win.tx.send(event).ok(); - }); + win.set_page(page); win.spec = spec; } } - pub fn capture_ui_event(&mut self, window_id:&WindowId, event:&WindowEvent){ - if let Some(win) = self.windows.iter_mut().find(|win| win.id == *window_id){ - win.sieve.capture(event); - } - } - - pub fn use_ui_transform(&mut self, window_id:&WindowId, matrix:&Option){ - if let Some(win) = self.windows.iter_mut().find(|win| win.id == *window_id){ - if let Some(matrix) = matrix { - win.sieve.use_transform(*matrix); - } - } - } - - pub fn set_fullscreen_state(&mut self, window_id:&WindowId, is_fullscreen:bool){ - if let Some(win) = self.windows.iter_mut().find(|win| win.id == *window_id){ - // tell the window to change state - win.tx.send(CanvasEvent::Fullscreen(is_fullscreen)).ok(); - } - // and make sure the change is reflected in local state - self.use_fullscreen_state(window_id, is_fullscreen); - } - - pub fn use_fullscreen_state(&mut self, window_id:&WindowId, is_fullscreen:bool){ - if let Some(mut win) = self.windows.iter_mut().find(|win| win.id == *window_id){ - if win.spec.fullscreen != is_fullscreen{ - win.sieve.go_fullscreen(is_fullscreen); - win.spec.fullscreen = is_fullscreen; - } - } + pub fn find(&mut self, id:&WindowId, f:F) where F:FnMut(&mut Window){ + self.windows.iter_mut().find(|win| win.id() == *id).map(f); } pub fn has_ui_changes(&self) -> bool { @@ -186,10 +119,10 @@ impl WindowManager { let mut ui = Map::new(); let mut state = Map::new(); self.windows.iter_mut().for_each(|win|{ - if let Some(payload) = win.sieve.serialize(){ - ui.insert(win.spec.id.clone(), payload); + if !win.sieve.is_empty(){ + ui.insert(win.spec.id.to_string(), win.sieve.collect()); } - state.insert(win.spec.id.clone(), json!(win.spec)); + state.insert(win.spec.id.to_string(), json!(win.spec)); }); json!({ "ui": ui, "state": state }) } @@ -197,15 +130,11 @@ impl WindowManager { pub fn get_geometry(&mut self) -> Value { let mut positions = Map::new(); self.windows.iter_mut().for_each(|win|{ - positions.insert(win.spec.id.clone(), json!({"left":win.spec.left, "top":win.spec.top})); + positions.insert(win.spec.id.to_string(), json!({"left":win.spec.left, "top":win.spec.top})); }); json!({"geom":positions}) } - pub fn len(&self) -> usize { - self.windows.len() - } - pub fn is_empty(&self) -> bool { self.windows.len() == 0 } diff --git a/src/lib.rs b/src/lib.rs index 4813f9f6..f2a905b0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -245,11 +245,13 @@ fn main(mut cx: ModuleContext) -> NeonResult<()> { // -- Window ----------------------------------------------------------------------------------- #[cfg(feature = "window")] { - cx.export_function("App_launch", gui::launch)?; + cx.export_function("App_register", gui::register)?; + cx.export_function("App_activate", gui::activate)?; cx.export_function("App_quit", gui::quit)?; cx.export_function("App_closeWindow", gui::close)?; cx.export_function("App_openWindow", gui::open)?; cx.export_function("App_setRate", gui::set_rate)?; + cx.export_function("App_setMode", gui::set_mode)?; } Ok(()) diff --git a/test/canvas.test.js b/test/canvas.test.js index 8738017d..b0605529 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -455,7 +455,6 @@ describe("Canvas", ()=>{ canvas.saveAsSync(`${TMP}/output-{2}.png`) let files = findTmp(`/output-0?.png`) - console.log({files}) expect(files.length).toEqual(colors.length+1) for (const [i, fn] of files.entries()){ @@ -464,8 +463,6 @@ describe("Canvas", ()=>{ await img.decode() expect(img.complete).toBe(true) - console.log({img}) - // second page inherits the first's size, then they increase let dim = i<2 ? 512 : 512 + 100 * (i-1) expect(img.width).toEqual(dim)