├── .github
└── workflows
│ └── ci.yml
├── .gitignore
├── LICENSE
├── README.md
├── assets
└── solid.png
├── deno.json
├── deno.lock
├── icon
├── icon-1024.png
├── icon-128.png
├── icon-16.png
├── icon-256.png
├── icon-32.png
├── icon-512.png
└── icon-64.png
├── import_map.json
├── jsx
└── index.d.ts
├── native
├── core
│ ├── application.ts
│ ├── dialogs
│ │ ├── color
│ │ │ └── color-dialog.ts
│ │ └── file
│ │ │ └── file-dialog.ts
│ ├── dom
│ │ ├── dom-utils.ts
│ │ ├── environment.ts
│ │ ├── index.d.ts
│ │ └── index.ts
│ ├── index.ts
│ ├── layout
│ │ └── index.ts
│ ├── style
│ │ ├── index.ts
│ │ ├── properties.ts
│ │ └── utils
│ │ │ ├── color.ts
│ │ │ └── parse.ts
│ └── views
│ │ ├── button
│ │ ├── button.ts
│ │ └── native-button.ts
│ │ ├── checkbox
│ │ └── checkbox.ts
│ │ ├── coloropenbutton
│ │ ├── coloropenbutton.ts
│ │ └── native-coloropenbutton.ts
│ │ ├── combobox
│ │ ├── combobox.ts
│ │ └── native-combobox.ts
│ │ ├── common
│ │ └── native-target.ts
│ │ ├── date-picker
│ │ └── date-picker.ts
│ │ ├── decorators
│ │ ├── native.ts
│ │ ├── overrides.ts
│ │ ├── property.ts
│ │ └── view.ts
│ │ ├── fileopenbutton
│ │ ├── fileopenbutton.ts
│ │ └── native-fileopenbutton.ts
│ │ ├── filesavebutton
│ │ ├── filesavebutton.ts
│ │ └── native-filesavebutton.ts
│ │ ├── image
│ │ └── image.ts
│ │ ├── menu
│ │ ├── menu-item.ts
│ │ ├── menu-section-header.ts
│ │ ├── menu-separator.ts
│ │ └── menu.ts
│ │ ├── outline
│ │ └── outline.ts
│ │ ├── popover
│ │ └── popover.ts
│ │ ├── progress
│ │ └── progress.ts
│ │ ├── radiobutton
│ │ └── radiobutton.ts
│ │ ├── scroll-view
│ │ └── scroll-view.ts
│ │ ├── slider
│ │ └── slider.ts
│ │ ├── split-view
│ │ ├── content-list.ts
│ │ ├── sidebar.ts
│ │ ├── split-view-controller.ts
│ │ ├── split-view-item.ts
│ │ └── split-view.ts
│ │ ├── status-bar
│ │ └── status-bar.ts
│ │ ├── switch
│ │ └── switch.ts
│ │ ├── table
│ │ └── table-cell.ts
│ │ ├── text-field
│ │ └── text-field.ts
│ │ ├── text-view
│ │ └── text-view.ts
│ │ ├── text
│ │ ├── text-base.ts
│ │ └── text.ts
│ │ ├── toolbar
│ │ ├── toolbar-flexible-space.ts
│ │ ├── toolbar-group.ts
│ │ ├── toolbar-item.ts
│ │ ├── toolbar-sidebar-tracking-separator.ts
│ │ ├── toolbar-space.ts
│ │ ├── toolbar-toggle-sidebar.ts
│ │ └── toolbar.ts
│ │ ├── view
│ │ ├── native-view.ts
│ │ ├── view-base.ts
│ │ └── view.ts
│ │ ├── webview
│ │ └── webview.ts
│ │ └── window
│ │ ├── native-window.ts
│ │ └── window.ts
└── index.ts
├── package.json
├── scripts
└── bundle-solidjs.ts
├── snippets
├── .bolt
│ └── config.json
├── .gitignore
├── README.md
├── dist
│ ├── assets
│ │ ├── index-IZm_L_TO.js
│ │ └── index-eucFxVK-.css
│ ├── index.html
│ └── vite.svg
├── index.html
├── package-lock.json
├── package.json
├── postcss.config.js
├── public
│ └── vite.svg
├── src
│ ├── App.tsx
│ ├── assets
│ │ └── solid.svg
│ ├── components
│ │ ├── CodeSnippet.tsx
│ │ ├── CodeSnippetLight.tsx
│ │ ├── prisim-atom-dark.css
│ │ └── prisim-one-light.css
│ ├── index.css
│ ├── index.tsx
│ └── vite-env.d.ts
├── tailwind.config.js
├── tsconfig.app.json
├── tsconfig.app.tsbuildinfo
├── tsconfig.json
├── tsconfig.node.json
├── tsconfig.node.tsbuildinfo
└── vite.config.ts
├── solid-native
└── renderer.js
├── src
├── app-menus.tsx
├── app.tsx
├── components
│ ├── Switch.tsx
│ ├── button.tsx
│ ├── checkbox.tsx
│ ├── color-picker.tsx
│ ├── combobox.tsx
│ ├── date-picker.tsx
│ ├── file-dialog.tsx
│ ├── image.tsx
│ ├── modal.tsx
│ ├── popover.tsx
│ ├── progress.tsx
│ ├── radio-button.tsx
│ ├── save-file.tsx
│ ├── slider.tsx
│ ├── text-field.tsx
│ ├── text-view.tsx
│ ├── text.tsx
│ ├── webview.tsx
│ └── window.tsx
├── contentview.tsx
├── examples
│ ├── counter.tsx
│ ├── index.tsx
│ └── todo.tsx
├── hooks
│ └── use-color-scheme.ts
├── pages
│ ├── common.ts
│ ├── components.tsx
│ ├── getting-started.tsx
│ ├── overview.tsx
│ └── setup.tsx
├── sidebar.tsx
├── state.tsx
├── toolbar.tsx
├── utils
│ └── colors.ts
└── webdisplay.tsx
└── vendor
└── undom-ng
├── LICENSE
├── README.md
├── package.json
└── src
├── namespaces.js
├── serializer.js
├── undom-ng.js
├── undom.js
└── utils.js
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches: [main]
6 | pull_request:
7 | branches: [main]
8 |
9 | jobs:
10 | lint:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - name: Setup repo
14 | uses: actions/checkout@v2
15 |
16 | - name: Setup Deno
17 | uses: denoland/setup-deno@main
18 | with:
19 | deno-version: 'v2.x'
20 |
21 | - name: Check Formatting
22 | run: deno fmt --check
23 |
24 | - name: Lint
25 | run: deno lint
26 |
27 | test-build:
28 | runs-on: macos-latest
29 | steps:
30 | - name: Setup repo
31 | uses: actions/checkout@v2
32 |
33 | - name: Setup Deno
34 | uses: denoland/setup-deno@main
35 | with:
36 | deno-version: 'v2.x'
37 |
38 | - name: Build
39 | run: deno task bundle
40 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 | .vscode
4 | .DS_Store
5 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Solid for macOS
6 |
7 | Solid for macOS empowers you to build truly native desktop apps, leveraging
8 | native [AppKit](https://developer.apple.com/documentation/appkit) components for
9 | a seamless and performant user experience. Unlike purely webview based
10 | cross-platform frameworks (_or frameworks that attempt to recreate the entire
11 | platform interface_), Solid for macOS directly integrates with native APIs,
12 | ensuring your apps behave, look and feel right at home on macOS. This
13 | integration allows you to develop fully native apps that utilize _all_ the
14 | nuanced capabilities of the entire platform, providing users with a smooth and
15 | responsive experience.
16 |
17 | The first macOS app built with [Solid](https://www.solidjs.com/) is already
18 | available on the
19 | [Mac App Store here](https://apps.apple.com/us/app/solid-for-macos/id1574916360).
20 | We welcome you to install and give it a try (_compatible with macOS 14+ and
21 | M-series arm64 machines_).
22 |
23 | You can also try out the example app in this repository. To run the example,
24 | clone this repository and run the following commands:
25 |
26 | ```bash
27 | deno task start
28 | ```
29 |
30 | Yes, **you don't need Xcode installed** to run and develop the app for macOS.
31 | You can start the app directly from the terminal, it's as simple as that. Only
32 | when you are ready to release your app would you need Xcode and an Apple
33 | developer account to publish to the store.
34 |
35 | ## Architecture
36 |
37 | Let's explore the architecture of Solid for macOS. There are a lot of components
38 | working together behind the scenes to make Solid for macOS possible.
39 |
40 | ### Runtime
41 |
42 | The runtime is a critical component that connects macOS APIs to JavaScript.
43 | Written in Objective-C++, it leverages
44 | [Node-API](https://nodejs.org/api/n-api.html#node-api) to facilitate seamless
45 | communication with any JavaScript engine that implements engine-agnostic
46 | Node-API layer. This open-source runtime, available at
47 | [macos-node-api](https://github.com/NativeScript/runtime-node-api), handles the
48 | task of bringing all the native APIs seamlessly to JavaScript land.
49 |
50 | ## DOM
51 |
52 | DOM provides the simplest possible implementation of a
53 | [DOM Document](https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model),
54 | just enough to expose the most basic DOM Apis needed by Solid. We are using
55 | [undom-ng](https://github.com/ClassicOldSong/undom-ng) as a simple and
56 | lightweight DOM implementation.
57 |
58 | ## Foundation
59 |
60 | Foundation integrates the concepts of web and native, making the process of
61 | building native macOS apps intuitive and familiar for web developers. By
62 | combining the best ideas from both worlds, it ensures that developers can
63 | leverage their existing web development skills to create high-quality apps from
64 | day one.
65 |
66 | One of foundation's primary responsibilities is to handle various aspects of the
67 | application, including styling, layout, rendering, windowing, menubars, and the
68 | overall application lifecycle. It ensures that all essential elements of a macOS
69 | app are seamlessly managed, providing a cohesive development experience.
70 |
71 | To achieve this, foundation makes each AppKit UI component to be a DOM element
72 | and registers it with the global document object, allowing developers to
73 | interact with native components using familiar web development paradigms.
74 |
75 | For layout management, foundation utilizes the flexbox layout engine provided by
76 | [Meta's Yoga](https://github.com/facebook/yoga), enabling developers to apply
77 | almost all flexbox properties to DOM elements. This approach simplifies the
78 | process of creating complex and responsive layouts.
79 |
80 | The most basic UI element provided by foundation is the view, which internally
81 | creates an [NSView](https://developer.apple.com/documentation/appkit/nsview)
82 | which serves as a building block for more complex components and user
83 | interfaces. Foundation already includes a comprehensive set of basic AppKit
84 | components, and ongoing efforts are being made to expand this library further,
85 | ensuring that developers have access to all AppKit has to offer.
86 |
87 | ## Solid Renderer
88 |
89 | A custom renderer, implemented at [renderer](./solid-native/renderer.js), is
90 | used to transform JSX into our DOM implementation. This renderer seamlessly
91 | integrates Solid's reactive capabilities with native macOS components.
92 |
93 | ## Deployment
94 |
95 | If you have noticed, our
96 | [Solid Desktop app on Mac App Store](https://apps.apple.com/us/app/solid-for-macos/id1574916360)
97 | is just 5.5 MB in size. That is possible with
98 | [Hermes](https://github.com/facebook/hermes), a JavaScript engine developed by
99 | Meta. Credit to [Tzvetan Mikov](https://github.com/tmikov) for his continued
100 | excellence on the engine. Hermes provides optimal performance while having
101 | minimal footprint.
102 |
103 | https://github.com/user-attachments/assets/ad087d8c-f303-485a-bbe3-889430286bd9
104 |
105 | ## Contributors
106 |
107 | - [Ammar Ahmed](https://github.com/ammarahm-ed)
108 | - [Diljit Singh](https://github.com/DjDeveloperr)
109 | - [Nathan Walker](https://github.com/NathanWalker)
110 |
111 | ## License
112 |
113 | Apache-2.0
114 |
--------------------------------------------------------------------------------
/assets/solid.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ammarahm-ed/nativescript-macos-solid/e86e63aee31f05cdcf3dea5b0a742f5ac3df436e/assets/solid.png
--------------------------------------------------------------------------------
/deno.json:
--------------------------------------------------------------------------------
1 | {
2 | "tasks": {
3 | "start": "deno task bundle && deno task run-native",
4 | "bundle": "deno run -A ./scripts/bundle-solidjs.ts \"src/app.tsx\" \"dist/out.js\"",
5 | "run-native": "deno run -A --unstable-sloppy-imports ./native/index.ts"
6 | },
7 | "license": "Apache-2.0",
8 | "importMap": "./import_map.json",
9 | "lint": {
10 | "include": [
11 | "jsx",
12 | "native",
13 | "scripts",
14 | "src"
15 | ],
16 | "rules": {
17 | "exclude": [
18 | "no-explicit-any",
19 | "no-var",
20 | "no-empty-interface",
21 | "ban-types"
22 | ]
23 | }
24 | },
25 | "fmt": {
26 | "include": [
27 | "jsx",
28 | "native",
29 | "scripts",
30 | "src",
31 | "deno.json",
32 | "import_map.json",
33 | "README.md"
34 | ]
35 | },
36 | "compilerOptions": {
37 | "jsxImportSource": "@jsx",
38 | "jsx": "react-jsx",
39 | "experimentalDecorators": true,
40 | "emitDecoratorMetadata": true,
41 | "noImplicitOverride": false
42 | },
43 | "nodeModulesDir": "auto"
44 | }
45 |
--------------------------------------------------------------------------------
/icon/icon-1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ammarahm-ed/nativescript-macos-solid/e86e63aee31f05cdcf3dea5b0a742f5ac3df436e/icon/icon-1024.png
--------------------------------------------------------------------------------
/icon/icon-128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ammarahm-ed/nativescript-macos-solid/e86e63aee31f05cdcf3dea5b0a742f5ac3df436e/icon/icon-128.png
--------------------------------------------------------------------------------
/icon/icon-16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ammarahm-ed/nativescript-macos-solid/e86e63aee31f05cdcf3dea5b0a742f5ac3df436e/icon/icon-16.png
--------------------------------------------------------------------------------
/icon/icon-256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ammarahm-ed/nativescript-macos-solid/e86e63aee31f05cdcf3dea5b0a742f5ac3df436e/icon/icon-256.png
--------------------------------------------------------------------------------
/icon/icon-32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ammarahm-ed/nativescript-macos-solid/e86e63aee31f05cdcf3dea5b0a742f5ac3df436e/icon/icon-32.png
--------------------------------------------------------------------------------
/icon/icon-512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ammarahm-ed/nativescript-macos-solid/e86e63aee31f05cdcf3dea5b0a742f5ac3df436e/icon/icon-512.png
--------------------------------------------------------------------------------
/icon/icon-64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ammarahm-ed/nativescript-macos-solid/e86e63aee31f05cdcf3dea5b0a742f5ac3df436e/icon/icon-64.png
--------------------------------------------------------------------------------
/import_map.json:
--------------------------------------------------------------------------------
1 | {
2 | "imports": {
3 | "@jsx/jsx-runtime": "./jsx/index.d.ts",
4 | "undom-ng": "./vendor/undom-ng/src/undom-ng.js",
5 | "dom": "./native/core/dom/index.ts",
6 | "dom-types": "./native/core/dom/index.d.ts",
7 | "app": "./dist/out.js",
8 | "@nativescript/macos-node-api": "npm:@nativescript/macos-node-api@~0.1.3",
9 | "solid-native-renderer": "./solid-native/renderer.js"
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/native/core/application.ts:
--------------------------------------------------------------------------------
1 | import "@nativescript/macos-node-api";
2 | import type { NativeWindow } from "./views/window/native-window.ts";
3 | import type { Window } from "./views/window/window.ts";
4 | objc.import("AppKit");
5 |
6 | @NativeClass
7 | class AppDelegate extends NSObject implements NSApplicationDelegate {
8 | window?: NativeWindow;
9 | running = true;
10 | isActive = true;
11 | static windowTitle: string;
12 | static ObjCProtocols = [NSApplicationDelegate];
13 | static ObjCExposedMethods = {
14 | showMainWindow: { returns: interop.types.void, params: [interop.types.id] },
15 | themeChanged: { returns: interop.types.void, params: [interop.types.id] },
16 | };
17 |
18 | applicationDidFinishLaunching(_notification: NSNotification) {
19 | NSApp.activateIgnoringOtherApps(false);
20 | NSApp.stop(this);
21 | // Allow users to customize the app's Touch Bar items
22 | NSApplication.sharedApplication
23 | .isAutomaticCustomizeTouchBarMenuItemEnabled = true;
24 | NSDistributedNotificationCenter.defaultCenter.addObserverSelectorNameObject(
25 | this,
26 | "themeChanged",
27 | "AppleInterfaceThemeChangedNotification",
28 | null,
29 | );
30 | RunLoop();
31 | }
32 |
33 | applicationShouldHandleReopenHasVisibleWindows(
34 | sender: NSApplication,
35 | hasVisibleWindows: boolean,
36 | ): boolean {
37 | if (!hasVisibleWindows) {
38 | (sender.windows.firstObject as NSWindow).makeKeyAndOrderFront(sender);
39 | }
40 | return true;
41 | }
42 |
43 | applicationWillTerminate(_notification: NSNotification): void {
44 | this.running = false;
45 | }
46 |
47 | showMainWindow(_id: this) {
48 | NativeScriptApplication.showMainWindow();
49 | }
50 |
51 | themeChanged(_id: this) {
52 | console.log(
53 | "themeChanged",
54 | NSApp.effectiveAppearance.name === "NSAppearanceNameDarkAqua"
55 | ? "dark"
56 | : "light",
57 | );
58 | }
59 | }
60 |
61 | function RunLoop() {
62 | let delay = 2;
63 | let lastEventTime = 0;
64 |
65 | const loop = () => {
66 | const event = NSApp.nextEventMatchingMaskUntilDateInModeDequeue(
67 | NSEventMask.Any,
68 | null,
69 | "kCFRunLoopDefaultMode",
70 | true,
71 | );
72 |
73 | const timeSinceLastEvent = Date.now() - lastEventTime;
74 | if (event != null) {
75 | NSApp.sendEvent(event);
76 | delay = timeSinceLastEvent < 32 ? 2 : 8;
77 | lastEventTime = Date.now();
78 | } else {
79 | delay = timeSinceLastEvent > 6000
80 | ? 128
81 | : timeSinceLastEvent > 4000
82 | ? 64
83 | : timeSinceLastEvent > 2000
84 | ? 16
85 | : 8;
86 | }
87 |
88 | if (NativeScriptApplication.delegate.running) {
89 | setTimeout(loop, NativeScriptApplication.ensure60FPS ? 8 : delay);
90 | }
91 | };
92 | setTimeout(loop, 0);
93 | }
94 |
95 | export class Application {
96 | static delegate: AppDelegate;
97 | static application: NSApplication;
98 | static rootView: HTMLViewElement;
99 | static window: Window;
100 | static appMenu: NSMenu;
101 | static ensure60FPS: boolean;
102 | static initEditMenu: boolean;
103 |
104 | static launch() {
105 | if (!(document.body instanceof HTMLElement)) {
106 | throw new Error("document.body instance of NSView");
107 | }
108 | Application.rootView = document.body as unknown as HTMLViewElement;
109 | Application.rootView?.connectedCallback();
110 |
111 | if (NativeScriptApplication.window) {
112 | NativeScriptApplication.window.open();
113 | } else {
114 | throw new Error("Window is not initialized");
115 | }
116 |
117 | Application.application = NSApplication.sharedApplication;
118 | Application.delegate = AppDelegate.new();
119 | Application.delegate.window = NativeScriptApplication.window.nativeView;
120 | Application.createMenu();
121 | NSApp.delegate = Application.delegate;
122 | NSApp.setActivationPolicy(NSApplicationActivationPolicy.Regular);
123 | NSApp.run();
124 | }
125 |
126 | static createMenu() {
127 | if (!Application.appMenu) {
128 | const menu = NSMenu.new();
129 | NSApp.mainMenu = menu;
130 | Application.appMenu = menu;
131 | }
132 | }
133 | static showMainWindow() {
134 | // override
135 | }
136 | }
137 |
138 | declare global {
139 | var NativeScriptApplication: typeof Application;
140 | }
141 | globalThis.NativeScriptApplication = Application;
142 |
--------------------------------------------------------------------------------
/native/core/dialogs/color/color-dialog.ts:
--------------------------------------------------------------------------------
1 | import "@nativescript/macos-node-api";
2 |
3 | type ChangeCallback = ((color: string) => void | undefined) | undefined;
4 | export interface ColorDialogOptions {
5 | color?: string;
6 | change?: ChangeCallback;
7 | }
8 | let colorDialog: NSColorPanel;
9 | let colorTarget: NativeColorTarget;
10 |
11 | @NativeClass
12 | class NativeColorTarget extends NSObject {
13 | static ObjCExposedMethods = {
14 | changeColor: { returns: interop.types.void, params: [interop.types.id] },
15 | };
16 |
17 | declare _resolve: Function;
18 | declare _changeCallback: ChangeCallback;
19 | static initWithResolve(resolve: Function, changeCallback: ChangeCallback) {
20 | const delegate = NativeColorTarget.new();
21 | delegate._resolve = resolve;
22 | delegate._changeCallback = changeCallback;
23 | return delegate;
24 | }
25 |
26 | changeColor(_id: this) {
27 | if (this._changeCallback) {
28 | this._changeCallback(nsColorToHex(colorDialog.color));
29 | } else if (this._resolve) {
30 | this._resolve(nsColorToHex(colorDialog.color));
31 | }
32 | }
33 | }
34 |
35 | export function openColorDialog(options: ColorDialogOptions) {
36 | return new Promise((resolve) => {
37 | colorDialog = NSColorPanel.new();
38 | colorDialog.setIsVisible(true);
39 | colorDialog.isContinuous = true;
40 | if (options.color) {
41 | colorDialog.color = hexToNSColor(options.color);
42 | }
43 | colorTarget = NativeColorTarget.initWithResolve(resolve, options.change);
44 | colorDialog.setTarget(colorTarget);
45 | colorDialog.setAction("changeColor");
46 | });
47 | }
48 |
49 | export function hexToNSColor(hex: string) {
50 | // Ensure that the hex string is in the format "#RRGGBB" or "RRGGBB"
51 | hex = hex.replace(/^#/, "");
52 |
53 | // Parse the red, green, and blue components
54 | var red = parseInt(hex.substring(0, 2), 16) / 255.0;
55 | var green = parseInt(hex.substring(2, 4), 16) / 255.0;
56 | var blue = parseInt(hex.substring(4, 6), 16) / 255.0;
57 |
58 | // Create and return an NSColor object
59 | return NSColor.colorWithSRGBRedGreenBlueAlpha(red, green, blue, 1.0); // Alpha is set to 1.0 for full opacity
60 | }
61 |
62 | export function nsColorToHex(color: NSColor) {
63 | // Get the color components
64 | var red = color.redComponent;
65 | var green = color.greenComponent;
66 | var blue = color.blueComponent;
67 |
68 | // Convert to 255 scale and then to hex
69 | var r = Math.round(red * 255).toString(16).padStart(2, "0");
70 | var g = Math.round(green * 255).toString(16).padStart(2, "0");
71 | var b = Math.round(blue * 255).toString(16).padStart(2, "0");
72 |
73 | // Return the hex color code
74 | return `#${r}${g}${b}`;
75 | }
76 |
--------------------------------------------------------------------------------
/native/core/dialogs/file/file-dialog.ts:
--------------------------------------------------------------------------------
1 | export interface FileDialogOptions {
2 | chooseFiles?: boolean;
3 | chooseDirectories?: boolean;
4 | multiple?: boolean;
5 | fileTypes?: Array;
6 | directoryUrl?: NSURL;
7 | }
8 | let fileDialog: NSOpenPanel;
9 | export interface SaveFileDialogOptions {
10 | createDirectories?: boolean;
11 | filename?: string;
12 | fileTypes?: Array;
13 | directoryUrl?: NSURL;
14 | }
15 | let saveDialog: NSSavePanel;
16 |
17 | export function openFileDialog(options: FileDialogOptions) {
18 | return new Promise>((resolve, reject) => {
19 | fileDialog = NSOpenPanel.new();
20 | fileDialog.canChooseFiles = options?.chooseFiles || false;
21 | fileDialog.canChooseDirectories = options?.chooseDirectories || false;
22 | fileDialog.allowsMultipleSelection = options?.multiple || false;
23 | fileDialog.allowedFileTypes = options?.fileTypes || ["*"];
24 | if (options?.directoryUrl) {
25 | fileDialog.directoryURL = options.directoryUrl;
26 | } else {
27 | fileDialog.directoryURL = NSURL.fileURLWithPath(
28 | NSSearchPathForDirectoriesInDomains(
29 | NSSearchPathDirectory.Desktop,
30 | NSSearchPathDomainMask.UserDomain,
31 | true,
32 | ).firstObject,
33 | );
34 | }
35 |
36 | const response = fileDialog.runModal();
37 | if (response === NSModalResponseOK) {
38 | const urls: Array = [];
39 | for (let i = 0; i < fileDialog.URLs.count; i++) {
40 | const url = fileDialog.URLs.objectAtIndex(i) as NSURL;
41 | urls.push(url.absoluteString);
42 | }
43 | resolve(urls);
44 | } else {
45 | reject([]);
46 | }
47 | });
48 | }
49 |
50 | export function saveFileDialog(options: SaveFileDialogOptions) {
51 | return new Promise((resolve, reject) => {
52 | saveDialog = NSSavePanel.new();
53 | saveDialog.canCreateDirectories = options?.createDirectories || false;
54 | saveDialog.nameFieldStringValue = options?.filename || "";
55 | saveDialog.allowedFileTypes = options?.fileTypes || ["*"];
56 | if (options?.directoryUrl) {
57 | saveDialog.directoryURL = options.directoryUrl;
58 | } else {
59 | saveDialog.directoryURL = NSURL.fileURLWithPath(
60 | NSSearchPathForDirectoriesInDomains(
61 | NSSearchPathDirectory.Desktop,
62 | NSSearchPathDomainMask.UserDomain,
63 | true,
64 | ).firstObject,
65 | );
66 | }
67 |
68 | const response = saveDialog.runModal();
69 | if (response === NSModalResponseOK) {
70 | resolve(saveDialog.URL.absoluteString);
71 | } else {
72 | reject();
73 | }
74 | });
75 | }
76 |
--------------------------------------------------------------------------------
/native/core/dom/dom-utils.ts:
--------------------------------------------------------------------------------
1 | export { createEvent, getAttributeNS, setAttributeNS } from "undom-ng";
2 | export { Event };
3 | import { Event as UndomEvent } from "undom-ng";
4 |
5 | const Event = UndomEvent as unknown as typeof globalThis.Event;
6 |
--------------------------------------------------------------------------------
/native/core/dom/environment.ts:
--------------------------------------------------------------------------------
1 | import { createEnvironment } from "undom-ng";
2 | const initDocument = (document: any) => {
3 | document.body = document.createElement("view");
4 | };
5 |
6 | const {
7 | scope,
8 | createDocument,
9 | createElement,
10 | registerElement: registerElement,
11 | } = createEnvironment({
12 | silent: true,
13 | initDocument: initDocument,
14 | onSetTextContent(_text: string) {
15 | this.updateTextContent?.();
16 | },
17 | onSetData(_data: any) {
18 | if (this.parentNode) {
19 | let parentElement = this.parentNode;
20 | while (parentElement.nodeType !== 1) {
21 | parentElement = parentElement.parentNode;
22 | }
23 | (parentElement as any).updateTextContent?.();
24 | }
25 | },
26 | } as any) as unknown as {
27 | scope: typeof Scope;
28 | createDocument: (namespace: string, documentElementTag: string) => Document;
29 | createElement: (tagName: string) => Node;
30 | registerElement: (tagName: string, element: Node) => void;
31 | };
32 |
33 | globalThis.registerElement = registerElement;
34 | globalThis.createElement = createElement;
35 | globalThis.Scope = scope;
36 | globalThis.HTMLElement = scope.HTMLElement;
37 | globalThis.Node = scope.Node;
38 | globalThis.EventTarget = scope.EventTarget;
39 | globalThis.Element = scope.Element;
40 | globalThis.Text = scope.Text;
41 |
42 | const window = {
43 | Scope: scope,
44 | registerElement: registerElement,
45 | createElement: createElement,
46 | } as any;
47 |
48 | globalThis.window = window;
49 |
50 | const registerGlobalDocument = (_global = globalThis) => {
51 | const document = createDocument("http://www.w3.org/1999/xhtml", "#view");
52 | window.document = document;
53 | globalThis.document = document;
54 | };
55 |
56 | export {
57 | createDocument,
58 | createElement,
59 | registerElement,
60 | registerGlobalDocument,
61 | scope,
62 | };
63 |
--------------------------------------------------------------------------------
/native/core/dom/index.d.ts:
--------------------------------------------------------------------------------
1 | import { ViewBase } from "../views/view/view-base.ts";
2 | import { View } from "../views/view/view.ts";
3 | import type { Slider } from "../views/slider/slider.ts";
4 | import type { Progress } from "../views/progress/progress.ts";
5 | import type { TextField } from "../views/text-field/text-field.ts";
6 | import type { Checkbox } from "../views/checkbox/checkbox.ts";
7 | import type { Window } from "../views/window/window.ts";
8 | import type { Popover } from "../views/popover/popover.ts";
9 | import type { Switch } from "../views/switch/switch.ts";
10 | import type { DatePicker } from "../views/date-picker/date-picker.ts";
11 | import type { WebView } from "../views/webview/webview.ts";
12 | import type { Menu } from "../views/menu/menu.ts";
13 |
14 | declare global {
15 | // undom
16 | var Scope: {
17 | Node: typeof Node;
18 | Element: typeof Element;
19 | Text: typeof Text;
20 | Comment: typeof Comment;
21 | Document: typeof Document;
22 | DocumentFragment: typeof DocumentFragment;
23 | HTMLElement: typeof HTMLElement;
24 | EventTarget: typeof EventTarget;
25 | CharacterData: typeof CharacterData;
26 | };
27 |
28 | // Defining views as elements is done as following
29 | interface HTMLViewBaseElement extends ViewBase {}
30 | var HTMLViewBaseElement: {
31 | new (): HTMLViewBaseElement;
32 | prototype: HTMLViewBaseElement;
33 | };
34 |
35 | interface HTMLViewElement extends View {}
36 | var HTMLViewElement: {
37 | new (): HTMLViewElement;
38 | prototype: HTMLViewBaseElement;
39 | };
40 |
41 | // Register your view here if needed, this is not required for JSX since JSX Intrinsic elements
42 | // are registered separately in jsx/index.d.ts
43 | interface HTMlButtonElement extends HTMLViewBaseElement {}
44 |
45 | var HTMlButtonElement: {
46 | new (): HTMlButtonElement;
47 | prototype: HTMlButtonElement;
48 | };
49 |
50 | interface HTMLWindowElement extends Window {}
51 |
52 | interface HTMLWebViewElement extends WebView {}
53 |
54 | var HTMLWindowElement: {
55 | new (): HTMLWindowElement;
56 | prototype: HTMLWindowElement;
57 | };
58 |
59 | interface HTMLScrollViewElement extends View {}
60 |
61 | var HTMLScrollViewElement: {
62 | new (): HTMLScrollViewElement;
63 | prototype: HTMLViewBaseElement;
64 | };
65 |
66 | interface HTMLImageElement extends View {}
67 |
68 | var HTMLImageElement: {
69 | new (): HTMLImageElement;
70 | prototype: HTMLImageElement;
71 | };
72 |
73 | interface HTMLTableCellElement extends View {}
74 |
75 | var HTMLTableCellElement: {
76 | new (): HTMLTableCellElement;
77 | prototype: HTMLTableCellElement;
78 | };
79 |
80 | interface HTMLOutlineElement extends View {}
81 |
82 | var HTMLOutlineElement: {
83 | new (): HTMLOutlineElement;
84 | prototype: HTMLOutlineElement;
85 | };
86 |
87 | interface HTMlTextElement extends Text {}
88 |
89 | var HTMlTextElement: {
90 | new (): HTMlTextElement;
91 | prototype: HTMlTextElement;
92 | };
93 |
94 | interface HTMLSliderElement extends Slider {}
95 | var HTMLSliderElement: {
96 | new (): HTMLSliderElement;
97 | prototype: HTMLSliderElement;
98 | };
99 |
100 | interface HTMLProgressElement extends Progress {}
101 | var HTMLProgressElement: {
102 | new (): HTMLProgressElement;
103 | prototype: HTMLProgressElement;
104 | };
105 |
106 | interface HTMLTextFieldElement extends TextField {}
107 | var HTMLTextFieldElement: {
108 | new (): HTMLTextFieldElement;
109 | prototype: HTMLTextFieldElement;
110 | };
111 |
112 | interface HTMLTextViewElement extends TextField {}
113 | var HTMLTextViewElement: {
114 | new (): HTMLTextViewElement;
115 | prototype: HTMLTextViewElement;
116 | };
117 |
118 | interface HTMLCheckboxElement extends Checkbox {}
119 | var HTMLCheckboxElement: {
120 | new (): HTMLCheckboxElement;
121 | prototype: HTMLCheckboxElement;
122 | };
123 |
124 | interface HTMLPopoverElement extends Popover {}
125 | var HTMLPopoverElement: {
126 | new (): HTMLPopoverElement;
127 | prototype: HTMLPopoverElement;
128 | };
129 |
130 | interface HTMLSwitchElement extends Switch {}
131 | var HTMLSwitchElement: {
132 | new (): HTMLSwitchElement;
133 | prototype: HTMLSwitchElement;
134 | };
135 |
136 | interface HTMLDatePickerElement extends DatePicker {}
137 | var HTMLDatePickerElement: {
138 | new (): HTMLDatePickerElement;
139 | prototype: HTMLDatePickerElement;
140 | };
141 |
142 | interface HTMLNSMenuElement extends Menu {}
143 | var HTMLContextMenuElement: {
144 | new (): HTMLNSMenuElement;
145 | prototype: HTMLNSMenuElement;
146 | };
147 |
148 | interface HTMLElementTagNameMap {
149 | view: HTMLViewElement;
150 | }
151 |
152 | function registerElement(tagName: string, element: any): void;
153 | function createElement(tagName: string): Node;
154 |
155 |
156 | interface Node {
157 | connectedCallback?(): void;
158 | disconnectedCallback?(): void;
159 | updateTextContent?(): void;
160 | }
161 | }
162 |
163 | export {};
164 |
--------------------------------------------------------------------------------
/native/core/dom/index.ts:
--------------------------------------------------------------------------------
1 | import { Button } from "../views/button/button.ts";
2 | import { ContentList } from "../views/split-view/content-list.ts";
3 | import { Image } from "../views/image/image.ts";
4 | import { Outline } from "../views/outline/outline.ts";
5 | import { ScrollView } from "../views/scroll-view/scroll-view.ts";
6 | import { SideBar } from "../views/split-view/sidebar.ts";
7 | import { Slider } from "../views/slider/slider.ts";
8 | import SplitView from "../views/split-view/split-view.ts";
9 | import { TableCell } from "../views/table/table-cell.ts";
10 | import { Text } from "../views/text/text.ts";
11 | import { View } from "../views/view/view.ts";
12 | import { Window } from "../views/window/window.ts";
13 | import { WebView } from "../views/webview/webview.ts";
14 | import { registerGlobalDocument } from "./environment.ts";
15 | import { Checkbox } from "../views/checkbox/checkbox.ts";
16 | import { ComboBox } from "../views/combobox/combobox.ts";
17 | import { Progress } from "../views/progress/progress.ts";
18 | import { Toolbar } from "../views/toolbar/toolbar.ts";
19 | import { ToolbarItem } from "../views/toolbar/toolbar-item.ts";
20 | import { ToolbarToggleSidebar } from "../views/toolbar/toolbar-toggle-sidebar.ts";
21 | import { ToolbarSidebarTrackingSeparator } from "../views/toolbar/toolbar-sidebar-tracking-separator.ts";
22 | import { ToolbarFlexibleSpace } from "../views/toolbar/toolbar-flexible-space.ts";
23 | import { ToolbarGroup } from "../views/toolbar/toolbar-group.ts";
24 | import { RadioButton } from "../views/radiobutton/radiobutton.ts";
25 | import { FileOpenButton } from "../views/fileopenbutton/fileopenbutton.ts";
26 | import { ColorOpenButton } from "../views/coloropenbutton/coloropenbutton.ts";
27 | import { FileSaveButton } from "../views/filesavebutton/filesavebutton.ts";
28 | import { TextField } from "../views/text-field/text-field.ts";
29 | import { TextView } from "../views/text-view/text-view.ts";
30 | import { Menu } from "../views/menu/menu.ts";
31 | import { MenuItem } from "../views/menu/menu-item.ts";
32 | import { MenuSeparator } from "../views/menu/menu-separator.ts";
33 | import { MenuSectionHeader } from "../views/menu/menu-section-header.ts";
34 | import { StatusBar } from "../views/status-bar/status-bar.ts";
35 | import { Popover } from "../views/popover/popover.ts";
36 | import { Switch } from "../views/switch/switch.ts";
37 | import { DatePicker } from "../views/date-picker/date-picker.ts";
38 | Button.register();
39 | Checkbox.register();
40 | ColorOpenButton.register();
41 | ComboBox.register();
42 | ContentList.register();
43 | FileOpenButton.register();
44 | FileSaveButton.register();
45 | Image.register();
46 | Outline.register();
47 | Progress.register();
48 | RadioButton.register();
49 | ScrollView.register();
50 | SplitView.register();
51 | SideBar.register();
52 | Slider.register();
53 | TableCell.register();
54 | Text.register();
55 | Toolbar.register();
56 | ToolbarItem.register();
57 | ToolbarToggleSidebar.register();
58 | ToolbarSidebarTrackingSeparator.register();
59 | ToolbarFlexibleSpace.register();
60 | ToolbarGroup.register();
61 | WebView.register();
62 | Window.register();
63 | View.register();
64 | TextField.register();
65 | TextView.register();
66 | Menu.register();
67 | MenuItem.register();
68 | MenuSeparator.register();
69 | MenuSectionHeader.register();
70 | StatusBar.register();
71 | Popover.register();
72 | Switch.register();
73 | DatePicker.register();
74 | registerGlobalDocument();
75 |
--------------------------------------------------------------------------------
/native/core/index.ts:
--------------------------------------------------------------------------------
1 | import "@nativescript/macos-node-api";
2 | import "dom";
3 | import "dom-types";
4 | export { Application } from "./application.ts";
5 |
--------------------------------------------------------------------------------
/native/core/style/utils/parse.ts:
--------------------------------------------------------------------------------
1 | export function parsePercent(percentString: string): number {
2 | if (percentString.endsWith("%")) {
3 | const numericString = percentString.slice(0, -1);
4 | const numericValue = parseFloat(numericString);
5 | if (!isNaN(numericValue)) {
6 | return numericValue;
7 | }
8 | }
9 | throw new Error("Invalid percent string");
10 | }
11 |
--------------------------------------------------------------------------------
/native/core/views/button/button.ts:
--------------------------------------------------------------------------------
1 | import { Layout, type YogaNodeLayout } from "../../layout/index.ts";
2 | import { Color } from "../../style/utils/color.ts";
3 | import type { NativePropertyConfig } from "../decorators/native.ts";
4 | import { native } from "../decorators/native.ts";
5 | import { overrides } from "../decorators/overrides.ts";
6 | import { view } from "../decorators/view.ts";
7 | import { TextBase } from "../text/text-base.ts";
8 | import { NativeButton } from "./native-button.ts";
9 |
10 | const TitleProperty: NativePropertyConfig = {
11 | setNative: (view: Button, _key, value) => {
12 | if (view.nativeView && !view.firstChild) {
13 | view.nativeView.setTitle(value || "");
14 | }
15 | },
16 | shouldLayout: true,
17 | };
18 |
19 | export type ButtonEvents = "click";
20 |
21 | @view({
22 | name: "HTMLButtonElement",
23 | tagName: "button",
24 | })
25 | export class Button extends TextBase {
26 |
27 | declare nativeView?: NativeButton;
28 |
29 | override get isLeaf(): boolean {
30 | return true;
31 | }
32 |
33 | public override initNativeView(): NativeButton {
34 | this.nativeView = NativeButton.initWithOwner(new WeakRef(this));
35 | return this.nativeView;
36 | }
37 |
38 | public override prepareNativeView(nativeView: NativeButton): void {
39 | nativeView.target = this.nativeView;
40 | nativeView.action = "clicked";
41 | }
42 |
43 | override updateTextContent() {
44 | if (this.nativeView && this.firstChild) {
45 | this.nativeView.setTitle(this.textContent || "");
46 | Layout.computeAndLayout(this);
47 | }
48 | }
49 |
50 | @native(TitleProperty)
51 | declare title: string;
52 |
53 | @overrides("color")
54 | setColor(key: string, value: string, config: NativePropertyConfig) {
55 | const nativeValue = config.converter?.toNative?.(key, value) as
56 | | NSColor
57 | | undefined;
58 | if (nativeValue && this.nativeView) {
59 | this.nativeView.setTitleColor(nativeValue);
60 | }
61 | }
62 |
63 | @overrides("backgroundColor")
64 | setBackgroundColor(
65 | _key: string,
66 | value: string,
67 | _config: NativePropertyConfig,
68 | ) {
69 | if (this.nativeView) {
70 | const nativeValue = !value ? undefined : new Color(value).toNSColor();
71 | this.nativeView.bezelColor = nativeValue!;
72 | if (
73 | this.nativeView.bezelStyle === NSBezelStyle.TexturedSquare ||
74 | this.nativeView.bezelStyle === NSBezelStyle.TexturedRounded
75 | ) {
76 | this.nativeView.wantsLayer = true;
77 | this.nativeView.layer.backgroundColor = nativeValue?.CGColor as any;
78 | }
79 | }
80 | }
81 |
82 | @native({
83 | setNative(view: Button, _key, value) {
84 | if (view.nativeView) {
85 | view.nativeView.bezelStyle = value;
86 | }
87 | },
88 | })
89 | declare bezelStyle: number;
90 |
91 | @native({
92 | setNative(view: Button, _key, value) {
93 | if (view.nativeView) {
94 | view.nativeView.setButtonType(value);
95 | }
96 | },
97 | })
98 | declare buttonType: number;
99 |
100 | @native({
101 | setNative(view: Button, _key, value) {
102 | if (view.nativeView) {
103 | let img: NSImage;
104 | if (typeof value === "string" && value?.indexOf("