From d43f5fe99a7fefc5efc613aa7cc04d16020c08e7 Mon Sep 17 00:00:00 2001 From: Florian Blasius Date: Fri, 29 Sep 2023 08:29:36 +0200 Subject: [PATCH] Added window js wrapper for napi (#3544) * Added window js wrapper for napi * Remove position tests. * Update api/napi/src/types/size.rs Co-authored-by: Simon Hausmann * Code review fixes --------- Co-authored-by: Simon Hausmann --- api/napi/__test__/compiler.spec.ts | 4 +- api/napi/__test__/types.spec.ts | 5 +- api/napi/__test__/window.spec.ts | 38 +++++++ api/napi/src/interpreter.rs | 3 + .../src/interpreter/component_instance.rs | 8 ++ api/napi/src/interpreter/window.rs | 103 ++++++++++++++++++ api/napi/src/types.rs | 6 + api/napi/src/types/point.rs | 52 +++++++++ api/napi/src/types/size.rs | 60 ++++++++++ 9 files changed, 274 insertions(+), 5 deletions(-) create mode 100644 api/napi/__test__/window.spec.ts create mode 100644 api/napi/src/interpreter/window.rs create mode 100644 api/napi/src/types/point.rs create mode 100644 api/napi/src/types/size.rs diff --git a/api/napi/__test__/compiler.spec.ts b/api/napi/__test__/compiler.spec.ts index 875f11c53..f1d165bc4 100644 --- a/api/napi/__test__/compiler.spec.ts +++ b/api/napi/__test__/compiler.spec.ts @@ -3,7 +3,7 @@ import test from 'ava' -import { ComponentCompiler, ComponentDefinition, ComponentInstance, Property, ValueType } from '../index' +import { ComponentCompiler, ComponentDefinition, ComponentInstance, ValueType} from '../index' test('get/set include paths', (t) => { let compiler = new ComponentCompiler; @@ -236,4 +236,4 @@ test('non-existent properties and callbacks', (t) => { }); t.is(callback_err!.code, 'GenericFailure'); t.is(callback_err!.message, 'Callback non-existent-callback not found in the component'); -}) +}) \ No newline at end of file diff --git a/api/napi/__test__/types.spec.ts b/api/napi/__test__/types.spec.ts index b8c018878..538613660 100644 --- a/api/napi/__test__/types.spec.ts +++ b/api/napi/__test__/types.spec.ts @@ -3,7 +3,7 @@ import test from 'ava'; -import { Brush, Color, ArrayModel } from '../index' +import { Brush, Color, ArrayModel, Size } from '../index' test('Color from fromRgb', (t) => { let color = Color.fromRgb(100, 110, 120); @@ -76,7 +76,6 @@ test('ArrayModel setRowData', (t) => { t.is(arrayModel.rowData(0), 2); }) - test('ArrayModel remove', (t) => { let arrayModel = new ArrayModel([0, 2, 1]); @@ -87,4 +86,4 @@ test('ArrayModel remove', (t) => { arrayModel.remove(0, 2); t.is(arrayModel.rowCount(), 1); t.is(arrayModel.rowData(0), 1); -}) \ No newline at end of file +}) diff --git a/api/napi/__test__/window.spec.ts b/api/napi/__test__/window.spec.ts new file mode 100644 index 000000000..de4d30a99 --- /dev/null +++ b/api/napi/__test__/window.spec.ts @@ -0,0 +1,38 @@ +// Copyright © SixtyFPS GmbH +// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial + +import test from 'ava' + +import { ComponentCompiler, Window } from '../index' + +test('Window constructor', (t) => { + t.throws(() => { + new Window() + }, + { + code: "GenericFailure", + message: "Window can only be created by using a Component." + } + ); +}) + +test('Window show / hide', (t) => { + let compiler = new ComponentCompiler; + let definition = compiler.buildFromSource(` + + export component App inherits Window { + width: 300px; + height: 300px; + }`, ""); + t.not(definition, null); + + let instance = definition!.create(); + t.not(instance, null); + + let window = instance!.window(); + t.is(window.isVisible, false); + window.show(); + t.is(window.isVisible, true); + window.hide(); + t.is(window.isVisible, false); +}) \ No newline at end of file diff --git a/api/napi/src/interpreter.rs b/api/napi/src/interpreter.rs index 656e37e92..876fc3b9c 100644 --- a/api/napi/src/interpreter.rs +++ b/api/napi/src/interpreter.rs @@ -15,3 +15,6 @@ pub use diagnostic::*; mod value; pub use value::*; + +mod window; +pub use window::*; diff --git a/api/napi/src/interpreter/component_instance.rs b/api/napi/src/interpreter/component_instance.rs index ef980c273..55945adbf 100644 --- a/api/napi/src/interpreter/component_instance.rs +++ b/api/napi/src/interpreter/component_instance.rs @@ -2,9 +2,12 @@ // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial use i_slint_compiler::langtype::Type; +use i_slint_core::window::WindowInner; use napi::{Env, Error, JsFunction, JsUnknown, NapiRaw, NapiValue, Ref, Result}; use slint_interpreter::{ComponentHandle, ComponentInstance, Value}; +use crate::JsWindow; + use super::JsComponentDefinition; #[napi(js_name = "ComponentInstance")] @@ -329,6 +332,11 @@ impl JsComponentInstance { .map_err(|_| napi::Error::from_reason("Cannot invoke callback."))?; super::to_js_unknown(&env, &result) } + + #[napi] + pub fn window(&self) -> Result { + Ok(JsWindow { inner: WindowInner::from_pub(self.inner.window()).window_adapter() }) + } } // Wrapper around Ref<>, which requires manual ref-counting. diff --git a/api/napi/src/interpreter/window.rs b/api/napi/src/interpreter/window.rs new file mode 100644 index 000000000..61d66b67a --- /dev/null +++ b/api/napi/src/interpreter/window.rs @@ -0,0 +1,103 @@ +// Copyright © SixtyFPS GmbH +// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial + +use crate::types::{JsPoint, JsSize}; +use i_slint_core::window::WindowAdapterRc; +use slint_interpreter::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize}; + +#[napi(js_name = "Window")] +pub struct JsWindow { + pub(crate) inner: WindowAdapterRc, +} + +impl From for JsWindow { + fn from(instance: WindowAdapterRc) -> Self { + Self { inner: instance } + } +} + +#[napi] +impl JsWindow { + #[napi(constructor)] + pub fn new() -> napi::Result { + Err(napi::Error::from_reason( + "Window can only be created by using a Component.".to_string(), + )) + } + + #[napi] + pub fn show(&self) -> napi::Result<()> { + self.inner + .window() + .show() + .map_err(|_| napi::Error::from_reason("Cannot show window.".to_string())) + } + + #[napi] + pub fn hide(&self) -> napi::Result<()> { + self.inner + .window() + .hide() + .map_err(|_| napi::Error::from_reason("Cannot hide window.".to_string())) + } + + #[napi(getter)] + pub fn is_visible(&self) -> bool { + self.inner.window().is_visible() + } + + #[napi(getter)] + pub fn get_logical_position(&self) -> JsPoint { + let pos = self.inner.window().position().to_logical(self.inner.window().scale_factor()); + JsPoint { x: pos.x as f64, y: pos.y as f64 } + } + + #[napi(setter)] + pub fn set_logical_position(&self, position: JsPoint) { + self.inner + .window() + .set_position(LogicalPosition { x: position.x as f32, y: position.y as f32 }); + } + + #[napi(getter)] + pub fn get_physical_position(&self) -> JsPoint { + let pos = self.inner.window().position(); + JsPoint { x: pos.x as f64, y: pos.y as f64 } + } + + #[napi(setter)] + pub fn set_physical_position(&self, position: JsPoint) { + self.inner.window().set_position(PhysicalPosition { + x: position.x.floor() as i32, + y: position.y.floor() as i32, + }); + } + + #[napi(getter)] + pub fn get_logical_size(&self) -> JsSize { + let size = self.inner.window().size().to_logical(self.inner.window().scale_factor()); + JsSize { width: size.width as f64, height: size.height as f64 } + } + + #[napi(setter)] + pub fn set_logical_size(&self, size: JsSize) { + self.inner.window().set_size(LogicalSize::from_physical( + PhysicalSize { width: size.width.floor() as u32, height: size.height.floor() as u32 }, + self.inner.window().scale_factor(), + )); + } + + #[napi(getter)] + pub fn get_physical_size(&self) -> JsSize { + let size = self.inner.window().size(); + JsSize { width: size.width as f64, height: size.height as f64 } + } + + #[napi(setter)] + pub fn set_physical_size(&self, size: JsSize) { + self.inner.window().set_size(PhysicalSize { + width: size.width.floor() as u32, + height: size.height.floor() as u32, + }); + } +} diff --git a/api/napi/src/types.rs b/api/napi/src/types.rs index 5d6f2f0d5..68413a21b 100644 --- a/api/napi/src/types.rs +++ b/api/napi/src/types.rs @@ -9,3 +9,9 @@ pub use image_data::*; mod model; pub use model::*; + +mod point; +pub use point::*; + +mod size; +pub use size::*; diff --git a/api/napi/src/types/point.rs b/api/napi/src/types/point.rs new file mode 100644 index 000000000..81db851f2 --- /dev/null +++ b/api/napi/src/types/point.rs @@ -0,0 +1,52 @@ +// Copyright © SixtyFPS GmbH +// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial + +use napi::{ + bindgen_prelude::{FromNapiValue, Object}, + JsUnknown, +}; + +#[napi(js_name = Point)] +pub struct JsPoint { + pub x: f64, + pub y: f64, +} + +#[napi] +impl JsPoint { + #[napi(constructor)] + pub fn new(x: f64, y: f64) -> Self { + Self { x, y } + } +} + +impl FromNapiValue for JsPoint { + unsafe fn from_napi_value( + env: napi::sys::napi_env, + napi_val: napi::sys::napi_value, + ) -> napi::Result { + let obj = unsafe { Object::from_napi_value(env, napi_val)? }; + let x: f64 = obj + .get::<_, JsUnknown>("x") + .ok() + .flatten() + .and_then(|p| p.coerce_to_number().ok()) + .and_then(|f64_num| f64_num.try_into().ok()) + .ok_or_else( + || napi::Error::from_reason( + format!("Cannot convert object to Point, because the provided object does not have an f64 x property") + ))?; + let y: f64 = obj + .get::<_, JsUnknown>("y") + .ok() + .flatten() + .and_then(|p| p.coerce_to_number().ok()) + .and_then(|f64_num| f64_num.try_into().ok()) + .ok_or_else( + || napi::Error::from_reason( + format!("Cannot convert object to Point, because the provided object does not have an f64 y property") + ))?; + + Ok(JsPoint { x, y }) + } +} diff --git a/api/napi/src/types/size.rs b/api/napi/src/types/size.rs new file mode 100644 index 000000000..6a5606264 --- /dev/null +++ b/api/napi/src/types/size.rs @@ -0,0 +1,60 @@ +// Copyright © SixtyFPS GmbH +// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial + +use napi::{ + bindgen_prelude::{FromNapiValue, Object}, + JsUnknown, Result, +}; + +#[napi(js_name = Size)] +pub struct JsSize { + pub width: f64, + pub height: f64, +} + +#[napi] +impl JsSize { + #[napi(constructor)] + pub fn new(width: f64, height: f64) -> Result { + if width < 0. { + return Err(napi::Error::from_reason("width cannot be negative".to_string())); + } + + if height < 0. { + return Err(napi::Error::from_reason("height cannot be negative".to_string())); + } + + Ok(Self { width, height }) + } +} + +impl FromNapiValue for JsSize { + unsafe fn from_napi_value( + env: napi::sys::napi_env, + napi_val: napi::sys::napi_value, + ) -> napi::Result { + let obj = unsafe { Object::from_napi_value(env, napi_val)? }; + let width: f64 = obj + .get::<_, JsUnknown>("width") + .ok() + .flatten() + .and_then(|p| p.coerce_to_number().ok()) + .and_then(|f64_num| f64_num.try_into().ok()) + .ok_or_else( + || napi::Error::from_reason( + format!("Cannot convert object to Size, because the provided object does not have an f64 width property") + ))?; + let height: f64 = obj + .get::<_, JsUnknown>("height") + .ok() + .flatten() + .and_then(|p| p.coerce_to_number().ok()) + .and_then(|f64_num| f64_num.try_into().ok()) + .ok_or_else( + || napi::Error::from_reason( + format!("Cannot convert object to Size, because the provided object does not have an f64 height property") + ))?; + + Ok(JsSize { width, height }) + } +}