node: api review adjustements part I (#3766)

* Update api/node/src/types/brush.rs

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* Update tests/cases/callbacks/handler_with_arg.slint

Co-authored-by: Olivier Goffart <olivier.goffart@slint.dev>

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Olivier Goffart <olivier.goffart@slint.dev>
This commit is contained in:
Florian Blasius 2023-10-26 10:02:49 +02:00 committed by GitHub
parent 081e7fe456
commit 0045787e1c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 340 additions and 142 deletions

View file

@ -128,7 +128,7 @@ export component MyComponent inherits Window {
} }
``` ```
The callbacks in JavaScript are exposed as property that has a setHandler function, and that can be called as a function. The callbacks in JavaScript are exposed as property and that can be called as a function.
**`main.js`** **`main.js`**
@ -139,7 +139,7 @@ let ui = slint.loadFile("ui/my-component.slint");
let component = new ui.MyComponent(); let component = new ui.MyComponent();
// connect to a callback // connect to a callback
component.clicked.setHandler(function() { console.log("hello"); }) component.clicked = function() { console.log("hello"); };
// emit a callback // emit a callback
component.clicked(); component.clicked();
``` ```

View file

@ -22,25 +22,25 @@ test('loadFile', (t) => {
t.is(error?.message, "Could not compile " + errorPath); t.is(error?.message, "Could not compile " + errorPath);
t.deepEqual(error?.diagnostics, [ t.deepEqual(error?.diagnostics, [
{ {
column: 18, columnNumber: 18,
level: 0, level: 0,
lineNumber: 7, lineNumber: 7,
message: 'Missing type. The syntax to declare a property is `property <type> name;`. Only two way bindings can omit the type', message: 'Missing type. The syntax to declare a property is `property <type> name;`. Only two way bindings can omit the type',
sourceFile: errorPath fileName: errorPath
}, },
{ {
column: 22, columnNumber: 22,
level: 0, level: 0,
lineNumber: 7, lineNumber: 7,
message: 'Syntax error: expected \';\'', message: 'Syntax error: expected \';\'',
sourceFile: errorPath fileName: errorPath
}, },
{ {
column: 22, columnNumber: 22,
level: 0, level: 0,
lineNumber: 7, lineNumber: 7,
message: 'Parse error', message: 'Parse error',
sourceFile: errorPath fileName: errorPath
}, },
]); ]);
}) })
@ -50,7 +50,6 @@ test('constructor parameters', (t) => {
let hello = ""; let hello = "";
let test = new demo.Test({ say_hello: function() { hello = "hello"; }, check: "test"}); let test = new demo.Test({ say_hello: function() { hello = "hello"; }, check: "test"});
// test.say_hello.setHandler(function () { blub = "hello"; });
test.say_hello(); test.say_hello();
t.is(test.check, "test"); t.is(test.check, "test");

View file

@ -15,6 +15,20 @@ test('get/set include paths', (t) => {
t.deepEqual(compiler.includePaths, ["path/one/", "path/two/", "path/three/"]); t.deepEqual(compiler.includePaths, ["path/one/", "path/two/", "path/three/"]);
}) })
test('get/set library paths', (t) => {
let compiler = new private_api.ComponentCompiler;
compiler.libraryPaths = {
"libfile.slint" : "third_party/libfoo/ui/lib.slint",
"libdir" : "third_party/libbar/ui/",
};
t.deepEqual(compiler.libraryPaths, {
"libfile.slint" : "third_party/libfoo/ui/lib.slint",
"libdir" : "third_party/libbar/ui/",
});
})
test('get/set style', (t) => { test('get/set style', (t) => {
let compiler = new private_api.ComponentCompiler; let compiler = new private_api.ComponentCompiler;
@ -209,8 +223,8 @@ test('compiler diagnostics', (t) => {
level: 0, level: 0,
message: 'Parse error', message: 'Parse error',
lineNumber: 2, lineNumber: 2,
column: 12, columnNumber: 12,
sourceFile: 'testsource.slint' fileName: 'testsource.slint'
}); });
}) })

View file

@ -190,9 +190,9 @@ test('get/set image properties', async (t) => {
t.not(instance, null); t.not(instance, null);
let slintImage = instance!.getProperty("image"); let slintImage = instance!.getProperty("image");
if (t.true((slintImage instanceof ImageData))) { if (t.true((slintImage instanceof private_api.ImageData))) {
t.deepEqual((slintImage as ImageData).width, 64); t.deepEqual((slintImage as private_api.ImageData).width, 64);
t.deepEqual((slintImage as ImageData).height, 64); t.deepEqual((slintImage as private_api.ImageData).height, 64);
let image = await Jimp.read(path.join(__dirname, "resources/rgb.png")); let image = await Jimp.read(path.join(__dirname, "resources/rgb.png"));
@ -237,6 +237,8 @@ test('get/set brush properties', (t) => {
in-out property <brush> black: #000000; in-out property <brush> black: #000000;
in-out property <brush> trans: transparent; in-out property <brush> trans: transparent;
in-out property <brush> ref: transparent; in-out property <brush> ref: transparent;
in-out property <brush> linear-gradient: @linear-gradient(90deg, #3f87a6 0%, #ebf8e1 50%, #f69d3c 100%);
in-out property <brush> radial-gradient: @radial-gradient(circle, #f00 0%, #0f0 50%, #00f 100%);
} }
`, ""); `, "");
t.not(definition, null); t.not(definition, null);
@ -271,7 +273,7 @@ test('get/set brush properties', (t) => {
t.assert((transparent as Brush).isTransparent); t.assert((transparent as Brush).isTransparent);
} }
let ref = Brush.fromColor(Color.fromRgb(100, 110, 120)); let ref = new Brush({ red: 100, green: 110, blue: 120, alpha: 255 });
instance!.setProperty("ref", ref); instance!.setProperty("ref", ref);
let instance_ref = instance!.getProperty("ref"); let instance_ref = instance!.getProperty("ref");
@ -281,6 +283,21 @@ test('get/set brush properties', (t) => {
t.deepEqual(ref_color.red, 100); t.deepEqual(ref_color.red, 100);
t.deepEqual(ref_color.green, 110); t.deepEqual(ref_color.green, 110);
t.deepEqual(ref_color.blue, 120); t.deepEqual(ref_color.blue, 120);
t.deepEqual(ref_color.alpha, 255);
}
let radialGradient = instance!.getProperty("radial-gradient");
if (t.true((radialGradient instanceof Brush))) {
t.is((radialGradient as Brush).toString(),
"radial-gradient(circle, rgba(255, 0, 0, 255) 0%, rgba(0, 255, 0, 255) 50%, rgba(0, 0, 255, 255) 100%)");
}
let linearGradient = instance!.getProperty("linear-gradient");
if (t.true((linearGradient instanceof Brush))) {
t.is((linearGradient as Brush).toString(),
"linear-gradient(90deg, rgba(63, 135, 166, 255) 0%, rgba(235, 248, 225, 255) 50%, rgba(246, 157, 60, 255) 100%)");
} }
}) })

View file

@ -3,10 +3,10 @@
import test from 'ava'; import test from 'ava';
import { Brush, Color, ArrayModel, Timer } from '../index' import { Brush, ArrayModel, Timer, private_api } from '../index'
test('Color from fromRgb', (t) => { test('Color from fromRgb', (t) => {
let color = Color.fromRgb(100, 110, 120); let color = private_api.SlintColor.fromRgb(100, 110, 120);
t.deepEqual(color.red, 100); t.deepEqual(color.red, 100);
t.deepEqual(color.green, 110); t.deepEqual(color.green, 110);
@ -14,7 +14,7 @@ test('Color from fromRgb', (t) => {
}) })
test('Color from fromArgb', (t) => { test('Color from fromArgb', (t) => {
let color = Color.fromArgb(120, 100, 110, 120); let color = private_api.SlintColor.fromArgb(120, 100, 110, 120);
t.deepEqual(color.red, 100); t.deepEqual(color.red, 100);
t.deepEqual(color.green, 110); t.deepEqual(color.green, 110);
@ -23,7 +23,7 @@ test('Color from fromArgb', (t) => {
}) })
test('Color from fromArgbEncoded', (t) => { test('Color from fromArgbEncoded', (t) => {
let color = Color.fromArgbEncoded(2019847800); let color = private_api.SlintColor.fromArgbEncoded(2019847800);
t.deepEqual(color.red, 100); t.deepEqual(color.red, 100);
t.deepEqual(color.green, 110); t.deepEqual(color.green, 110);
@ -31,7 +31,7 @@ test('Color from fromArgbEncoded', (t) => {
}) })
test('Color brighter', (t) => { test('Color brighter', (t) => {
let color = Color.fromRgb(100, 110, 120).brighter(0.1); let color = private_api.SlintColor.fromRgb(100, 110, 120).brighter(0.1);
t.deepEqual(color.red, 110); t.deepEqual(color.red, 110);
t.deepEqual(color.green, 121); t.deepEqual(color.green, 121);
@ -39,7 +39,7 @@ test('Color brighter', (t) => {
}) })
test('Color darker', (t) => { test('Color darker', (t) => {
let color = Color.fromRgb(100, 110, 120).darker(0.1); let color = private_api.SlintColor.fromRgb(100, 110, 120).darker(0.1);
t.deepEqual(color.red, 91); t.deepEqual(color.red, 91);
t.deepEqual(color.green, 100); t.deepEqual(color.green, 100);
@ -47,11 +47,20 @@ test('Color darker', (t) => {
}) })
test('Brush from Color', (t) => { test('Brush from Color', (t) => {
let brush = Brush.fromColor(Color.fromRgb(100, 110, 120)); let brush = new Brush({ red: 100, green: 110, blue: 120, alpha: 255 });
t.deepEqual(brush.color.red, 100); t.deepEqual(brush.color.red, 100);
t.deepEqual(brush.color.green, 110); t.deepEqual(brush.color.green, 110);
t.deepEqual(brush.color.blue, 120); t.deepEqual(brush.color.blue, 120);
t.throws(() => {
new Brush({ red: -100, green: 110, blue: 120, alpha: 255 })
},
{
code: "GenericFailure",
message: "A channel of Color cannot be negative"
}
);
}) })
test('ArrayModel push', (t) => { test('ArrayModel push', (t) => {

View file

@ -7,7 +7,7 @@ import { private_api, Window } from '../index'
test('Window constructor', (t) => { test('Window constructor', (t) => {
t.throws(() => { t.throws(() => {
new Window() new private_api.Window()
}, },
{ {
code: "GenericFailure", code: "GenericFailure",
@ -30,9 +30,9 @@ test('Window show / hide', (t) => {
t.not(instance, null); t.not(instance, null);
let window = instance!.window(); let window = instance!.window();
t.is(window.is_visible, false); t.is(window.isVisible, false);
window.show(); window.show();
t.is(window.is_visible, true); t.is(window.isVisible, true);
window.hide(); window.hide();
t.is(window.is_visible, false); t.is(window.isVisible, false);
}) })

View file

@ -127,7 +127,7 @@ export component MyComponent inherits Window {
} }
``` ```
The callbacks in JavaScript are exposed as property that has a setHandler function, and that can be called as a function. The callbacks in JavaScript are exposed as property and that can be called as a function.
**`main.js`** **`main.js`**
@ -138,7 +138,7 @@ let ui = slint.loadFile("ui/my-component.slint");
let component = new ui.MyComponent(); let component = new ui.MyComponent();
// connect to a callback // connect to a callback
component.clicked.setHandler(function() { console.log("hello"); }) component.clicked = function() { console.log("hello"); };
// emit a callback // emit a callback
component.clicked(); component.clicked();
``` ```

View file

@ -4,40 +4,78 @@
import * as path from "path"; import * as path from "path";
import * as napi from "./rust-module"; import * as napi from "./rust-module";
export { Diagnostic, DiagnosticLevel, Window, Brush, Color, ImageData, Point, Size, SlintModelNotify } from "./rust-module"; export { Diagnostic, DiagnosticLevel, Brush, Color, SlintModelNotify } from "./rust-module";
/** /**
* ModelPeer is the interface that the run-time implements. An instance is * Represents a two-dimensional point.
* set on dynamic {@link Model} instances and can be used to notify the run-time
* of changes in the structure or data of the model.
*/ */
export interface ModelPeer { export interface Point {
x: number,
y: number
}
/**
* Represents a two-dimensional size.
*/
export interface Size {
width: number,
height: number
}
/**
* This type represents a window towards the windowing system, that's used to render the
* scene of a component. It provides API to control windowing system specific aspects such
* as the position on the screen.
*/
export interface Window {
/** /**
* Call this function from our own model to notify that fields of data * Shows the window on the screen. An additional strong reference on the
* in the specified row have changed. * associated component is maintained while the window is visible.
* @argument row
*/ */
rowDataChanged(row: number): void; show(): void;
/**
* Call this function from your own model to notify that one or multiple /** Hides the window, so that it is not visible anymore. */
* rows were added to the model, starting at the specified row. hide(): void;
* @param row
* @param count
*/
rowAdded(row: number, count: number): void;
/**
* Call this function from your own model to notify that one or multiple
* rows were removed from the model, starting at the specified row.
* @param row
* @param count
*/
rowRemoved(row: number, count: number): void;
/** /**
* Call this function from your own model to notify that the model has been * Returns the visibility state of the window. This function can return false even if you previously called show()
* changed and everything must be reloaded * on it, for example if the user minimized the window.
*/ */
reset(): void; get isVisible(): boolean;
/** Gets or sets the logical position of the window on the screen. */
logicalPosition: Point;
/** Gets or sets the physical position of the window on the screen. */
physicalPosition: Point;
/** Gets or sets the logical size of the window on the screen, */
logicalSize: Size;
/** Gets or sets the physical size of the window on the screen, */
physicalSize: Size;
}
/**
* An image data type that can be displayed by the Image element.
*
* This interface is inspired by the web [ImageData](https://developer.mozilla.org/en-US/docs/Web/API/ImageData) interface.
*/
export interface ImageData {
/**
* Returns the image as buffer.
*/
get data(): Uint8Array;
/**
* Returns the width of the image in pixels.
*/
get width(): number;
/**
* Returns the height of the image in pixels.
*/
get height(): number;
} }
/** /**
@ -53,11 +91,10 @@ export interface ModelPeer {
* ```js * ```js
* export class ArrayModel<T> implements Model<T> { * export class ArrayModel<T> implements Model<T> {
* private a: Array<T> * private a: Array<T>
* notify: ModelPeer;
* *
* constructor(arr: Array<T>) { * constructor(arr: Array<T>) {
* super();
* this.a = arr; * this.a = arr;
* this.notify = new NullPeer();
* } * }
* *
* rowCount() { * rowCount() {
@ -98,34 +135,38 @@ export interface ModelPeer {
*} *}
* ``` * ```
*/ */
export interface Model<T> { export abstract class Model<T> {
/**
* @hidden
*/
notify: NullPeer;
constructor() {
this.notify = new NullPeer();
}
/** /**
* Implementations of this function must return the current number of rows. * Implementations of this function must return the current number of rows.
*/ */
rowCount(): number; abstract rowCount(): number;
/** /**
* Implementations of this function must return the data at the specified row. * Implementations of this function must return the data at the specified row.
* @param row * @param row
*/ */
rowData(row: number): T | undefined; abstract rowData(row: number): T | undefined;
/** /**
* Implementations of this function must store the provided data parameter * Implementations of this function must store the provided data parameter
* in the model at the specified row. * in the model at the specified row.
* @param row * @param row
* @param data * @param data
*/ */
setRowData(row: number, data: T): void; abstract setRowData(row: number, data: T): void;
/**
* This public member is set by the run-time and implementation must use this
* to notify the run-time of changes in the model.
*/
notify: ModelPeer;
} }
/** /**
* @hidden * @hidden
*/ */
class NullPeer implements ModelPeer { class NullPeer {
rowDataChanged(row: number): void { } rowDataChanged(row: number): void { }
rowAdded(row: number, count: number): void { } rowAdded(row: number, count: number): void { }
rowRemoved(row: number, count: number): void { } rowRemoved(row: number, count: number): void { }
@ -136,12 +177,11 @@ class NullPeer implements ModelPeer {
* ArrayModel wraps a JavaScript array for use in `.slint` views. The underlying * ArrayModel wraps a JavaScript array for use in `.slint` views. The underlying
* array can be modified with the [[ArrayModel.push]] and [[ArrayModel.remove]] methods. * array can be modified with the [[ArrayModel.push]] and [[ArrayModel.remove]] methods.
*/ */
export class ArrayModel<T> implements Model<T> { export class ArrayModel<T> extends Model<T> {
/** /**
* @hidden * @hidden
*/ */
private a: Array<T> private a: Array<T>
notify: ModelPeer;
/** /**
* Creates a new ArrayModel. * Creates a new ArrayModel.
@ -149,8 +189,8 @@ export class ArrayModel<T> implements Model<T> {
* @param arr * @param arr
*/ */
constructor(arr: Array<T>) { constructor(arr: Array<T>) {
super();
this.a = arr; this.a = arr;
this.notify = new NullPeer();
} }
rowCount() { rowCount() {
@ -176,9 +216,7 @@ export class ArrayModel<T> implements Model<T> {
// FIXME: should this be named splice and have the splice api? // FIXME: should this be named splice and have the splice api?
/** /**
* Removes the specified number of element from the array that's backing * Removes the specified number of element from the array that's backing
* the model, starting at the specified index. This is equivalent to calling * the model, starting at the specified index.
* Array.slice() on the array and notifying the run-time about the removed
* rows.
* @param index * @param index
* @param size * @param size
*/ */
@ -225,7 +263,7 @@ export interface ComponentHandle {
* Returns the {@link Window} associated with this component instance. * Returns the {@link Window} associated with this component instance.
* The window API can be used to control different aspects of the integration into the windowing system, such as the position on the screen. * The window API can be used to control different aspects of the integration into the windowing system, such as the position on the screen.
*/ */
get window(): napi.Window; get window(): Window;
} }
/** /**
@ -253,7 +291,7 @@ class Component implements ComponentHandle {
this.instance.window().hide(); this.instance.window().hide();
} }
get window(): napi.Window { get window(): Window {
return this.instance.window(); return this.instance.window();
} }
@ -265,13 +303,6 @@ class Component implements ComponentHandle {
} }
} }
/**
* @hidden
*/
interface Callback {
(): any;
setHandler(cb: any): void;
}
/** /**
* Represents an errors that can be emitted by the compiler. * Represents an errors that can be emitted by the compiler.
@ -292,17 +323,65 @@ export class CompileError extends Error {
} }
/** /**
* Loads the given slint file and returns a constructor to create an instance of the exported component. * LoadFileOptions are used to defines different optional parameters that can be used to configure the compiler.
*/ */
export function loadFile(filePath: string): Object { export interface LoadFileOptions {
/**
* If set to true warnings from the compiler will not be printed to the console.
*/
quiet?: boolean,
/**
* Sets the widget style the compiler is currently using when compiling .slint files.
*/
style?: string
/**
* Sets the include paths used for looking up `.slint` imports to the specified vector of paths.
*/
includePaths?: Array<string>,
/**
* Sets library paths used for looking up `@library` imports to the specified map of library names to paths.
*/
libraryPaths?: Record<string, string>
}
/**
* Loads the given slint file and returns an objects that contains a functions to construct the exported
* component of the slint file.
*
* ```js
* import * as slint from "slint-ui";
* let ui = slint.loadFile(".ui/main.slint");
* let main = new ui.Main();
* ```
*/
export function loadFile(filePath: string, options?: LoadFileOptions) : Object {
let compiler = new napi.ComponentCompiler; let compiler = new napi.ComponentCompiler;
if (typeof options !== 'undefined') {
if (typeof options.style !== 'undefined') {
compiler.style = options.style;
}
if (typeof options.includePaths !== 'undefined') {
compiler.includePaths = options.includePaths;
}
if (typeof options.libraryPaths !== 'undefined') {
compiler.libraryPaths = options.libraryPaths;
}
}
let definition = compiler.buildFromPath(filePath); let definition = compiler.buildFromPath(filePath);
let diagnostics = compiler.diagnostics; let diagnostics = compiler.diagnostics;
if (diagnostics.length > 0) { if (diagnostics.length > 0) {
let warnings = diagnostics.filter((d) => d.level == napi.DiagnosticLevel.Warning); let warnings = diagnostics.filter((d) => d.level == napi.DiagnosticLevel.Warning);
warnings.forEach((w) => console.log("Warning: " + w));
if (typeof options !== 'undefined' && options.quiet !== true) {
warnings.forEach((w) => console.warn("Warning: " + w));
}
let errors = diagnostics.filter((d) => d.level == napi.DiagnosticLevel.Error); let errors = diagnostics.filter((d) => d.level == napi.DiagnosticLevel.Error);
@ -343,9 +422,10 @@ export function loadFile(filePath: string): Object {
instance!.definition().callbacks.forEach((cb) => { instance!.definition().callbacks.forEach((cb) => {
Object.defineProperty(componentHandle, cb.replace(/-/g, '_') , { Object.defineProperty(componentHandle, cb.replace(/-/g, '_') , {
get() { get() {
let callback = function () { return instance!.invoke(cb, Array.from(arguments)); } as Callback; return function () { return instance!.invoke(cb, Array.from(arguments)); };
callback.setHandler = function (callback) { instance!.setCallback(cb, callback) }; },
return callback; set(callback) {
instance!.setCallback(cb, callback);
}, },
enumerable: true, enumerable: true,
}) })
@ -375,6 +455,9 @@ export namespace private_api {
export import ComponentDefinition = napi.ComponentDefinition; export import ComponentDefinition = napi.ComponentDefinition;
export import ComponentInstance = napi.ComponentInstance; export import ComponentInstance = napi.ComponentInstance;
export import ValueType = napi.ValueType; export import ValueType = napi.ValueType;
export import Window = napi.Window;
export import ImageData = napi.ImageData;
export import SlintColor = napi.SlintColor;
export function send_mouse_click(component: Component, x: number, y: number) { export function send_mouse_click(component: Component, x: number, y: number) {
component.component_instance.sendMouseClick(x, y); component.component_instance.sendMouseClick(x, y);

View file

@ -71,6 +71,17 @@ impl JsComponentCompiler {
self.internal.set_library_paths(library_paths); self.internal.set_library_paths(library_paths);
} }
#[napi(getter)]
pub fn library_paths(&self) -> HashMap<String, String> {
let mut library_paths = HashMap::new();
for (key, path) in self.internal.library_paths() {
library_paths.insert(key.clone(), path.to_str().unwrap_or_default().to_string());
}
library_paths
}
#[napi(setter)] #[napi(setter)]
pub fn set_style(&mut self, style: String) { pub fn set_style(&mut self, style: String) {
self.internal.set_style(style); self.internal.set_style(style);

View file

@ -35,14 +35,14 @@ pub struct JsDiagnostic {
/// Message for this diagnostic. /// Message for this diagnostic.
pub message: String, pub message: String,
/// The line number in the .slint source file. /// The line number in the .slint source file. The line number starts with 1.
pub line_number: u32, pub line_number: u32,
// The column in the .slint source file // The column in the .slint source file. The column number starts with 1.
pub column: u32, pub column_number: u32,
/// The path of the source file where this diagnostic occurred. /// The path of the source file where this diagnostic occurred.
pub source_file: Option<String>, pub file_name: Option<String>,
} }
impl From<Diagnostic> for JsDiagnostic { impl From<Diagnostic> for JsDiagnostic {
@ -52,8 +52,8 @@ impl From<Diagnostic> for JsDiagnostic {
level: internal_diagnostic.level().into(), level: internal_diagnostic.level().into(),
message: internal_diagnostic.message().into(), message: internal_diagnostic.message().into(),
line_number: line_number as u32, line_number: line_number as u32,
column: column as u32, column_number: column as u32,
source_file: internal_diagnostic file_name: internal_diagnostic
.source_file() .source_file()
.and_then(|path| path.to_str()) .and_then(|path| path.to_str())
.map(|str| str.into()), .map(|str| str.into()),

View file

@ -50,7 +50,7 @@ impl JsWindow {
/// Returns the visibility state of the window. This function can return false even if you previously called show() /// Returns the visibility state of the window. This function can return false even if you previously called show()
/// on it, for example if the user minimized the window. /// on it, for example if the user minimized the window.
#[napi(getter, js_name = "is_visible")] #[napi(getter)]
pub fn is_visible(&self) -> bool { pub fn is_visible(&self) -> bool {
self.inner.window().is_visible() self.inner.window().is_visible()
} }

View file

@ -1,23 +1,39 @@
// Copyright © SixtyFPS GmbH <info@slint.dev> // Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial
use i_slint_core::{Brush, Color}; use i_slint_core::{graphics::GradientStop, Brush, Color};
use napi::bindgen_prelude::External; use napi::{bindgen_prelude::External, Error, Result};
/// Color represents a color in the Slint run-time, represented using 8-bit channels for red, green, blue and the alpha (opacity). /// Color represents a color in the Slint run-time, represented using 8-bit channels for red, green, blue and the alpha (opacity).
#[napi(js_name = Color)] #[napi(object, js_name = "Color")]
pub struct JsColor { pub struct JsColor {
/// Represents the red channel of the color as u8 in the range 0..255.
pub red: f64,
/// Represents the green channel of the color as u8 in the range 0..255.
pub green: f64,
/// Represents the blue channel of the color as u8 in the range 0..255.
pub blue: f64,
/// Represents the alpha channel of the color as u8 in the range 0..255.
pub alpha: f64,
}
/// Color represents a color in the Slint run-time, represented using 8-bit channels for red, green, blue and the alpha (opacity).
#[napi(js_name = SlintColor)]
pub struct JsSlintColor {
inner: Color, inner: Color,
} }
impl From<Color> for JsColor { impl From<Color> for JsSlintColor {
fn from(color: Color) -> Self { fn from(color: Color) -> Self {
Self { inner: color } Self { inner: color }
} }
} }
#[napi] #[napi]
impl JsColor { impl JsSlintColor {
/// Creates a new transparent color. /// Creates a new transparent color.
#[napi(constructor)] #[napi(constructor)]
pub fn new() -> Self { pub fn new() -> Self {
@ -80,8 +96,8 @@ impl JsColor {
/// So for example `brighter(0.2)` will increase the brightness by 20%, and /// So for example `brighter(0.2)` will increase the brightness by 20%, and
/// calling `brighter(-0.5)` will return a color that's 50% darker. /// calling `brighter(-0.5)` will return a color that's 50% darker.
#[napi] #[napi]
pub fn brighter(&self, factor: f64) -> JsColor { pub fn brighter(&self, factor: f64) -> JsSlintColor {
JsColor::from(self.inner.brighter(factor as f32)) JsSlintColor::from(self.inner.brighter(factor as f32))
} }
/// Returns a new version of this color that has the brightness decreased /// Returns a new version of this color that has the brightness decreased
@ -90,29 +106,29 @@ impl JsColor {
/// result is converted back to RGB and the alpha channel is unchanged. /// result is converted back to RGB and the alpha channel is unchanged.
/// So for example `darker(0.3)` will decrease the brightness by 30%. /// So for example `darker(0.3)` will decrease the brightness by 30%.
#[napi] #[napi]
pub fn darker(&self, factor: f64) -> JsColor { pub fn darker(&self, factor: f64) -> JsSlintColor {
JsColor::from(self.inner.darker(factor as f32)) JsSlintColor::from(self.inner.darker(factor as f32))
} }
/// Returns a new version of this color with the opacity decreased by `factor`. /// Returns a new version of this color with the opacity decreased by `factor`.
/// ///
/// The transparency is obtained by multiplying the alpha channel by `(1 - factor)`. /// The transparency is obtained by multiplying the alpha channel by `(1 - factor)`.
#[napi] #[napi]
pub fn transparentize(&self, amount: f64) -> JsColor { pub fn transparentize(&self, amount: f64) -> JsSlintColor {
JsColor::from(self.inner.transparentize(amount as f32)) JsSlintColor::from(self.inner.transparentize(amount as f32))
} }
/// Returns a new color that is a mix of `self` and `other`, with a proportion /// Returns a new color that is a mix of `self` and `other`, with a proportion
/// factor given by `factor` (which will be clamped to be between `0.0` and `1.0`). /// factor given by `factor` (which will be clamped to be between `0.0` and `1.0`).
#[napi] #[napi]
pub fn mix(&self, other: &JsColor, factor: f64) -> JsColor { pub fn mix(&self, other: &JsSlintColor, factor: f64) -> JsSlintColor {
JsColor::from(self.inner.mix(&other.inner, factor as f32)) JsSlintColor::from(self.inner.mix(&other.inner, factor as f32))
} }
/// Returns a new version of this color with the opacity set to `alpha`. /// Returns a new version of this color with the opacity set to `alpha`.
#[napi] #[napi]
pub fn with_alpha(&self, alpha: f64) -> JsColor { pub fn with_alpha(&self, alpha: f64) -> JsSlintColor {
JsColor::from(self.inner.with_alpha(alpha as f32)) JsSlintColor::from(self.inner.with_alpha(alpha as f32))
} }
/// Returns the color as string in hex representation e.g. `#000000` for black. /// Returns the color as string in hex representation e.g. `#000000` for black.
@ -137,30 +153,38 @@ impl From<Brush> for JsBrush {
} }
} }
impl From<JsColor> for JsBrush { impl From<JsSlintColor> for JsBrush {
fn from(color: JsColor) -> Self { fn from(color: JsSlintColor) -> Self {
Self::from(Brush::from(color.inner)) Self::from(Brush::from(color.inner))
} }
} }
#[napi] #[napi]
impl JsBrush { impl JsBrush {
/// Creates a new transparent brush.
#[napi(constructor)] #[napi(constructor)]
pub fn new() -> Self { pub fn new_with_color(color: JsColor) -> Result<Self> {
Self { inner: Brush::default() } if color.red < 0. || color.green < 0. || color.blue < 0. || color.alpha < 0. {
return Err(Error::from_reason("A channel of Color cannot be negative"));
}
Ok(Self {
inner: Brush::SolidColor(Color::from_argb_u8(
color.alpha.floor() as u8,
color.red.floor() as u8,
color.green.floor() as u8,
color.blue.floor() as u8,
)),
})
} }
/// Creates a brush form a `Color`. /// Creates a brush form a `Color`.
#[napi(factory)] pub fn from_slint_color(color: &JsSlintColor) -> Self {
pub fn from_color(color: &JsColor) -> Self {
Self { inner: Brush::SolidColor(color.inner) } Self { inner: Brush::SolidColor(color.inner) }
} }
/// If the brush is SolidColor, the contained color is returned. /// @hidden
/// If the brush is a LinearGradient, the color of the first stop is returned.
#[napi(getter)] #[napi(getter)]
pub fn color(&self) -> JsColor { pub fn color(&self) -> JsSlintColor {
self.inner.color().into() self.inner.color().into()
} }
@ -217,11 +241,52 @@ impl JsBrush {
/// It is only implemented for solid color brushes. /// It is only implemented for solid color brushes.
#[napi] #[napi]
pub fn to_string(&self) -> String { pub fn to_string(&self) -> String {
if let Brush::SolidColor(_) = self.inner { match &self.inner {
return self.color().to_string(); Brush::SolidColor(_) => {
return self.color().to_string();
}
Brush::LinearGradient(gradient) => {
return format!(
"linear-gradient({}deg, {})",
gradient.angle(),
gradient_stops_to_string(gradient.stops())
);
}
Brush::RadialGradient(gradient) => {
return format!(
"radial-gradient(circle, {})",
gradient_stops_to_string(gradient.stops())
);
}
_ => String::default(),
} }
println!("toString() is not yet implemented for gradient brushes.");
String::default()
} }
} }
fn gradient_stops_to_string<'a>(stops: impl Iterator<Item = &'a GradientStop>) -> String {
let stops: Vec<String> = stops
.map(|s| {
format!(
"rgba({}, {}, {}, {}) {}%",
s.color.red(),
s.color.green(),
s.color.blue(),
s.color.alpha(),
s.position * 100.
)
})
.collect();
let mut stops_string = String::default();
let len = stops.len();
for i in 0..len {
stops_string.push_str(stops[i].as_str());
if i < len - 1 {
stops_string.push_str(", ");
}
}
stops_string
}

View file

@ -20,7 +20,7 @@ for (let i = tiles.length - 1; i > 0; i--) {
let model = new slint.ArrayModel(tiles); let model = new slint.ArrayModel(tiles);
mainWindow.memory_tiles = model; mainWindow.memory_tiles = model;
mainWindow.check_if_pair_solved.setHandler(function () { mainWindow.check_if_pair_solved = function () {
let flipped_tiles = []; let flipped_tiles = [];
tiles.forEach((tile, index) => { tiles.forEach((tile, index) => {
if (tile.image_visible && !tile.solved) { if (tile.image_visible && !tile.solved) {
@ -60,7 +60,7 @@ mainWindow.check_if_pair_solved.setHandler(function () {
} }
} }
}); };
mainWindow.run(); mainWindow.run();

View file

@ -18,7 +18,7 @@ for (let i = tiles.length - 1; i > 0; i--) {
let model = new slint.ArrayModel(tiles); let model = new slint.ArrayModel(tiles);
window.memory_tiles = model; window.memory_tiles = model;
window.check_if_pair_solved.setHandler(function () { window.check_if_pair_solved = function () {
let flipped_tiles = []; let flipped_tiles = [];
tiles.forEach((tile, index) => { tiles.forEach((tile, index) => {
if (tile.image_visible && !tile.solved) { if (tile.image_visible && !tile.solved) {
@ -58,6 +58,6 @@ window.check_if_pair_solved.setHandler(function () {
} }
} }
}) };
window.run(); window.run();

View file

@ -12,12 +12,12 @@ window.ink_levels = [
{ color: "#ffff00", level: 0.60 }, { color: "#ffff00", level: 0.60 },
{ color: "#000000", level: 0.90 }]; { color: "#000000", level: 0.90 }];
window.fax_number_erase.setHandler(function () { window.fax_number_erase = function () {
window.fax_number = window.fax_number.substring(0, window.fax_number.length - 1); window.fax_number = window.fax_number.substring(0, window.fax_number.length - 1);
}) };
window.fax_send.setHandler(function () { window.fax_send = function () {
console.log("Send fax to " + window.fax_number); console.log("Send fax to " + window.fax_number);
window.fax_number = ""; window.fax_number = "";
}) };
window.run(); window.run();

View file

@ -43,11 +43,11 @@ let model = new slint.ArrayModel([
]); ]);
app.todo_model = model; app.todo_model = model;
app.todo_added.setHandler(function (text) { app.todo_added = function (text) {
model.push({ title: text, checked: false }) model.push({ title: text, checked: false })
}) };
app.remove_done.setHandler(function () { app.remove_done = function () {
let offset = 0; let offset = 0;
const length = model.length; const length = model.length;
for (let i = 0; i < length; ++i) { for (let i = 0; i < length; ++i) {
@ -56,6 +56,6 @@ app.remove_done.setHandler(function () {
offset++; offset++;
} }
} }
}) };
app.run(); app.run();

View file

@ -84,11 +84,11 @@ try {
assert.equal(instance.callback_emission_count, 0); assert.equal(instance.callback_emission_count, 0);
/// also test setHandler /// also test assigning with a function
instance.test_callback2.setHandler(function(a) { instance.test_callback2 = function(a) {
callback_3_emitted += 100; callback_3_emitted += 100;
callback_3_string_value = a; callback_3_string_value = a;
}); };
instance.test_callback2("salùt") instance.test_callback2("salùt")
assert.equal(callback_3_emitted, 101); assert.equal(callback_3_emitted, 101);
assert.equal(callback_3_string_value, "salùt"); assert.equal(callback_3_string_value, "salùt");

View file

@ -204,11 +204,11 @@ assert!(!instance.window().is_visible());
```js ```js
var instance = new slint.Hello(); var instance = new slint.Hello();
assert(!instance.window.is_visible); assert(!instance.window.isVisible);
instance.window.show(); instance.window.show();
assert(instance.window.is_visible); assert(instance.window.isVisible);
instance.window.hide(); instance.window.hide();
assert(!instance.window.is_visible); assert(!instance.window.isVisible);
``` ```
*/ */