A browser app takes part in the desktop as a Wayland client does: fullscreen, the desktop's windows, activation and the shell protocol through forge/web, its own files through ltk::vfs, and images its caller names
On a page with forge/web an ltk app had a main surface, overlays and subsurfaces, but none of what a shell such as crustace needs from the compositor: fullscreen did nothing, the app heard nothing of the other windows, an activation token it asked for never came, forge/web was never told when a launched window had shown, the shell could not speak its protocol, and the app had no way to read its own files in a browser. Each now goes through the app's connection to forge/web, in the shape of the Wayland protocol it stands for. Fullscreen. On a page with forge/web, `App::fullscreen` and `App::start_fullscreen` send a `set_fullscreen` request for the main window, as `xdg_toplevel.set_fullscreen` does, instead of doing nothing; alone on a page the canvas still takes the browser's fullscreen. `examples/web_hello.rs` gains a button that toggles it. The desktop's windows. On connecting, the backend asks for the list of toplevels, and forge/web's `toplevel_opened` and `toplevel_closed` events become `ToplevelEvent`s for `App::on_toplevel_event`, as `ext-foreign-toplevel-list-v1` does on Wayland, so a shell's dock knows which applications have windows open. Activation. The tokens an app asks for with `App::take_activation_requests` are issued before it draws: forge/web's, through its `activation_token` request, or empty without a desktop, as on Wayland without xdg-activation, and handed to `App::on_activation_token` with their messages processed. A browser app that launched others through tokens waited for one that never came. Each surface's first presented frame is reported with a `presented` request (`WebSurfaceState::presented`), so forge/web knows when a window it launched has shown and can report it activated, which dismisses crustace's splash. The shell protocol. `platform/web/shell.rs` carries the JSON a shell speaks to forge over its socket over the app's connection to forge/web: `ltk::web::shell_send( json )` sends it as a `shell` request, and each `shell` event's message reaches the listener `ltk::web::set_shell_listener` gives, as JSON. What is sent before the app has connected is kept and sent once it has, so a shell can tell forge/web its theme while it starts. An overlay's role now carries its `App::overlay_namespace`, as its layer-shell namespace does on Wayland, so forge/web can tell a shell's overlays apart. The cursor over every surface. The CSS `cursor` was set on the main surface's element only, so over an overlay the pointer kept the page's arrow. `WebSurfaceState` keeps the element that takes its input, the shape moves to that of the surface the pointer enters, and `WebPlatform::cursor` keeps the last one asked for. An app's own files. `ltk::vfs`, through which ltk reads its themes, fonts and keyboard layouts, is public: `read`, `read_to_string`, `read_dir`, `is_file` and `is_dir` read the file system natively and, in a browser, the files the page loaded into memory. `ltk::web::load_tree( base, root ).await` loads any tree served with a `MANIFEST` under `root`, and `load_resources` is that into `/ltk` with the directory made the resource directory. `ltk::time::Instant` is public too, `std`'s natively and `web_time`'s on wasm32, where `std::time::Instant::now` panics, so an app that builds for both measures time with ltk's clock. wasm-bindgen-cli. `scripts/wasm-bindgen-cli.sh TOOLS_DIR` builds wasm-bindgen-cli into `TOOLS_DIR/bin` at the version of the `wasm-bindgen` crate ltk pins in its `Cargo.toml`, from its sources with a lock resolved for Debian's rustc, which is what `make web-deps` did inline and now runs. Since `make install` copies `scripts/` into the registry, a project that builds for the browser against the installed ltk, such as forge/web or crustace, gets the same CLI without a checkout of ltk. The README and docs/architecture.md say so. Images named by their caller. Every draw through `draw_image_data` fingerprints its pixels to find them in the image caches, which for a terminal means hashing every glyph of every frame. `Canvas::draw_image_keyed( key, rgba, w, h, dest, opacity )` is `draw_image_data` with `key` naming the pixels in place of their fingerprint: every draw with the same key must pass the same pixels, which are read only when the image is not cached. Both backends' caches are keyed by `ImageId`, `Content` for a fingerprint and `Keyed` for a caller's key, so a key never names an image drawn by content even when it equals its fingerprint. `GlesCanvas` and `SoftwareCanvas` gain `draw_image_keyed`, and their `draw_image_data` and it share one private `draw_image`. Tests check that a keyed image draws as its pixels do and that a key equal to a fingerprint is another image. A held button is not also tapped. A button with `on_long_press` and no `on_drag_start` stayed a tap after its hold fired, so releasing it sent its `on_press` as well. `GestureState::long_press_msg_fired` is set when the hold fires `long_press_msg` and suppresses the press message on release; a test covers it. `CHANGELOG.md` describes these changes.
This commit is contained in:
@@ -6,6 +6,14 @@ All notable changes to `ltk` are documented here. The format is based on [Keep a
|
|||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
- **`Canvas::draw_image_keyed( key, rgba, w, h, dest, opacity )`** — `draw_image_data` with `key` naming the pixels in place of a fingerprint of them, so a caller that draws the same images on every frame, such as a terminal's glyphs, does not hash every one of them each time. Every draw with the same `key` must pass the same pixels, which are read only when the image is not cached; a key never names an image drawn by content.
|
||||||
|
- **Fullscreen in a browser's desktop** — on a page with forge/web, `App::fullscreen` and `App::start_fullscreen` ask forge/web for the main window to fill the desktop with a `set_fullscreen` request, as Wayland's `xdg_toplevel.set_fullscreen`, where they did nothing; alone on a page the app's canvas still takes the browser's fullscreen.
|
||||||
|
- **A browser app hears of the desktop's windows** — on a page with forge/web, the web backend asks for the list of toplevels when it connects and turns its `toplevel_opened` and `toplevel_closed` events into `App::on_toplevel_event`, as the Wayland backend does with `ext-foreign-toplevel-list-v1`, so a shell's dock knows which applications have windows open and names each by its `identifier`.
|
||||||
|
- **A browser app tells the desktop it is shown** — on a page with forge/web, each surface's first presented frame is reported with a `presented` request, as a Wayland client's first commit follows `xdg_activation_v1.activate`, so forge/web knows when a window it launched has shown and reports `app_activated` with its token, which dismisses crustace's splash.
|
||||||
|
- **An app's own files in a browser: `ltk::vfs` and `ltk::web::load_tree`** — `vfs`, through which ltk reads its themes, fonts and layouts, is public: `read`, `read_to_string`, `read_dir`, `is_file` and `is_dir` are the file system's natively and, in a browser, read the files the page loaded into memory. `load_tree( base, root ).await` loads any tree served with a `MANIFEST`, as ltk's resources are, under `root`, and `load_resources` is now `load_tree` into `/ltk` and that directory made the resource directory. crustace loads its catalog of applications and their icons this way. In a browser the activation tokens an app asks for with `App::take_activation_requests` are now issued: forge/web's, through its `activation_token` request, or empty without a desktop, as on Wayland without xdg-activation, handed to `App::on_activation_token` and their messages processed before the app draws. Before, a browser app that launched others through tokens waited for one that never came.
|
||||||
|
- **The shell protocol in a browser: `ltk::web::shell_send` and `ltk::web::set_shell_listener`** — a shell such as crustace speaks to forge over a socket in JSON; on a page with forge/web the same JSON travels over the app's connection to it, `shell_send( json )` as a `shell` request and each `shell` event's message to the listener `set_shell_listener` gives, as JSON. What is sent before the app has connected is sent once it has, so a shell can tell forge/web its theme while it starts. An overlay's role now carries its `App::overlay_namespace`, as its layer-shell namespace does on Wayland, so forge/web can tell crustace's launcher and Quick Settings apart.
|
||||||
|
- **`scripts/wasm-bindgen-cli.sh TOOLS_DIR`** — builds wasm-bindgen-cli into `TOOLS_DIR/bin` at the version of the `wasm-bindgen` crate ltk pins in its `Cargo.toml`, from its sources with a lock resolved for Debian's rustc, which is what `make web-deps` did inline. `make web-deps` runs it, and since `make install` copies `scripts/` into the registry, a project that builds for the browser against the installed ltk, such as forge/web or crustace, gets the same CLI without a checkout of ltk.
|
||||||
|
- **`ltk::time::Instant`** — ltk's monotonic clock, `std::time::Instant` natively and `web_time::Instant` on wasm32, where `std::time::Instant::now` panics, so an app that also builds for the browser measures time with the same clock as ltk and as `osk::Panel::press_down`, which takes it.
|
||||||
- **A browser app joins the page's desktop** — on a page where forge/web has published `globalThis.forgeWeb`, the web backend connects to it as the app's `app_id` and asks for its main surface in the role the app's hooks give (`shell_mode`, `layer_anchor`, `layer_size`, `exclusive_zone`, `keyboard_exclusive`, `window_size_hint`), as a Wayland compositor would read them; it draws into the canvas forge/web gives, listens on the input element above it, takes its size and scale from forge/web's `configure`, sends its input region when it changes, and leaves the keyboard focus to the desktop. A `close` reaches `App::on_close_requested`, and on exit the surface is destroyed and the connection closed. The app's overlays become surfaces of the desktop as `App::overlays()` changes, layer surfaces or, for `Layer::Window`, windows, with their sizes and zones in logical pixels and an `update_role` when one changes; forge/web's `close` of one delivers its `on_dismiss`. Popups anchored to a widget and the tooltip wait for a popup role. What every backend does as overlays come and go, resolving an `OverlaySpec::size` and moving a drag and the focuses off an overlay that goes, moved out of the Wayland backend into `event_loop/overlays.rs` (`AppData::overlay_size`, `AppData::remove_overlay`). The app's subsurfaces become surfaces of the desktop too, each sized to its parent and taking no input, painted on one shared GPU canvas only when it appears, its size changes or its `content_version` does, and otherwise moved with an `update_role`, which the browser composes without painting; `App::on_subsurface_committed` follows each paint, and while the app animates by subsurface motion only (`App::subsurface_motion_only`) a parent's frames keep coming without painting it. Painting a view on a canvas with no focus, hover or press, which both backends' subsurfaces do, moved out of the Wayland backend into `draw::paint_detached`. A lost WebGL context now drops the overlays' and subsurfaces' GPU canvases too, not only the main surface's. `examples/web_shell.rs` is a shell for the desktop, a background with a bar in the top layer, a panel that takes the keyboard and a card on a subsurface that slides. A page without a desktop, or one that named a canvas with `set_canvas`, runs the app as before. The new `RunError::Desktop` says forge/web refused the main surface. Every surface now paints through the app's one WebGL2 context, on an `OffscreenCanvas`, and its frame moves to the surface's canvas, a `bitmaprenderer`, as an `ImageBitmap`: a page holds few WebGL contexts, and a desktop's apps have many surfaces.
|
- **A browser app joins the page's desktop** — on a page where forge/web has published `globalThis.forgeWeb`, the web backend connects to it as the app's `app_id` and asks for its main surface in the role the app's hooks give (`shell_mode`, `layer_anchor`, `layer_size`, `exclusive_zone`, `keyboard_exclusive`, `window_size_hint`), as a Wayland compositor would read them; it draws into the canvas forge/web gives, listens on the input element above it, takes its size and scale from forge/web's `configure`, sends its input region when it changes, and leaves the keyboard focus to the desktop. A `close` reaches `App::on_close_requested`, and on exit the surface is destroyed and the connection closed. The app's overlays become surfaces of the desktop as `App::overlays()` changes, layer surfaces or, for `Layer::Window`, windows, with their sizes and zones in logical pixels and an `update_role` when one changes; forge/web's `close` of one delivers its `on_dismiss`. Popups anchored to a widget and the tooltip wait for a popup role. What every backend does as overlays come and go, resolving an `OverlaySpec::size` and moving a drag and the focuses off an overlay that goes, moved out of the Wayland backend into `event_loop/overlays.rs` (`AppData::overlay_size`, `AppData::remove_overlay`). The app's subsurfaces become surfaces of the desktop too, each sized to its parent and taking no input, painted on one shared GPU canvas only when it appears, its size changes or its `content_version` does, and otherwise moved with an `update_role`, which the browser composes without painting; `App::on_subsurface_committed` follows each paint, and while the app animates by subsurface motion only (`App::subsurface_motion_only`) a parent's frames keep coming without painting it. Painting a view on a canvas with no focus, hover or press, which both backends' subsurfaces do, moved out of the Wayland backend into `draw::paint_detached`. A lost WebGL context now drops the overlays' and subsurfaces' GPU canvases too, not only the main surface's. `examples/web_shell.rs` is a shell for the desktop, a background with a bar in the top layer, a panel that takes the keyboard and a card on a subsurface that slides. A page without a desktop, or one that named a canvas with `set_canvas`, runs the app as before. The new `RunError::Desktop` says forge/web refused the main surface. Every surface now paints through the app's one WebGL2 context, on an `OffscreenCanvas`, and its frame moves to the surface's canvas, a `bitmaprenderer`, as an `ImageBitmap`: a page holds few WebGL contexts, and a desktop's apps have many surfaces.
|
||||||
- **Browser backend, the `web` feature** — `ltk::run` on a page's canvas, built for `wasm32-unknown-unknown` with wasm-bindgen, js-sys and web-sys, optional dependencies on that target only. `build.rs` refuses it with another backend or for another target, so a web build uses `default-features = false, features = [ "web" ]`. A page's main thread must not block, so `run` sets the app up and returns, and `setTimeout` and `requestAnimationFrame` drive the loop; `Sources` (`event_loop/sources.rs`) gains a queue of timers and channels for it, and page events and `ChannelSender::send` only queue and schedule an iteration. The app draws on a canvas laid over the whole page, or the one `ltk::web::set_canvas` gives, through WebGL2 with the GLES canvas unchanged, or without it in software through `putImageData`; its logical size is the canvas's CSS size, its scale the page's `devicePixelRatio`, fractional as it may be, so the canvas holds the display's own pixels. Panics go to the browser's console. When the app exits, the canvas receives an `ltk-exit` event. The new `RunError::NoDocument` is returned where there is no page, as in a worker. The built-in keyboard serves text where the page is used by touch. Input comes from the canvas's Pointer Events, a mouse as the pointer and fingers and pens as touches, at `offsetX` / `offsetY` in CSS pixels; a press captures the pointer and focuses the canvas. A wheel in lines scrolls in detents, one in pixels continuously, and the page neither scrolls nor zooms under the canvas. Keys take their keysym from `KeyboardEvent.key`, their code from `.code` as the Linux input code Wayland reports, and repeat on the browser's clock; AltGr types its character rather than counting as Ctrl and Alt. ltk takes every key the browser gives a page, and while fullscreen also those `navigator.keyboard.lock()` gives where there is one. The cursor's shape is the canvas's CSS `cursor`, and on macOS Command counts as Ctrl. Copies go to `navigator.clipboard.writeText`; a page reads the clipboard only from a `paste` event, so Ctrl+V is let through to the browser and held until its `paste` has put the text in `AppData::clipboard`, and a paste from the browser's menu comes as a Ctrl+V of its own. A lost WebGL context drops the GPU canvases and draws nothing until it is restored, on a new `glow::Context`, so the glyph atlas and the caches of the old one are not reused. On ltk's own canvas the page's `env( safe-area-inset-* )`, read from a hidden element, become the main surface's insets. Resources are the tree an Android package carries, which `scripts/web-assets.sh` builds through `android-assets.sh` and `make web-assets` builds into `target/web/ltk/` with the in-tree default theme and the fallback fonts `WEB_FONTS` names; `ltk::web::load_resources( base ).await` fetches its `MANIFEST` and every file it lists, with the manifest's hash in their URLs so a changed tree is not served from the browser's cache, into memory under `/ltk`, which becomes the resource directory. The per-script fallback fonts are now read through `resources::read_font`, so a path that does not exist is found by file name in the resource directory's `fonts/`, which is where a page's are. `wasm-bindgen-futures` joins the `web` feature. `examples/web_hello.rs` is the smoke test: `make web-check` typechecks the backend, `make web-example` builds the example and runs wasm-bindgen into `target/web/`, `make web-serve` serves it, and `make web-deps` installs Debian's std for wasm32 and wasm-bindgen-cli at the version of `Cargo.lock`. The text-scale watcher does not run in a browser, and the built-in keyboard's language is the page's. Not yet: overlays, secondary surfaces and subsurfaces, dead keys and input methods, the accessibility tree, the primary selection, and a canvas that changes size while the window does not.
|
- **Browser backend, the `web` feature** — `ltk::run` on a page's canvas, built for `wasm32-unknown-unknown` with wasm-bindgen, js-sys and web-sys, optional dependencies on that target only. `build.rs` refuses it with another backend or for another target, so a web build uses `default-features = false, features = [ "web" ]`. A page's main thread must not block, so `run` sets the app up and returns, and `setTimeout` and `requestAnimationFrame` drive the loop; `Sources` (`event_loop/sources.rs`) gains a queue of timers and channels for it, and page events and `ChannelSender::send` only queue and schedule an iteration. The app draws on a canvas laid over the whole page, or the one `ltk::web::set_canvas` gives, through WebGL2 with the GLES canvas unchanged, or without it in software through `putImageData`; its logical size is the canvas's CSS size, its scale the page's `devicePixelRatio`, fractional as it may be, so the canvas holds the display's own pixels. Panics go to the browser's console. When the app exits, the canvas receives an `ltk-exit` event. The new `RunError::NoDocument` is returned where there is no page, as in a worker. The built-in keyboard serves text where the page is used by touch. Input comes from the canvas's Pointer Events, a mouse as the pointer and fingers and pens as touches, at `offsetX` / `offsetY` in CSS pixels; a press captures the pointer and focuses the canvas. A wheel in lines scrolls in detents, one in pixels continuously, and the page neither scrolls nor zooms under the canvas. Keys take their keysym from `KeyboardEvent.key`, their code from `.code` as the Linux input code Wayland reports, and repeat on the browser's clock; AltGr types its character rather than counting as Ctrl and Alt. ltk takes every key the browser gives a page, and while fullscreen also those `navigator.keyboard.lock()` gives where there is one. The cursor's shape is the canvas's CSS `cursor`, and on macOS Command counts as Ctrl. Copies go to `navigator.clipboard.writeText`; a page reads the clipboard only from a `paste` event, so Ctrl+V is let through to the browser and held until its `paste` has put the text in `AppData::clipboard`, and a paste from the browser's menu comes as a Ctrl+V of its own. A lost WebGL context drops the GPU canvases and draws nothing until it is restored, on a new `glow::Context`, so the glyph atlas and the caches of the old one are not reused. On ltk's own canvas the page's `env( safe-area-inset-* )`, read from a hidden element, become the main surface's insets. Resources are the tree an Android package carries, which `scripts/web-assets.sh` builds through `android-assets.sh` and `make web-assets` builds into `target/web/ltk/` with the in-tree default theme and the fallback fonts `WEB_FONTS` names; `ltk::web::load_resources( base ).await` fetches its `MANIFEST` and every file it lists, with the manifest's hash in their URLs so a changed tree is not served from the browser's cache, into memory under `/ltk`, which becomes the resource directory. The per-script fallback fonts are now read through `resources::read_font`, so a path that does not exist is found by file name in the resource directory's `fonts/`, which is where a page's are. `wasm-bindgen-futures` joins the `web` feature. `examples/web_hello.rs` is the smoke test: `make web-check` typechecks the backend, `make web-example` builds the example and runs wasm-bindgen into `target/web/`, `make web-serve` serves it, and `make web-deps` installs Debian's std for wasm32 and wasm-bindgen-cli at the version of `Cargo.lock`. The text-scale watcher does not run in a browser, and the built-in keyboard's language is the page's. Not yet: overlays, secondary surfaces and subsurfaces, dead keys and input methods, the accessibility tree, the primary selection, and a canvas that changes size while the window does not.
|
||||||
- **The built-in keyboard serves an app's own text input** — while `App::external_text_input` asks for text and no ltk text field has the focus, the built-in keyboard shows with `App::external_text_purpose` (a terminal gets its row of Escape, Tab, Ctrl, Alt and arrows), comes back when the serial changes, and hides when the request ends. Its keys reach the app as physical ones, pressed and released through `App::on_raw_key_event` with the panel's Ctrl and Alt held for them. Before, the built-in keyboard only served ltk's text fields, so such an app had no keyboard where the compositor offers no input method.
|
- **The built-in keyboard serves an app's own text input** — while `App::external_text_input` asks for text and no ltk text field has the focus, the built-in keyboard shows with `App::external_text_purpose` (a terminal gets its row of Escape, Tab, Ctrl, Alt and arrows), comes back when the serial changes, and hides when the request ends. Its keys reach the app as physical ones, pressed and released through `App::on_raw_key_event` with the panel's Ctrl and Alt held for them. Before, the built-in keyboard only served ltk's text fields, so such an app had no keyboard where the compositor offers no input method.
|
||||||
@@ -79,6 +87,9 @@ All notable changes to `ltk` are documented here. The format is based on [Keep a
|
|||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
- **A held button is not also tapped.** A button with `on_long_press` and no `on_drag_start` stayed a tap after its hold fired, so releasing it sent its `on_press` as well; it no longer does.
|
||||||
|
- **Drawing many small images no longer slows down as the image caches fill.** Every hit in the GLES texture cache, the GLES surface cache and the software canvas's premultiplied cache looked its key up in a linear LRU order, so an app that draws thousands of small images, a terminal's glyphs in each of their colours, walked thousands of entries for each one on every frame. The caches now find, insert and evict an entry in O(log n), with the same budgets and eviction order.
|
||||||
|
- **The cursor's shape over a browser app's overlays** — the web backend set the CSS `cursor` on the main surface's element only, so over an overlay, such as crustace's dock or a panel, the pointer kept the page's arrow. Each surface now keeps the element that takes its input, the cursor's shape moves to that of the surface the pointer enters, and the last shape asked for is kept for it.
|
||||||
- **A focus request reaches a widget scrolled out of view.** Scroll viewports drop the widgets outside their visible area from the hit-test list, so `App::take_focus_request` for a text input that a tap had just added below the fold (a password field under the selected network of a Wi-Fi list) never found it — and the runtime kept re-requesting a rebuild and a redraw every frame while it waited. Widgets with a `WidgetId` that a scroll clips are now recorded with their content position (`scroll_clipped`); a request whose target is among them moves that viewport with the same rule keyboard navigation uses (`scroll::offset_into_view`) and is satisfied on the next frame. The wait is bounded to a few drawn frames, after which the request is dropped instead of spinning.
|
- **A focus request reaches a widget scrolled out of view.** Scroll viewports drop the widgets outside their visible area from the hit-test list, so `App::take_focus_request` for a text input that a tap had just added below the fold (a password field under the selected network of a Wi-Fi list) never found it — and the runtime kept re-requesting a rebuild and a redraw every frame while it waited. Widgets with a `WidgetId` that a scroll clips are now recorded with their content position (`scroll_clipped`); a request whose target is among them moves that viewport with the same rule keyboard navigation uses (`scroll::offset_into_view`) and is satisfied on the next frame. The wait is bounded to a few drawn frames, after which the request is dropped instead of spinning.
|
||||||
- **Fast two-thumb typing no longer drops taps.** A finger landing while another was still down went to the auxiliary slot and never reached the gesture machine, so on a touch keypad every overlapping tap was lost. A finger landing on a primary that is still a plain press (`GestureState::is_plain_press`: no drag, scroll, swipe or slider in flight) now commits that press as a tap at its last position and takes the primary slot; the earlier finger's release reports through `App::on_touch_up`. Presses that have become a drag keep the old behaviour, so a second finger during a pattern stroke or a slider drag still stays out of the gesture machine.
|
- **Fast two-thumb typing no longer drops taps.** A finger landing while another was still down went to the auxiliary slot and never reached the gesture machine, so on a touch keypad every overlapping tap was lost. A finger landing on a primary that is still a plain press (`GestureState::is_plain_press`: no drag, scroll, swipe or slider in flight) now commits that press as a tap at its last position and takes the primary slot; the earlier finger's release reports through `App::on_touch_up`. Presses that have become a drag keep the old behaviour, so a second finger during a pattern stroke or a slider drag still stays out of the gesture machine.
|
||||||
- **`PatternGrid::on_node` fires once per node, not once per motion event.** Every touch motion within a node's snap radius re-emitted the node's message and requested a repaint, so drawing a pattern rebuilt and re-rastered the surface at the touchscreen's motion rate. The gesture machine now remembers the node the finger is over (`last_pattern_node`) and emits only on change; a `Slider` move outcome without a message no longer requests a redraw.
|
- **`PatternGrid::on_node` fires once per node, not once per motion event.** Every touch motion within a node's snap radius re-emitted the node's message and requested a repaint, so drawing a pattern rebuilt and re-rastered the surface at the touchscreen's motion rate. The gesture machine now remembers the node the finger is over (`last_pattern_node`) and emits only on change; a `Slider` move outcome without a message no longer requests a redraw.
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ ANDROID_APK ?= target/android/ltk-hello.apk
|
|||||||
|
|
||||||
# The browser build uses Debian's std for wasm32 (libstd-rust-dev-wasm32) and
|
# The browser build uses Debian's std for wasm32 (libstd-rust-dev-wasm32) and
|
||||||
# wasm-bindgen-cli, which Debian does not package and whose version must be
|
# wasm-bindgen-cli, which Debian does not package and whose version must be
|
||||||
# the one of the wasm-bindgen crate in Cargo.lock.
|
# the one of the wasm-bindgen crate ltk pins.
|
||||||
WEB_TARGET = wasm32-unknown-unknown
|
WEB_TARGET = wasm32-unknown-unknown
|
||||||
WEB_CARGO = --target $(WEB_TARGET) --no-default-features --features web
|
WEB_CARGO = --target $(WEB_TARGET) --no-default-features --features web
|
||||||
WEB_OUT ?= target/web
|
WEB_OUT ?= target/web
|
||||||
@@ -33,7 +33,6 @@ WEB_EXAMPLES = web_hello web_shell
|
|||||||
# Fallback fonts a page bundles beyond the theme's, for text Sora does not
|
# Fallback fonts a page bundles beyond the theme's, for text Sora does not
|
||||||
# cover: Cyrillic, Greek and extended Latin, symbols, arrows and box drawing.
|
# cover: Cyrillic, Greek and extended Latin, symbols, arrows and box drawing.
|
||||||
WEB_FONTS ?= $(wildcard /usr/share/fonts/truetype/noto/NotoSans-Regular.ttf /usr/share/fonts/truetype/dejavu/DejaVuSans.ttf /usr/share/fonts/truetype/noto/NotoSansSymbols2-Regular.ttf)
|
WEB_FONTS ?= $(wildcard /usr/share/fonts/truetype/noto/NotoSans-Regular.ttf /usr/share/fonts/truetype/dejavu/DejaVuSans.ttf /usr/share/fonts/truetype/noto/NotoSansSymbols2-Regular.ttf)
|
||||||
WASM_BINDGEN_SRC = target/wasm-bindgen-cli
|
|
||||||
# Where web-deps installs wasm-bindgen-cli, so nothing needs to be on PATH;
|
# Where web-deps installs wasm-bindgen-cli, so nothing needs to be on PATH;
|
||||||
# one already on PATH is used when there is none here.
|
# one already on PATH is used when there is none here.
|
||||||
WEB_TOOLS = target/tools
|
WEB_TOOLS = target/tools
|
||||||
@@ -97,20 +96,10 @@ android-apk: android-assets
|
|||||||
android-install: android-apk
|
android-install: android-apk
|
||||||
adb install -r $(ANDROID_APK)
|
adb install -r $(ANDROID_APK)
|
||||||
|
|
||||||
# What the web-* targets need: Debian's std for wasm32, and wasm-bindgen-cli
|
# What the web-* targets need: Debian's std for wasm32, and wasm-bindgen-cli at the version ltk pins, which scripts/wasm-bindgen-cli.sh builds.
|
||||||
# from crates.io at the version Cargo.lock holds. The lock it was published
|
|
||||||
# with needs a newer rustc, and `cargo install` ignores rust-version when it
|
|
||||||
# resolves, so its sources get a lock resolved for the rust-version it
|
|
||||||
# declares and are installed from there.
|
|
||||||
web-deps:
|
web-deps:
|
||||||
sudo apt-get install -y libstd-rust-dev-wasm32 curl
|
sudo apt-get install -y libstd-rust-dev-wasm32 curl
|
||||||
cargo metadata --format-version 1 >/dev/null
|
./scripts/wasm-bindgen-cli.sh $(WEB_TOOLS)
|
||||||
version="$$(sed -n '/^name = "wasm-bindgen"$$/{n;s/^version = "\(.*\)"$$/\1/p}' Cargo.lock)" && \
|
|
||||||
rm -rf $(WASM_BINDGEN_SRC) && mkdir -p $(WASM_BINDGEN_SRC) && \
|
|
||||||
curl -sSfL "https://static.crates.io/crates/wasm-bindgen-cli/wasm-bindgen-cli-$$version.crate" | tar -xz -C $(WASM_BINDGEN_SRC) --strip-components=1 && \
|
|
||||||
cd $(WASM_BINDGEN_SRC) && \
|
|
||||||
CARGO_RESOLVER_INCOMPATIBLE_RUST_VERSIONS=fallback cargo generate-lockfile && \
|
|
||||||
cargo install --path . --locked --root $(CURDIR)/$(WEB_TOOLS)
|
|
||||||
|
|
||||||
# Typecheck the browser backend.
|
# Typecheck the browser backend.
|
||||||
web-check:
|
web-check:
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ to keep.
|
|||||||
`apksigner`, `android-framework-res`, `librsvg2-bin`, `default-jdk-headless`,
|
`apksigner`, `android-framework-res`, `librsvg2-bin`, `default-jdk-headless`,
|
||||||
`art-standalone` (for `dx`), `google-android-platform-34-installer` (non-free) and
|
`art-standalone` (for `dx`), `google-android-platform-34-installer` (non-free) and
|
||||||
`android-sdk-platform-tools`.
|
`android-sdk-platform-tools`.
|
||||||
`make web-deps` installs what the `web-*` targets need: `libstd-rust-dev-wasm32` and `curl` from Debian, and `wasm-bindgen-cli` at the version of the `wasm-bindgen` crate in `Cargo.lock`, built from its sources into `target/tools`, where the Makefile finds it.
|
`make web-deps` installs what the `web-*` targets need: `libstd-rust-dev-wasm32` and `curl` from Debian, and `wasm-bindgen-cli` at the version of the `wasm-bindgen` crate ltk pins, which `scripts/wasm-bindgen-cli.sh` builds from its sources into `target/tools`, where the Makefile finds it. The script is installed with the package, so a project that builds against the installed ltk runs it to get the same CLI into a directory of its own.
|
||||||
- A usable system font (`fonts-sora`, `fonts-liberation`, `fonts-dejavu`,
|
- A usable system font (`fonts-sora`, `fonts-liberation`, `fonts-dejavu`,
|
||||||
…). If none is installed `ltk` falls back to an embedded Sora Regular
|
…). If none is installed `ltk` falls back to an embedded Sora Regular
|
||||||
build with a stderr warning.
|
build with a stderr warning.
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -11,14 +11,16 @@ enum Msg
|
|||||||
Press,
|
Press,
|
||||||
Edit( String ),
|
Edit( String ),
|
||||||
Submit,
|
Submit,
|
||||||
|
Fullscreen,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[ derive( Default ) ]
|
#[ derive( Default ) ]
|
||||||
struct Hello
|
struct Hello
|
||||||
{
|
{
|
||||||
presses: u32,
|
presses: u32,
|
||||||
typed: String,
|
typed: String,
|
||||||
submitted: String,
|
submitted: String,
|
||||||
|
fullscreen: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl App for Hello
|
impl App for Hello
|
||||||
@@ -28,6 +30,7 @@ impl App for Hello
|
|||||||
fn app_id( &self ) -> &str { "net.liberux.ltk.Hello" }
|
fn app_id( &self ) -> &str { "net.liberux.ltk.Hello" }
|
||||||
fn save_state( &self ) -> Option<Vec<u8>> { None }
|
fn save_state( &self ) -> Option<Vec<u8>> { None }
|
||||||
fn restore_state( &mut self, _state: Vec<u8> ) {}
|
fn restore_state( &mut self, _state: Vec<u8> ) {}
|
||||||
|
fn fullscreen( &self ) -> Option<bool> { Some( self.fullscreen ) }
|
||||||
|
|
||||||
fn view( &self ) -> Element<Msg>
|
fn view( &self ) -> Element<Msg>
|
||||||
{
|
{
|
||||||
@@ -40,6 +43,7 @@ impl App for Hello
|
|||||||
.push( button::<Msg>( "Press" ).variant( ButtonVariant::Secondary ).on_press( Msg::Press ) )
|
.push( button::<Msg>( "Press" ).variant( ButtonVariant::Secondary ).on_press( Msg::Press ) )
|
||||||
.push( text_edit::<Msg>( "Type and press Enter", &self.typed ).fixed_width( 320.0 ).on_change( Msg::Edit ).on_submit( Msg::Submit ) )
|
.push( text_edit::<Msg>( "Type and press Enter", &self.typed ).fixed_width( 320.0 ).on_change( Msg::Edit ).on_submit( Msg::Submit ) )
|
||||||
.push( text( format!( "Submitted: {}", self.submitted ) ).size( 18.0 ).color( palette.text_secondary ) )
|
.push( text( format!( "Submitted: {}", self.submitted ) ).size( 18.0 ).color( palette.text_secondary ) )
|
||||||
|
.push( button::<Msg>( if self.fullscreen { "Leave fullscreen" } else { "Fullscreen" } ).variant( ButtonVariant::Secondary ).on_press( Msg::Fullscreen ) )
|
||||||
.push( spacer() )
|
.push( spacer() )
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
@@ -51,6 +55,7 @@ impl App for Hello
|
|||||||
Msg::Press => self.presses += 1,
|
Msg::Press => self.presses += 1,
|
||||||
Msg::Edit( value ) => self.typed = value,
|
Msg::Edit( value ) => self.typed = value,
|
||||||
Msg::Submit => self.submitted = std::mem::take( &mut self.typed ),
|
Msg::Submit => self.submitted = std::mem::take( &mut self.typed ),
|
||||||
|
Msg::Fullscreen => self.fullscreen = !self.fullscreen,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Executable
+28
@@ -0,0 +1,28 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Build wasm-bindgen-cli into TOOLS_DIR/bin at the version of the wasm-bindgen crate ltk pins in its Cargo.toml, which is the version a module built with ltk needs. Debian does not package the CLI, and the lock it is published with needs a rustc newer than Debian's, so it is built from its sources with a lock resolved for the rust-version it declares. It reads the Cargo.toml next to this script's directory, so the copy installed with the package works from any project.
|
||||||
|
#
|
||||||
|
# Usage: scripts/wasm-bindgen-cli.sh TOOLS_DIR
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
if [ $# -ne 1 ]
|
||||||
|
then
|
||||||
|
echo "usage: $0 TOOLS_DIR" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
mkdir -p "$1"
|
||||||
|
tools=$( cd "$1" && pwd )
|
||||||
|
|
||||||
|
manifest=$( dirname "$0" )/../Cargo.toml
|
||||||
|
version=$( sed -n 's/^wasm-bindgen *= *{ *version *= *"=\([^"]*\)".*/\1/p' "$manifest" )
|
||||||
|
if [ -z "$version" ]
|
||||||
|
then
|
||||||
|
echo "$0: no pinned wasm-bindgen in $manifest" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
src=$( mktemp -d )
|
||||||
|
trap 'rm -rf "$src"' EXIT
|
||||||
|
curl -sSfL "https://static.crates.io/crates/wasm-bindgen-cli/wasm-bindgen-cli-$version.crate" | tar -xz -C "$src" --strip-components=1
|
||||||
|
cd "$src"
|
||||||
|
CARGO_RESOLVER_INCOMPATIBLE_RUST_VERSIONS=fallback cargo generate-lockfile
|
||||||
|
cargo install --path . --locked --root "$tools"
|
||||||
@@ -49,6 +49,7 @@ impl<A: App, P: Platform> AppData<A, P>
|
|||||||
// text menu likewise does not switch to drag, so a
|
// text menu likewise does not switch to drag, so a
|
||||||
// subsequent motion can still extend a selection.
|
// subsequent motion can still extend a selection.
|
||||||
if ds_msg.is_some() { ss.gesture.long_press_fired = true; }
|
if ds_msg.is_some() { ss.gesture.long_press_fired = true; }
|
||||||
|
if lp_msg.is_some() { ss.gesture.long_press_msg_fired = true; }
|
||||||
ss.request_redraw();
|
ss.request_redraw();
|
||||||
Some( ( lp_msg, ds_msg, origin, text_idx ) )
|
Some( ( lp_msg, ds_msg, origin, text_idx ) )
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -14,7 +14,8 @@
|
|||||||
//! *different* buffer on the next frame, a pointer-keyed cache would
|
//! *different* buffer on the next frame, a pointer-keyed cache would
|
||||||
//! serve the stale texture for the new content. Content-keying makes
|
//! serve the stale texture for the new content. Content-keying makes
|
||||||
//! that impossible: identical bytes → identical key, regardless of
|
//! that impossible: identical bytes → identical key, regardless of
|
||||||
//! where they live in memory.
|
//! where they live in memory. `draw_image_keyed` takes the caller's key
|
||||||
|
//! in place of the fingerprint, in a key space of its own.
|
||||||
//!
|
//!
|
||||||
//! The cache is bounded to [`IMAGE_CACHE_MAX_BYTES`] of estimated GPU
|
//! The cache is bounded to [`IMAGE_CACHE_MAX_BYTES`] of estimated GPU
|
||||||
//! memory (RGBA8: `w × h × 4` per texture) with least-recently-drawn
|
//! memory (RGBA8: `w × h × 4` per texture) with least-recently-drawn
|
||||||
@@ -30,7 +31,7 @@
|
|||||||
|
|
||||||
use glow::HasContext;
|
use glow::HasContext;
|
||||||
|
|
||||||
use crate::render::helpers::fingerprint_rgba;
|
use crate::render::helpers::{ fingerprint_rgba, ImageId };
|
||||||
use crate::render::lru::Lru;
|
use crate::render::lru::Lru;
|
||||||
use crate::types::Rect;
|
use crate::types::Rect;
|
||||||
|
|
||||||
@@ -43,7 +44,7 @@ use super::GlesCanvas;
|
|||||||
/// the compositor.
|
/// the compositor.
|
||||||
pub const IMAGE_CACHE_MAX_BYTES: usize = 32 * 1024 * 1024;
|
pub const IMAGE_CACHE_MAX_BYTES: usize = 32 * 1024 * 1024;
|
||||||
|
|
||||||
type ImageKey = ( u32, u32, u64 );
|
type ImageKey = ( u32, u32, ImageId );
|
||||||
|
|
||||||
/// Content-keyed texture cache shared by every canvas on one GL context.
|
/// Content-keyed texture cache shared by every canvas on one GL context.
|
||||||
pub( super ) struct ImageCache
|
pub( super ) struct ImageCache
|
||||||
@@ -133,6 +134,16 @@ impl GlesCanvas
|
|||||||
/// the internal `upload_rgba_texture` helper enforces, raised one
|
/// the internal `upload_rgba_texture` helper enforces, raised one
|
||||||
/// level so the cache key is never seeded with a bogus mapping.
|
/// level so the cache key is never seeded with a bogus mapping.
|
||||||
pub fn draw_image_data( &mut self, rgba_data: &[u8], img_w: u32, img_h: u32, dest: Rect, opacity: f32 )
|
pub fn draw_image_data( &mut self, rgba_data: &[u8], img_w: u32, img_h: u32, dest: Rect, opacity: f32 )
|
||||||
|
{
|
||||||
|
self.draw_image( ImageId::Content( fingerprint_rgba( rgba_data ) ), rgba_data, img_w, img_h, dest, opacity );
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn draw_image_keyed( &mut self, key: u64, rgba_data: &[u8], img_w: u32, img_h: u32, dest: Rect, opacity: f32 )
|
||||||
|
{
|
||||||
|
self.draw_image( ImageId::Keyed( key ), rgba_data, img_w, img_h, dest, opacity );
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_image( &mut self, id: ImageId, rgba_data: &[u8], img_w: u32, img_h: u32, dest: Rect, opacity: f32 )
|
||||||
{
|
{
|
||||||
if !crate::render::helpers::validate_rgba_dims( "GlesCanvas", rgba_data, img_w, img_h )
|
if !crate::render::helpers::validate_rgba_dims( "GlesCanvas", rgba_data, img_w, img_h )
|
||||||
{
|
{
|
||||||
@@ -140,12 +151,10 @@ impl GlesCanvas
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.activate_target();
|
self.activate_target();
|
||||||
// Content-fingerprint key — see the module doc for the
|
// The (w, h) prefix keeps a pathological pair of buffers with
|
||||||
// rationale. The (w, h) prefix means a pathological pair of
|
// identical bytes but different declared sizes distinct (cannot
|
||||||
// buffers with identical bytes but different declared sizes
|
// happen for valid input, defence in depth).
|
||||||
// stays distinct (cannot happen for valid input, defence in
|
let cache_key = ( img_w, img_h, id );
|
||||||
// depth).
|
|
||||||
let cache_key = ( img_w, img_h, fingerprint_rgba( rgba_data ) );
|
|
||||||
|
|
||||||
let tex = self.image_cache.borrow_mut().image( &self.gl, cache_key, ||
|
let tex = self.image_cache.borrow_mut().image( &self.gl, cache_key, ||
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -130,6 +130,8 @@ pub struct GestureState<Msg: Clone>
|
|||||||
/// `app.on_drop`. Set by the touch deadline when `drag_start_msg`
|
/// `app.on_drop`. Set by the touch deadline when `drag_start_msg`
|
||||||
/// is present, or by the mouse-motion promotion path.
|
/// is present, or by the mouse-motion promotion path.
|
||||||
pub long_press_fired: bool,
|
pub long_press_fired: bool,
|
||||||
|
/// `true` once the hold has fired `long_press_msg`, so the release is not also a tap.
|
||||||
|
pub long_press_msg_fired: bool,
|
||||||
/// Accumulated downward overscroll (physical px) while a vertical
|
/// Accumulated downward overscroll (physical px) while a vertical
|
||||||
/// scroll is pinned at its top — built up by [`Self::on_move`] when a
|
/// scroll is pinned at its top — built up by [`Self::on_move`] when a
|
||||||
/// pull-down can no longer scroll content, unwound by upward drag, and
|
/// pull-down can no longer scroll content, unwound by upward drag, and
|
||||||
@@ -181,6 +183,7 @@ impl<Msg: Clone> GestureState<Msg>
|
|||||||
drag_start_msg: None,
|
drag_start_msg: None,
|
||||||
long_press_text_idx: None,
|
long_press_text_idx: None,
|
||||||
long_press_fired: false,
|
long_press_fired: false,
|
||||||
|
long_press_msg_fired: false,
|
||||||
overscroll_y: 0.0,
|
overscroll_y: 0.0,
|
||||||
mouse_press: false,
|
mouse_press: false,
|
||||||
last_pattern_node: None,
|
last_pattern_node: None,
|
||||||
@@ -243,8 +246,9 @@ impl<Msg: Clone> GestureState<Msg>
|
|||||||
// pointer Press handler flips `mouse_press` back to true
|
// pointer Press handler flips `mouse_press` back to true
|
||||||
// right after this call when the press came from a mouse;
|
// right after this call when the press came from a mouse;
|
||||||
// touch leaves it `false`.
|
// touch leaves it `false`.
|
||||||
self.long_press_fired = false;
|
self.long_press_fired = false;
|
||||||
self.mouse_press = false;
|
self.long_press_msg_fired = false;
|
||||||
|
self.mouse_press = false;
|
||||||
// Built-in long-press for text inputs: arms the same timer
|
// Built-in long-press for text inputs: arms the same timer
|
||||||
// even when the widget has no user-set `on_long_press`, so
|
// even when the widget has no user-set `on_long_press`, so
|
||||||
// the runtime can show the Copy / Cut / Paste menu after the
|
// the runtime can show the Copy / Cut / Paste menu after the
|
||||||
@@ -522,6 +526,7 @@ impl<Msg: Clone> GestureState<Msg>
|
|||||||
let pressed = self.pressed_idx.take();
|
let pressed = self.pressed_idx.take();
|
||||||
let released_slider = self.dragging_slider;
|
let released_slider = self.dragging_slider;
|
||||||
let long_press_fired = self.long_press_fired;
|
let long_press_fired = self.long_press_fired;
|
||||||
|
let long_press_msg_fired = self.long_press_msg_fired;
|
||||||
let horizontal_drag_started = self.horizontal_drag_started;
|
let horizontal_drag_started = self.horizontal_drag_started;
|
||||||
let vertical_drag_started = self.vertical_drag_started;
|
let vertical_drag_started = self.vertical_drag_started;
|
||||||
|
|
||||||
@@ -535,6 +540,7 @@ impl<Msg: Clone> GestureState<Msg>
|
|||||||
self.drag_start_msg = None;
|
self.drag_start_msg = None;
|
||||||
self.long_press_text_idx = None;
|
self.long_press_text_idx = None;
|
||||||
self.long_press_fired = false;
|
self.long_press_fired = false;
|
||||||
|
self.long_press_msg_fired = false;
|
||||||
self.mouse_press = false;
|
self.mouse_press = false;
|
||||||
self.horizontal_drag_started = false;
|
self.horizontal_drag_started = false;
|
||||||
self.vertical_drag_started = false;
|
self.vertical_drag_started = false;
|
||||||
@@ -677,7 +683,7 @@ impl<Msg: Clone> GestureState<Msg>
|
|||||||
// keep firing through the calloop repeat timer
|
// keep firing through the calloop repeat timer
|
||||||
// while held) — suppressing the release-time fire
|
// while held) — suppressing the release-time fire
|
||||||
// avoids a double tap on a quick click.
|
// avoids a double tap on a quick click.
|
||||||
if !w.handlers.is_repeating()
|
if !w.handlers.is_repeating() && !long_press_msg_fired
|
||||||
{
|
{
|
||||||
if let Some( msg ) = w.handlers.press_msg()
|
if let Some( msg ) = w.handlers.press_msg()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -551,6 +551,17 @@ fn release_after_long_press_fired_emits_drop()
|
|||||||
assert!( matches!( events[ 0 ], ReleaseEvent::Drop { .. } ) );
|
assert!( matches!( events[ 0 ], ReleaseEvent::Drop { .. } ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[ test ]
|
||||||
|
fn release_after_long_press_msg_fired_is_not_a_tap()
|
||||||
|
{
|
||||||
|
let widgets = vec![ button( 1, rect( 0.0, 0.0, 100.0, 100.0 ), Some( Msg::Pressed ), Some( Msg::LongPressed ) ) ];
|
||||||
|
let mut g = GestureState::<Msg>::new();
|
||||||
|
let _ = g.on_press( pt( 50.0, 50.0 ), &widgets, &[] );
|
||||||
|
g.long_press_msg_fired = true;
|
||||||
|
let events = g.on_release( pt( 50.0, 50.0 ), &widgets, &cfg_full( 800, 1200 ), false );
|
||||||
|
assert!( events.iter().all( |e| !matches!( e, ReleaseEvent::PushMsg( _ ) ) ) );
|
||||||
|
}
|
||||||
|
|
||||||
#[ test ]
|
#[ test ]
|
||||||
fn release_with_global_drag_emits_drop()
|
fn release_with_global_drag_emits_drop()
|
||||||
{
|
{
|
||||||
|
|||||||
+3
-3
@@ -319,8 +319,8 @@ pub( crate ) mod protocol;
|
|||||||
pub( crate ) mod session_state;
|
pub( crate ) mod session_state;
|
||||||
pub( crate ) mod secure_mem;
|
pub( crate ) mod secure_mem;
|
||||||
pub( crate ) mod resources;
|
pub( crate ) mod resources;
|
||||||
pub( crate ) mod time;
|
pub mod time;
|
||||||
pub( crate ) mod vfs;
|
pub mod vfs;
|
||||||
pub mod gles_render;
|
pub mod gles_render;
|
||||||
#[ cfg( not( target_arch = "wasm32" ) ) ]
|
#[ cfg( not( target_arch = "wasm32" ) ) ]
|
||||||
pub mod egl_context;
|
pub mod egl_context;
|
||||||
@@ -432,7 +432,7 @@ pub use platform::android::{ keep_running, set_android_app, AndroidApp };
|
|||||||
#[ cfg( feature = "web" ) ]
|
#[ cfg( feature = "web" ) ]
|
||||||
pub mod web
|
pub mod web
|
||||||
{
|
{
|
||||||
pub use crate::platform::web::{ load_resources, set_canvas };
|
pub use crate::platform::web::{ load_resources, load_tree, set_canvas, set_shell_listener, shell_send };
|
||||||
}
|
}
|
||||||
pub use resources::{ resource_dir, set_resource_dir };
|
pub use resources::{ resource_dir, set_resource_dir };
|
||||||
pub use xkeysym::Keysym;
|
pub use xkeysym::Keysym;
|
||||||
|
|||||||
@@ -18,6 +18,14 @@ const ROOT: &str = "/ltk";
|
|||||||
///
|
///
|
||||||
/// The files are fetched at once, each with the manifest's hash in its URL, so a changed tree is never served from the browser's cache; the manifest itself always comes from the server.
|
/// The files are fetched at once, each with the manifest's hash in its URL, so a changed tree is never served from the browser's cache; the manifest itself always comes from the server.
|
||||||
pub async fn load_resources( base: &str ) -> Result<(), String>
|
pub async fn load_resources( base: &str ) -> Result<(), String>
|
||||||
|
{
|
||||||
|
load_tree( base, ROOT ).await?;
|
||||||
|
crate::resources::set_resource_dir( ROOT );
|
||||||
|
Ok( () )
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fetch the tree served at `base`, listed by its `MANIFEST` as ltk's resources are, into memory under `root`, an absolute path such as `"/apps"`, where [`crate::vfs`] reads it. An app loads its own files this way, as crustace does its catalog of applications.
|
||||||
|
pub async fn load_tree( base: &str, root: &str ) -> Result<(), String>
|
||||||
{
|
{
|
||||||
let window = web_sys::window().ok_or( "no window" )?;
|
let window = web_sys::window().ok_or( "no window" )?;
|
||||||
let base = base.trim_end_matches( '/' );
|
let base = base.trim_end_matches( '/' );
|
||||||
@@ -38,9 +46,8 @@ pub async fn load_resources( base: &str ) -> Result<(), String>
|
|||||||
for ( path, request ) in pending
|
for ( path, request ) in pending
|
||||||
{
|
{
|
||||||
let bytes = body( request ).await.map_err( |e| format!( "{path}: {e}" ) )?;
|
let bytes = body( request ).await.map_err( |e| format!( "{path}: {e}" ) )?;
|
||||||
crate::vfs::insert( Path::new( ROOT ).join( path ), bytes );
|
crate::vfs::insert( Path::new( root ).join( path ), bytes );
|
||||||
}
|
}
|
||||||
crate::resources::set_resource_dir( ROOT );
|
|
||||||
Ok( () )
|
Ok( () )
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,9 +8,11 @@ use wasm_bindgen::closure::Closure;
|
|||||||
use wasm_bindgen::{ JsCast, JsValue };
|
use wasm_bindgen::{ JsCast, JsValue };
|
||||||
use web_sys::{ HtmlCanvasElement, HtmlElement };
|
use web_sys::{ HtmlCanvasElement, HtmlElement };
|
||||||
|
|
||||||
use crate::app::{ App, Layer, ShellMode };
|
use crate::app::{ App, Layer, ShellMode, ToplevelEvent };
|
||||||
|
use crate::event_loop::AppData;
|
||||||
|
|
||||||
use super::run::{ push, PageEvent };
|
use super::run::{ push, PageEvent };
|
||||||
|
use super::shell;
|
||||||
|
|
||||||
/// The id the main surface goes by.
|
/// The id the main surface goes by.
|
||||||
pub( super ) const MAIN: u32 = 0;
|
pub( super ) const MAIN: u32 = 0;
|
||||||
@@ -32,7 +34,13 @@ impl Connection
|
|||||||
let on_event = Closure::<dyn FnMut( String )>::new( on_event );
|
let on_event = Closure::<dyn FnMut( String )>::new( on_event );
|
||||||
let connection = connect.call2( &api, &JsValue::from_str( app_id ), on_event.as_ref() ).ok()?;
|
let connection = connect.call2( &api, &JsValue::from_str( app_id ), on_event.as_ref() ).ok()?;
|
||||||
let method = |name: &str| js_sys::Reflect::get( &connection, &name.into() ).ok().and_then( |f| f.dyn_into::<js_sys::Function>().ok() );
|
let method = |name: &str| js_sys::Reflect::get( &connection, &name.into() ).ok().and_then( |f| f.dyn_into::<js_sys::Function>().ok() );
|
||||||
Some( Self { request: method( "request" )?, disconnect: method( "disconnect" )?, _on_event: on_event } )
|
let this = Self { request: method( "request" )?, disconnect: method( "disconnect" )?, _on_event: on_event };
|
||||||
|
shell::connected( Some( this.request.clone() ) );
|
||||||
|
if let Err( e ) = this.request( &json!( { "type": "list_toplevels" } ) )
|
||||||
|
{
|
||||||
|
web_sys::console::warn_1( &format!( "[ltk] list_toplevels: {e}" ).into() );
|
||||||
|
}
|
||||||
|
Some( this )
|
||||||
}
|
}
|
||||||
|
|
||||||
pub( super ) fn request( &self, message: &Value ) -> Result<JsValue, String>
|
pub( super ) fn request( &self, message: &Value ) -> Result<JsValue, String>
|
||||||
@@ -51,8 +59,18 @@ impl Connection
|
|||||||
Ok( ( canvas, input ) )
|
Ok( ( canvas, input ) )
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Ask for `surface`, a window, to fill the desktop or to go back.
|
||||||
|
pub( super ) fn set_fullscreen( &self, surface: u32, on: bool )
|
||||||
|
{
|
||||||
|
if let Err( e ) = self.request( &json!( { "type": "set_fullscreen", "surface": surface, "on": on } ) )
|
||||||
|
{
|
||||||
|
web_sys::console::warn_1( &format!( "[ltk] set_fullscreen: {e}" ).into() );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub( super ) fn close( &self )
|
pub( super ) fn close( &self )
|
||||||
{
|
{
|
||||||
|
shell::connected( None );
|
||||||
let _ = self.disconnect.call0( &JsValue::NULL );
|
let _ = self.disconnect.call0( &JsValue::NULL );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -71,10 +89,35 @@ fn on_event( json: String )
|
|||||||
scale: event[ "scale" ].as_f64().unwrap_or( 1.0 ),
|
scale: event[ "scale" ].as_f64().unwrap_or( 1.0 ),
|
||||||
} ),
|
} ),
|
||||||
Some( "close" ) => push( PageEvent::Close { surface } ),
|
Some( "close" ) => push( PageEvent::Close { surface } ),
|
||||||
|
Some( "shell" ) => shell::deliver( &event[ "message" ] ),
|
||||||
|
Some( "toplevel_opened" ) => push( PageEvent::Toplevel( ToplevelEvent::Opened
|
||||||
|
{
|
||||||
|
id: event[ "id" ].as_u64().unwrap_or( 0 ) as u32,
|
||||||
|
app_id: event[ "app_id" ].as_str().unwrap_or_default().to_string(),
|
||||||
|
identifier: event[ "identifier" ].as_str().unwrap_or_default().to_string(),
|
||||||
|
title: event[ "title" ].as_str().unwrap_or_default().to_string(),
|
||||||
|
} ) ),
|
||||||
|
Some( "toplevel_closed" ) => push( PageEvent::Toplevel( ToplevelEvent::Closed { id: event[ "id" ].as_u64().unwrap_or( 0 ) as u32 } ) ),
|
||||||
_ => web_sys::console::warn_1( &format!( "[ltk] forge/web: {json}" ).into() ),
|
_ => web_sys::console::warn_1( &format!( "[ltk] forge/web: {json}" ).into() ),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Issue the activation tokens the app asked for and hand them to it before it draws, as xdg-activation does on Wayland: forge/web's, or empty without a desktop, so the app still goes ahead.
|
||||||
|
pub( super ) fn issue_activation_tokens<A: App>( data: &mut AppData<A> )
|
||||||
|
{
|
||||||
|
let tags = data.app.take_activation_requests();
|
||||||
|
if tags.is_empty() { return; }
|
||||||
|
for tag in tags
|
||||||
|
{
|
||||||
|
let token = data.platform.desktop.as_ref()
|
||||||
|
.and_then( |d| d.request( &json!( { "type": "activation_token" } ) ).ok() )
|
||||||
|
.and_then( |token| token.as_string() )
|
||||||
|
.unwrap_or_default();
|
||||||
|
if let Some( msg ) = data.app.on_activation_token( tag, token ) { data.pending_msgs.push( msg ); }
|
||||||
|
}
|
||||||
|
data.process_messages();
|
||||||
|
}
|
||||||
|
|
||||||
pub( super ) fn layer_name( layer: Layer ) -> &'static str
|
pub( super ) fn layer_name( layer: Layer ) -> &'static str
|
||||||
{
|
{
|
||||||
match layer
|
match layer
|
||||||
|
|||||||
@@ -73,7 +73,11 @@ impl<A: App> AppData<A>
|
|||||||
{
|
{
|
||||||
match input
|
match input
|
||||||
{
|
{
|
||||||
WebInput::Pointer( pointer ) => self.pointer_input( &pointer ),
|
WebInput::Pointer( pointer ) =>
|
||||||
|
{
|
||||||
|
if pointer.kind == PointerKind::Enter { self.point_cursor_at( pointer.focus ); }
|
||||||
|
self.pointer_input( &pointer );
|
||||||
|
}
|
||||||
WebInput::Touch( touch ) => self.touch_input( &touch ),
|
WebInput::Touch( touch ) => self.touch_input( &touch ),
|
||||||
WebInput::TouchCancel => { self.reset_touch_state(); }
|
WebInput::TouchCancel => { self.reset_touch_state(); }
|
||||||
WebInput::Key { key, action, ctrl, shift, alt } =>
|
WebInput::Key { key, action, ctrl, shift, alt } =>
|
||||||
@@ -101,6 +105,19 @@ impl<A: App> AppData<A>
|
|||||||
WebInput::Clipboard( text ) => self.clipboard = text,
|
WebInput::Clipboard( text ) => self.clipboard = text,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Show the cursor's shape on the surface `focus` names, which the pointer entered, as the cursor follows the pointer on Wayland.
|
||||||
|
fn point_cursor_at( &mut self, focus: SurfaceFocus )
|
||||||
|
{
|
||||||
|
let input = match focus
|
||||||
|
{
|
||||||
|
SurfaceFocus::Main => Some( &self.main.platform.input ),
|
||||||
|
SurfaceFocus::Overlay( id ) => self.overlays.get( &id ).map( |ss| &ss.platform.input ),
|
||||||
|
};
|
||||||
|
let Some( input ) = input.cloned() else { return };
|
||||||
|
let _ = input.style().set_property( "cursor", self.platform.cursor );
|
||||||
|
self.platform.cursor_target = input;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Listen on `target`, which takes the input of the surface `focus` names, for every input the runtime takes. A press focuses `target` when `take_focus`; in a desktop the desktop gives the keyboard.
|
/// Listen on `target`, which takes the input of the surface `focus` names, for every input the runtime takes. A press focuses `target` when `take_focus`; in a desktop the desktop gives the keyboard.
|
||||||
|
|||||||
+12
-4
@@ -7,6 +7,7 @@
|
|||||||
//! | --- | --- |
|
//! | --- | --- |
|
||||||
//! | `assets.rs` | The themes, fonts and keyboard layouts a page serves, fetched into memory as the app's resource directory |
|
//! | `assets.rs` | The themes, fonts and keyboard layouts a page serves, fetched into memory as the app's resource directory |
|
||||||
//! | `desktop.rs` | The connection to forge/web and the main surface's role |
|
//! | `desktop.rs` | The connection to forge/web and the main surface's role |
|
||||||
|
//! | `shell.rs` | The shell protocol over that connection, for a shell such as crustace: [`shell_send`] and [`set_shell_listener`] |
|
||||||
//! | `overlays.rs` | The app's overlays as surfaces of the desktop, kept in step with `App::overlays()` |
|
//! | `overlays.rs` | The app's overlays as surfaces of the desktop, kept in step with `App::overlays()` |
|
||||||
//! | `subsurfaces.rs` | The app's subsurfaces as surfaces of the desktop, painted when their content changes and otherwise only moved |
|
//! | `subsurfaces.rs` | The app's subsurfaces as surfaces of the desktop, painted when their content changes and otherwise only moved |
|
||||||
//! | `log.rs` | Panics into the browser's console |
|
//! | `log.rs` | Panics into the browser's console |
|
||||||
@@ -30,12 +31,14 @@ mod log;
|
|||||||
mod overlays;
|
mod overlays;
|
||||||
mod present;
|
mod present;
|
||||||
pub( crate ) mod run;
|
pub( crate ) mod run;
|
||||||
|
mod shell;
|
||||||
mod state;
|
mod state;
|
||||||
mod subsurfaces;
|
mod subsurfaces;
|
||||||
mod surface;
|
mod surface;
|
||||||
|
|
||||||
pub use assets::load_resources;
|
pub use assets::{ load_resources, load_tree };
|
||||||
pub use run::set_canvas;
|
pub use run::set_canvas;
|
||||||
|
pub use shell::{ set_shell_listener, shell_send };
|
||||||
pub( crate ) use state::WebPlatform;
|
pub( crate ) use state::WebPlatform;
|
||||||
pub( crate ) use surface::WebSurfaceState;
|
pub( crate ) use surface::WebSurfaceState;
|
||||||
|
|
||||||
@@ -69,10 +72,14 @@ impl<A: App> Platform for WebPlatform<A>
|
|||||||
|
|
||||||
fn minimize_window( &self, _main: &WebSurfaceState ) {}
|
fn minimize_window( &self, _main: &WebSurfaceState ) {}
|
||||||
|
|
||||||
/// In a desktop a window's fullscreen is forge/web's to give, which it does not yet.
|
/// In a desktop a window's fullscreen is forge/web's to give; alone, the page's canvas takes the browser's.
|
||||||
fn set_fullscreen( &self, main: &WebSurfaceState, fullscreen: bool ) -> bool
|
fn set_fullscreen( &self, main: &WebSurfaceState, fullscreen: bool ) -> bool
|
||||||
{
|
{
|
||||||
if self.desktop.is_some() { return true; }
|
if let Some( desktop ) = self.desktop.as_ref()
|
||||||
|
{
|
||||||
|
desktop.set_fullscreen( main.id, fullscreen );
|
||||||
|
return true;
|
||||||
|
}
|
||||||
lock_keyboard( fullscreen );
|
lock_keyboard( fullscreen );
|
||||||
if fullscreen
|
if fullscreen
|
||||||
{
|
{
|
||||||
@@ -113,7 +120,8 @@ impl<A: App> Platform for WebPlatform<A>
|
|||||||
|
|
||||||
fn set_cursor_shape( &mut self, shape: CursorShape ) -> bool
|
fn set_cursor_shape( &mut self, shape: CursorShape ) -> bool
|
||||||
{
|
{
|
||||||
self.cursor_target.style().set_property( "cursor", cursor::css( shape ) ).is_ok()
|
self.cursor = cursor::css( shape );
|
||||||
|
self.cursor_target.style().set_property( "cursor", self.cursor ).is_ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn text_input_active( &self ) -> bool
|
fn text_input_active( &self ) -> bool
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ fn overlay_role<A: App>( app: &A, spec: &OverlaySpec<A::Message>, ( width, heigh
|
|||||||
"height": logical( height ),
|
"height": logical( height ),
|
||||||
"exclusive_zone": zone,
|
"exclusive_zone": zone,
|
||||||
"keyboard": if spec.keyboard_exclusive { "exclusive" } else { "on_demand" },
|
"keyboard": if spec.keyboard_exclusive { "exclusive" } else { "on_demand" },
|
||||||
|
"namespace": app.overlay_namespace( spec.id ),
|
||||||
} )
|
} )
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,7 +87,7 @@ pub( super ) fn reconcile_overlays<A: App>( data: &mut AppData<A> )
|
|||||||
Vec::new()
|
Vec::new()
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let mut platform = WebSurfaceState::new( canvas, id, gpu, listeners );
|
let mut platform = WebSurfaceState::new( canvas, input, id, gpu, listeners );
|
||||||
platform.role = Some( role );
|
platform.role = Some( role );
|
||||||
let mut ss = SurfaceState::new( platform, 0.0, String::new() );
|
let mut ss = SurfaceState::new( platform, 0.0, String::new() );
|
||||||
// The main surface's scale until forge/web configures this one, so the first frame lays out at the right size.
|
// The main surface's scale until forge/web configures this one, so the first frame lays out at the right size.
|
||||||
|
|||||||
@@ -72,7 +72,16 @@ pub( super ) fn present<A: App>
|
|||||||
( _, _, Some( _ ) ) => present_software( surface, canvas ),
|
( _, _, Some( _ ) ) => present_software( surface, canvas ),
|
||||||
_ => false,
|
_ => false,
|
||||||
};
|
};
|
||||||
if let Some( desktop ) = platform.desktop.as_ref() { sync_input_region( desktop, surface, frame ); }
|
if let Some( desktop ) = platform.desktop.as_ref()
|
||||||
|
{
|
||||||
|
sync_input_region( desktop, surface, frame );
|
||||||
|
if shown && !surface.presented
|
||||||
|
{
|
||||||
|
surface.presented = true;
|
||||||
|
let request = json!( { "type": "presented", "surface": surface.id } );
|
||||||
|
if let Err( e ) = desktop.request( &request ) { web_sys::console::warn_1( &format!( "[ltk] presented: {e}" ).into() ); }
|
||||||
|
}
|
||||||
|
}
|
||||||
if let ( true, Some( focus ) ) = ( shown, frame.frame ) { platform.owe_frame( focus ); }
|
if let ( true, Some( focus ) ) = ( shown, frame.frame ) { platform.owe_frame( focus ); }
|
||||||
shown
|
shown
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-2
@@ -15,7 +15,7 @@ use wasm_bindgen::closure::Closure;
|
|||||||
use wasm_bindgen::{ JsCast, JsValue };
|
use wasm_bindgen::{ JsCast, JsValue };
|
||||||
use web_sys::{ Document, Event, EventTarget, HtmlCanvasElement, HtmlElement, OffscreenCanvas, WebGl2RenderingContext };
|
use web_sys::{ Document, Event, EventTarget, HtmlCanvasElement, HtmlElement, OffscreenCanvas, WebGl2RenderingContext };
|
||||||
|
|
||||||
use crate::app::App;
|
use crate::app::{ App, ToplevelEvent };
|
||||||
use crate::event_loop::{ AppData, RunError, Sources, SurfaceFocus, SurfaceState };
|
use crate::event_loop::{ AppData, RunError, Sources, SurfaceFocus, SurfaceState };
|
||||||
|
|
||||||
use super::desktop::{ main_role, Connection, MAIN };
|
use super::desktop::{ main_role, Connection, MAIN };
|
||||||
@@ -42,6 +42,8 @@ pub( super ) enum PageEvent
|
|||||||
{
|
{
|
||||||
surface: u32,
|
surface: u32,
|
||||||
},
|
},
|
||||||
|
/// A window of the desktop's, as `ext-foreign-toplevel-list` reports it.
|
||||||
|
Toplevel( ToplevelEvent ),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An event listener, registered for as long as it lives.
|
/// An event listener, registered for as long as it lives.
|
||||||
@@ -129,6 +131,7 @@ pub( crate ) fn try_run<A: App>( app: A ) -> Result<(), RunError>
|
|||||||
( Some( desktop ), _ ) =>
|
( Some( desktop ), _ ) =>
|
||||||
{
|
{
|
||||||
let ( canvas, input ) = desktop.create_surface( MAIN, main_role( &app ) ).map_err( RunError::Desktop )?;
|
let ( canvas, input ) = desktop.create_surface( MAIN, main_role( &app ) ).map_err( RunError::Desktop )?;
|
||||||
|
if app.start_fullscreen() { desktop.set_fullscreen( MAIN, true ); }
|
||||||
( canvas, input, false )
|
( canvas, input, false )
|
||||||
}
|
}
|
||||||
( None, Some( canvas ) ) => ( canvas.clone(), HtmlElement::from( canvas ), false ),
|
( None, Some( canvas ) ) => ( canvas.clone(), HtmlElement::from( canvas ), false ),
|
||||||
@@ -163,7 +166,7 @@ pub( crate ) fn try_run<A: App>( app: A ) -> Result<(), RunError>
|
|||||||
if own { let _ = target.focus(); }
|
if own { let _ = target.focus(); }
|
||||||
|
|
||||||
let coarse = window.match_media( "(pointer: coarse)" ).ok().flatten().is_some_and( |m| m.matches() );
|
let coarse = window.match_media( "(pointer: coarse)" ).ok().flatten().is_some_and( |m| m.matches() );
|
||||||
let main = SurfaceState::new( WebSurfaceState::new( canvas, MAIN, offscreen.is_some(), input ), 0.0, String::new() );
|
let main = SurfaceState::new( WebSurfaceState::new( canvas, target.clone(), MAIN, offscreen.is_some(), input ), 0.0, String::new() );
|
||||||
let platform = WebPlatform::new( desktop, offscreen, target, listeners, inset_probe );
|
let platform = WebPlatform::new( desktop, offscreen, target, listeners, inset_probe );
|
||||||
// ltk's own keyboard where the page is used by touch, since the page cannot raise the system's for a canvas.
|
// ltk's own keyboard where the page is used by touch, since the page cannot raise the system's for a canvas.
|
||||||
let mut data = AppData::new( app, platform, main, Sources::new( wake ), coarse, None );
|
let mut data = AppData::new( app, platform, main, Sources::new( wake ), coarse, None );
|
||||||
@@ -289,6 +292,10 @@ impl<A: App> AppData<A>
|
|||||||
if self.app.on_close_requested() { self.exit_requested = true; }
|
if self.app.on_close_requested() { self.exit_requested = true; }
|
||||||
}
|
}
|
||||||
PageEvent::Close { surface } => self.dismiss_overlay( surface ),
|
PageEvent::Close { surface } => self.dismiss_overlay( surface ),
|
||||||
|
PageEvent::Toplevel( event ) =>
|
||||||
|
{
|
||||||
|
if let Some( msg ) = self.app.on_toplevel_event( event ) { self.pending_msgs.push( msg ); }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.dispatch_sources();
|
self.dispatch_sources();
|
||||||
@@ -302,6 +309,7 @@ impl<A: App> AppData<A>
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.before_draw();
|
self.before_draw();
|
||||||
|
super::desktop::issue_activation_tokens( self );
|
||||||
super::overlays::reconcile_overlays( self );
|
super::overlays::reconcile_overlays( self );
|
||||||
let drawn = in_frame && self.draw_due();
|
let drawn = in_frame && self.draw_due();
|
||||||
super::subsurfaces::reconcile_subsurfaces( self );
|
super::subsurfaces::reconcile_subsurfaces( self );
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
// SPDX-License-Identifier: LGPL-2.1-only
|
||||||
|
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>
|
||||||
|
|
||||||
|
//! The shell protocol over the app's connection to forge/web (forge/web/DESIGN.md, section 7): the JSON a shell speaks to forge over its socket, sent as a `shell` request and received as a `shell` event. What is sent before the app has connected waits for the connection.
|
||||||
|
|
||||||
|
use std::cell::RefCell;
|
||||||
|
|
||||||
|
use serde_json::{ json, Value };
|
||||||
|
use wasm_bindgen::JsValue;
|
||||||
|
|
||||||
|
thread_local!
|
||||||
|
{
|
||||||
|
static REQUEST: RefCell<Option<js_sys::Function>> = const { RefCell::new( None ) };
|
||||||
|
static PENDING: RefCell<Vec<Value>> = const { RefCell::new( Vec::new() ) };
|
||||||
|
static LISTENER: RefCell<Option<Box<dyn Fn( &str )>>> = const { RefCell::new( None ) };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send `message`, one of the shell protocol's JSON objects, to forge/web.
|
||||||
|
pub fn shell_send( message: &str )
|
||||||
|
{
|
||||||
|
let message: Value = match serde_json::from_str( message )
|
||||||
|
{
|
||||||
|
Ok( m ) => m,
|
||||||
|
Err( e ) =>
|
||||||
|
{
|
||||||
|
web_sys::console::warn_1( &format!( "[ltk] shell message is not JSON ({e}): {message}" ).into() );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let request = REQUEST.with( |r| r.borrow().clone() );
|
||||||
|
match request
|
||||||
|
{
|
||||||
|
Some( request ) => send( &request, message ),
|
||||||
|
None => PENDING.with( |p| p.borrow_mut().push( message ) ),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Have `listener` receive each of forge/web's shell messages as JSON.
|
||||||
|
pub fn set_shell_listener( listener: impl Fn( &str ) + 'static )
|
||||||
|
{
|
||||||
|
LISTENER.with( |l| *l.borrow_mut() = Some( Box::new( listener ) ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
fn send( request: &js_sys::Function, message: Value )
|
||||||
|
{
|
||||||
|
let json = json!( { "type": "shell", "message": message } ).to_string();
|
||||||
|
if let Err( e ) = request.call1( &JsValue::NULL, &JsValue::from_str( &json ) )
|
||||||
|
{
|
||||||
|
web_sys::console::warn_2( &"[ltk] forge/web refused a shell message:".into(), &e );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The connection's `request`, once connected, and `None` once it has closed.
|
||||||
|
pub( super ) fn connected( request: Option<js_sys::Function> )
|
||||||
|
{
|
||||||
|
if let Some( request ) = &request
|
||||||
|
{
|
||||||
|
for message in PENDING.with( |p| std::mem::take( &mut *p.borrow_mut() ) )
|
||||||
|
{
|
||||||
|
send( request, message );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
REQUEST.with( |r| *r.borrow_mut() = request );
|
||||||
|
}
|
||||||
|
|
||||||
|
pub( super ) fn deliver( message: &Value )
|
||||||
|
{
|
||||||
|
let json = message.to_string();
|
||||||
|
LISTENER.with( |l|
|
||||||
|
{
|
||||||
|
if let Some( listener ) = l.borrow().as_ref() { listener( &json ); }
|
||||||
|
} );
|
||||||
|
}
|
||||||
@@ -28,8 +28,10 @@ pub( crate ) struct WebPlatform<A: App>
|
|||||||
pub gl: Option<Arc<glow::Context>>,
|
pub gl: Option<Arc<glow::Context>>,
|
||||||
/// The browser took the WebGL context away; nothing is drawn until it gives it back.
|
/// The browser took the WebGL context away; nothing is drawn until it gives it back.
|
||||||
pub context_lost: bool,
|
pub context_lost: bool,
|
||||||
/// Where the cursor's shape is set: the element under the pointer that takes the main surface's input.
|
/// Where the cursor's shape is set: the element taking the input of the surface the pointer last entered, since CSS gives each element its own.
|
||||||
pub cursor_target: HtmlElement,
|
pub cursor_target: HtmlElement,
|
||||||
|
/// The cursor's shape as CSS names it, for the next surface the pointer enters.
|
||||||
|
pub cursor: &'static str,
|
||||||
/// A hidden element padded by `env( safe-area-inset-* )`, which tells what a phone's notch and bars cover; only on ltk's own canvas over the page.
|
/// A hidden element padded by `env( safe-area-inset-* )`, which tells what a phone's notch and bars cover; only on ltk's own canvas over the page.
|
||||||
pub inset_probe: Option<Element>,
|
pub inset_probe: Option<Element>,
|
||||||
/// The surfaces a frame was presented or asked for; the loop reports them at the next animation frame.
|
/// The surfaces a frame was presented or asked for; the loop reports them at the next animation frame.
|
||||||
@@ -61,6 +63,7 @@ impl<A: App> WebPlatform<A>
|
|||||||
gl,
|
gl,
|
||||||
context_lost: false,
|
context_lost: false,
|
||||||
cursor_target,
|
cursor_target,
|
||||||
|
cursor: "default",
|
||||||
inset_probe,
|
inset_probe,
|
||||||
frame_due: RefCell::new( Vec::new() ),
|
frame_due: RefCell::new( Vec::new() ),
|
||||||
next_surface: Cell::new( super::desktop::MAIN + 1 ),
|
next_surface: Cell::new( super::desktop::MAIN + 1 ),
|
||||||
|
|||||||
@@ -151,12 +151,12 @@ pub( super ) fn reconcile_subsurfaces<A: App>( data: &mut AppData<A> )
|
|||||||
|
|
||||||
fn create<Msg: Clone>( desktop: &Connection, id: u32, spec: &SubsurfaceSpec<Msg>, parent: &Parent, gpu: bool ) -> Option<WebSubsurface>
|
fn create<Msg: Clone>( desktop: &Connection, id: u32, spec: &SubsurfaceSpec<Msg>, parent: &Parent, gpu: bool ) -> Option<WebSubsurface>
|
||||||
{
|
{
|
||||||
let ( canvas, _input ) = desktop.create_surface( id, role( spec.x, spec.y, parent ) ).map_err( |e| warn( spec.id, &e ) ).ok()?;
|
let ( canvas, input ) = desktop.create_surface( id, role( spec.x, spec.y, parent ) ).map_err( |e| warn( spec.id, &e ) ).ok()?;
|
||||||
// Input falls through to the parent, which keeps one gesture model for both.
|
// Input falls through to the parent, which keeps one gesture model for both.
|
||||||
if let Err( e ) = desktop.request( &json!( { "type": "set_input_region", "surface": id, "rects": [] } ) ) { warn( spec.id, &e ); }
|
if let Err( e ) = desktop.request( &json!( { "type": "set_input_region", "surface": id, "rects": [] } ) ) { warn( spec.id, &e ); }
|
||||||
Some( WebSubsurface
|
Some( WebSubsurface
|
||||||
{
|
{
|
||||||
surface: WebSurfaceState::new( canvas, id, gpu, Vec::new() ),
|
surface: WebSurfaceState::new( canvas, input, id, gpu, Vec::new() ),
|
||||||
canvas: None,
|
canvas: None,
|
||||||
parent: spec.parent,
|
parent: spec.parent,
|
||||||
parent_id: parent.id,
|
parent_id: parent.id,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>
|
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>
|
||||||
|
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use web_sys::{ CanvasRenderingContext2d, Element, HtmlCanvasElement, ImageBitmapRenderingContext };
|
use web_sys::{ CanvasRenderingContext2d, Element, HtmlCanvasElement, HtmlElement, ImageBitmapRenderingContext };
|
||||||
|
|
||||||
use crate::app::App;
|
use crate::app::App;
|
||||||
use crate::event_loop::AppData;
|
use crate::event_loop::AppData;
|
||||||
@@ -14,6 +14,8 @@ use super::run::Listener;
|
|||||||
pub( crate ) struct WebSurfaceState
|
pub( crate ) struct WebSurfaceState
|
||||||
{
|
{
|
||||||
pub canvas: HtmlCanvasElement,
|
pub canvas: HtmlCanvasElement,
|
||||||
|
/// Takes the surface's input: the element forge/web gives above the canvas, or the canvas itself without a desktop.
|
||||||
|
pub input: HtmlElement,
|
||||||
/// Takes the frames painted on the shared WebGL context.
|
/// Takes the frames painted on the shared WebGL context.
|
||||||
pub bitmap: Option<ImageBitmapRenderingContext>,
|
pub bitmap: Option<ImageBitmapRenderingContext>,
|
||||||
/// Takes the software renderer's frames, without WebGL2.
|
/// Takes the software renderer's frames, without WebGL2.
|
||||||
@@ -28,17 +30,19 @@ pub( crate ) struct WebSurfaceState
|
|||||||
pub role: Option<Value>,
|
pub role: Option<Value>,
|
||||||
/// The surface's input, removed with it.
|
/// The surface's input, removed with it.
|
||||||
pub listeners: Vec<Listener>,
|
pub listeners: Vec<Listener>,
|
||||||
|
/// A frame has been shown, which forge/web was told of: its first, after which a launched window counts as activated.
|
||||||
|
pub presented: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WebSurfaceState
|
impl WebSurfaceState
|
||||||
{
|
{
|
||||||
/// The surface shown on `canvas`, through the shared WebGL context when there is one, else in software.
|
/// The surface shown on `canvas`, through the shared WebGL context when there is one, else in software.
|
||||||
pub( super ) fn new( canvas: HtmlCanvasElement, id: u32, gpu: bool, listeners: Vec<Listener> ) -> Self
|
pub( super ) fn new( canvas: HtmlCanvasElement, input: HtmlElement, id: u32, gpu: bool, listeners: Vec<Listener> ) -> Self
|
||||||
{
|
{
|
||||||
let context = |kind: &str| canvas.get_context( kind ).ok().flatten();
|
let context = |kind: &str| canvas.get_context( kind ).ok().flatten();
|
||||||
let bitmap = if gpu { context( "bitmaprenderer" ).and_then( |c| wasm_bindgen::JsCast::dyn_into( c ).ok() ) } else { None };
|
let bitmap = if gpu { context( "bitmaprenderer" ).and_then( |c| wasm_bindgen::JsCast::dyn_into( c ).ok() ) } else { None };
|
||||||
let ctx2d = if gpu { None } else { context( "2d" ).and_then( |c| wasm_bindgen::JsCast::dyn_into( c ).ok() ) };
|
let ctx2d = if gpu { None } else { context( "2d" ).and_then( |c| wasm_bindgen::JsCast::dyn_into( c ).ok() ) };
|
||||||
Self { canvas, bitmap, ctx2d, pixels: Vec::new(), id, input_region: None, role: None, listeners }
|
Self { canvas, input, bitmap, ctx2d, pixels: Vec::new(), id, input_region: None, role: None, listeners, presented: false }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,14 @@ use tiny_skia::{ Path, PathBuilder };
|
|||||||
|
|
||||||
use crate::types::{ Corners, PathCmd };
|
use crate::types::{ Corners, PathCmd };
|
||||||
|
|
||||||
|
/// What names an image's pixels in both backends' image caches: a fingerprint of them, or a key its caller vouches for, kept apart so that neither is taken for the other.
|
||||||
|
#[ derive( Clone, Copy, PartialEq, Eq, Hash ) ]
|
||||||
|
pub( crate ) enum ImageId
|
||||||
|
{
|
||||||
|
Content( u64 ),
|
||||||
|
Keyed( u64 ),
|
||||||
|
}
|
||||||
|
|
||||||
/// A 64-bit fingerprint of an RGBA buffer, the key of both backends' image
|
/// A 64-bit fingerprint of an RGBA buffer, the key of both backends' image
|
||||||
/// caches. Hashes the full slice below 16 KB (icons, thumbnails) and a
|
/// caches. Hashes the full slice below 16 KB (icons, thumbnails) and a
|
||||||
/// strided 8 × 512-byte sample above it, so a wallpaper does not pay a
|
/// strided 8 × 512-byte sample above it, so a wallpaper does not pay a
|
||||||
|
|||||||
+13
-3
@@ -19,7 +19,7 @@ use tiny_skia::{ IntSize, Pixmap, PixmapPaint, Transform };
|
|||||||
|
|
||||||
use crate::types::Rect;
|
use crate::types::Rect;
|
||||||
|
|
||||||
use super::helpers::fingerprint_rgba;
|
use super::helpers::{ fingerprint_rgba, ImageId };
|
||||||
use super::lru::Lru;
|
use super::lru::Lru;
|
||||||
use super::SoftwareCanvas;
|
use super::SoftwareCanvas;
|
||||||
|
|
||||||
@@ -27,7 +27,7 @@ use super::SoftwareCanvas;
|
|||||||
/// cache: a phone wallpaper plus a working set of icons.
|
/// cache: a phone wallpaper plus a working set of icons.
|
||||||
const PREMUL_CACHE_MAX_BYTES: usize = 32 * 1024 * 1024;
|
const PREMUL_CACHE_MAX_BYTES: usize = 32 * 1024 * 1024;
|
||||||
|
|
||||||
type PremulKey = ( u32, u32, u64, u32 );
|
type PremulKey = ( u32, u32, ImageId, u32 );
|
||||||
|
|
||||||
/// A premultiplied source and whether every pixel was fully opaque, which
|
/// A premultiplied source and whether every pixel was fully opaque, which
|
||||||
/// lets a 1:1 draw be a row copy.
|
/// lets a 1:1 draw be a row copy.
|
||||||
@@ -84,6 +84,16 @@ fn copy_rows( dst: &mut Pixmap, src: &Pixmap, x: i32, y: i32 )
|
|||||||
impl SoftwareCanvas
|
impl SoftwareCanvas
|
||||||
{
|
{
|
||||||
pub fn draw_image_data( &mut self, rgba_data: &[u8], img_w: u32, img_h: u32, dest: Rect, opacity: f32 )
|
pub fn draw_image_data( &mut self, rgba_data: &[u8], img_w: u32, img_h: u32, dest: Rect, opacity: f32 )
|
||||||
|
{
|
||||||
|
self.draw_image( ImageId::Content( fingerprint_rgba( rgba_data ) ), rgba_data, img_w, img_h, dest, opacity );
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn draw_image_keyed( &mut self, key: u64, rgba_data: &[u8], img_w: u32, img_h: u32, dest: Rect, opacity: f32 )
|
||||||
|
{
|
||||||
|
self.draw_image( ImageId::Keyed( key ), rgba_data, img_w, img_h, dest, opacity );
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_image( &mut self, id: ImageId, rgba_data: &[u8], img_w: u32, img_h: u32, dest: Rect, opacity: f32 )
|
||||||
{
|
{
|
||||||
if !super::helpers::validate_rgba_dims( "SoftwareCanvas", rgba_data, img_w, img_h )
|
if !super::helpers::validate_rgba_dims( "SoftwareCanvas", rgba_data, img_w, img_h )
|
||||||
{
|
{
|
||||||
@@ -104,7 +114,7 @@ impl SoftwareCanvas
|
|||||||
let Some( int_size ) = tiny_skia::IntSize::from_wh( img_w, img_h ) else { return };
|
let Some( int_size ) = tiny_skia::IntSize::from_wh( img_w, img_h ) else { return };
|
||||||
|
|
||||||
let alpha = opacity * self.global_alpha;
|
let alpha = opacity * self.global_alpha;
|
||||||
let key = ( img_w, img_h, fingerprint_rgba( rgba_data ), alpha.to_bits() );
|
let key = ( img_w, img_h, id, alpha.to_bits() );
|
||||||
PREMUL_CACHE.with( |cell|
|
PREMUL_CACHE.with( |cell|
|
||||||
{
|
{
|
||||||
let mut cache = cell.borrow_mut();
|
let mut cache = cell.borrow_mut();
|
||||||
|
|||||||
@@ -962,6 +962,16 @@ impl Canvas
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// [`Self::draw_image_data`] with `key` naming the pixels instead of a fingerprint of them, so a caller that draws the same images on every frame, such as a terminal's glyphs, does not hash them each time. Every draw with the same `key` must pass the same pixels: they are read only when the image is not cached. A key never names an image drawn through `draw_image_data`.
|
||||||
|
pub fn draw_image_keyed( &mut self, key: u64, rgba_data: &[u8], img_w: u32, img_h: u32, dest: Rect, opacity: f32 )
|
||||||
|
{
|
||||||
|
match self
|
||||||
|
{
|
||||||
|
Canvas::Software( c ) => c.draw_image_keyed( key, rgba_data, img_w, img_h, dest, opacity ),
|
||||||
|
Canvas::Gles( c ) => c.draw_image_keyed( key, rgba_data, img_w, img_h, dest, opacity ),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Zero pixels inside each rect — used by the partial-redraw
|
/// Zero pixels inside each rect — used by the partial-redraw
|
||||||
/// path when the surface background is fully transparent.
|
/// path when the surface background is fully transparent.
|
||||||
pub fn clear_rects_transparent( &mut self, rects: &[Rect] )
|
pub fn clear_rects_transparent( &mut self, rects: &[Rect] )
|
||||||
@@ -1305,3 +1315,42 @@ mod pixel_snap_tests
|
|||||||
assert_ne!( at_int, at_high, "x+0.6 rounds up to the next column" );
|
assert_ne!( at_int, at_high, "x+0.6 rounds up to the next column" );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[ cfg( test ) ]
|
||||||
|
mod keyed_image_tests
|
||||||
|
{
|
||||||
|
use super::helpers::fingerprint_rgba;
|
||||||
|
use super::Canvas;
|
||||||
|
use crate::types::Rect;
|
||||||
|
|
||||||
|
const AT: Rect = Rect { x: 2.0, y: 2.0, width: 4.0, height: 4.0 };
|
||||||
|
|
||||||
|
fn pixels( canvas: &Canvas ) -> Vec<u8>
|
||||||
|
{
|
||||||
|
let Canvas::Software( c ) = canvas else { panic!( "software canvas" ) };
|
||||||
|
c.pixmap.data().to_vec()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[ test ]
|
||||||
|
fn a_keyed_image_draws_as_its_pixels_do()
|
||||||
|
{
|
||||||
|
let red = [ 255u8, 0, 0, 255 ].repeat( 16 );
|
||||||
|
let mut by_content = Canvas::new( 16, 16 );
|
||||||
|
by_content.draw_image_data( &red, 4, 4, AT, 1.0 );
|
||||||
|
let mut by_key = Canvas::new( 16, 16 );
|
||||||
|
by_key.draw_image_keyed( 7, &red, 4, 4, AT, 1.0 );
|
||||||
|
assert_eq!( pixels( &by_content ), pixels( &by_key ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
#[ test ]
|
||||||
|
fn a_key_equal_to_a_fingerprint_is_another_image()
|
||||||
|
{
|
||||||
|
let red = [ 255u8, 0, 0, 255 ].repeat( 16 );
|
||||||
|
let blue = [ 0u8, 0, 255, 255 ].repeat( 16 );
|
||||||
|
let mut canvas = Canvas::new( 16, 16 );
|
||||||
|
canvas.draw_image_data( &red, 4, 4, AT, 1.0 );
|
||||||
|
canvas.draw_image_keyed( fingerprint_rgba( &red ), &blue, 4, 4, AT, 1.0 );
|
||||||
|
let Canvas::Software( c ) = &canvas else { panic!( "software canvas" ) };
|
||||||
|
assert!( c.pixmap.pixel( 3, 3 ).unwrap().blue() > 200 );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+3
-3
@@ -1,9 +1,9 @@
|
|||||||
// SPDX-License-Identifier: LGPL-2.1-only
|
// SPDX-License-Identifier: LGPL-2.1-only
|
||||||
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>
|
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>
|
||||||
|
|
||||||
//! The monotonic clock, which `std` does not provide in a browser.
|
//! The monotonic clock, which `std` does not provide in a browser: [`Instant`] is `std`'s natively and `web_time`'s on wasm32, where `std::time::Instant::now` panics. An app that builds for both uses it in place of `std`'s.
|
||||||
|
|
||||||
#[ cfg( not( target_arch = "wasm32" ) ) ]
|
#[ cfg( not( target_arch = "wasm32" ) ) ]
|
||||||
pub( crate ) use std::time::Instant;
|
pub use std::time::Instant;
|
||||||
#[ cfg( target_arch = "wasm32" ) ]
|
#[ cfg( target_arch = "wasm32" ) ]
|
||||||
pub( crate ) use web_time::Instant;
|
pub use web_time::Instant;
|
||||||
|
|||||||
+10
-10
@@ -1,37 +1,37 @@
|
|||||||
// SPDX-License-Identifier: LGPL-2.1-only
|
// SPDX-License-Identifier: LGPL-2.1-only
|
||||||
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>
|
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>
|
||||||
|
|
||||||
//! Reading themes, fonts and keyboard layouts: from the file system, or in a browser, which has none, from the files the page loaded into memory.
|
//! Reading themes, fonts, keyboard layouts and an app's own files: from the file system, or in a browser, which has none, from the files the page loaded into memory with `ltk::web::load_resources` or `ltk::web::load_tree`. An app that also builds for the browser reads its files through it.
|
||||||
|
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::path::{ Path, PathBuf };
|
use std::path::{ Path, PathBuf };
|
||||||
|
|
||||||
pub( crate ) fn read_to_string( path: impl AsRef<Path> ) -> io::Result<String>
|
pub fn read_to_string( path: impl AsRef<Path> ) -> io::Result<String>
|
||||||
{
|
{
|
||||||
String::from_utf8( read( path )? ).map_err( |e| io::Error::new( io::ErrorKind::InvalidData, e ) )
|
String::from_utf8( read( path )? ).map_err( |e| io::Error::new( io::ErrorKind::InvalidData, e ) )
|
||||||
}
|
}
|
||||||
|
|
||||||
#[ cfg( not( target_arch = "wasm32" ) ) ]
|
#[ cfg( not( target_arch = "wasm32" ) ) ]
|
||||||
pub( crate ) fn read( path: impl AsRef<Path> ) -> io::Result<Vec<u8>>
|
pub fn read( path: impl AsRef<Path> ) -> io::Result<Vec<u8>>
|
||||||
{
|
{
|
||||||
std::fs::read( path )
|
std::fs::read( path )
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The paths of the entries in `dir`, skipping any that cannot be read.
|
/// The paths of the entries in `dir`, skipping any that cannot be read.
|
||||||
#[ cfg( not( target_arch = "wasm32" ) ) ]
|
#[ cfg( not( target_arch = "wasm32" ) ) ]
|
||||||
pub( crate ) fn read_dir( dir: impl AsRef<Path> ) -> io::Result<Vec<PathBuf>>
|
pub fn read_dir( dir: impl AsRef<Path> ) -> io::Result<Vec<PathBuf>>
|
||||||
{
|
{
|
||||||
Ok( std::fs::read_dir( dir )?.filter_map( Result::ok ).map( |entry| entry.path() ).collect() )
|
Ok( std::fs::read_dir( dir )?.filter_map( Result::ok ).map( |entry| entry.path() ).collect() )
|
||||||
}
|
}
|
||||||
|
|
||||||
#[ cfg( not( target_arch = "wasm32" ) ) ]
|
#[ cfg( not( target_arch = "wasm32" ) ) ]
|
||||||
pub( crate ) fn is_file( path: impl AsRef<Path> ) -> bool
|
pub fn is_file( path: impl AsRef<Path> ) -> bool
|
||||||
{
|
{
|
||||||
path.as_ref().is_file()
|
path.as_ref().is_file()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[ cfg( not( target_arch = "wasm32" ) ) ]
|
#[ cfg( not( target_arch = "wasm32" ) ) ]
|
||||||
pub( crate ) fn is_dir( path: impl AsRef<Path> ) -> bool
|
pub fn is_dir( path: impl AsRef<Path> ) -> bool
|
||||||
{
|
{
|
||||||
path.as_ref().is_dir()
|
path.as_ref().is_dir()
|
||||||
}
|
}
|
||||||
@@ -53,14 +53,14 @@ pub( crate ) fn insert( path: impl Into<PathBuf>, bytes: Vec<u8> )
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[ cfg( target_arch = "wasm32" ) ]
|
#[ cfg( target_arch = "wasm32" ) ]
|
||||||
pub( crate ) fn read( path: impl AsRef<Path> ) -> io::Result<Vec<u8>>
|
pub fn read( path: impl AsRef<Path> ) -> io::Result<Vec<u8>>
|
||||||
{
|
{
|
||||||
files().get( path.as_ref() ).map( |bytes| bytes.to_vec() ).ok_or_else( || io::ErrorKind::NotFound.into() )
|
files().get( path.as_ref() ).map( |bytes| bytes.to_vec() ).ok_or_else( || io::ErrorKind::NotFound.into() )
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The paths of the files and directories directly in `dir`.
|
/// The paths of the files and directories directly in `dir`.
|
||||||
#[ cfg( target_arch = "wasm32" ) ]
|
#[ cfg( target_arch = "wasm32" ) ]
|
||||||
pub( crate ) fn read_dir( dir: impl AsRef<Path> ) -> io::Result<Vec<PathBuf>>
|
pub fn read_dir( dir: impl AsRef<Path> ) -> io::Result<Vec<PathBuf>>
|
||||||
{
|
{
|
||||||
let dir = dir.as_ref();
|
let dir = dir.as_ref();
|
||||||
let mut entries: Vec<PathBuf> = files().keys()
|
let mut entries: Vec<PathBuf> = files().keys()
|
||||||
@@ -73,13 +73,13 @@ pub( crate ) fn read_dir( dir: impl AsRef<Path> ) -> io::Result<Vec<PathBuf>>
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[ cfg( target_arch = "wasm32" ) ]
|
#[ cfg( target_arch = "wasm32" ) ]
|
||||||
pub( crate ) fn is_file( path: impl AsRef<Path> ) -> bool
|
pub fn is_file( path: impl AsRef<Path> ) -> bool
|
||||||
{
|
{
|
||||||
files().contains_key( path.as_ref() )
|
files().contains_key( path.as_ref() )
|
||||||
}
|
}
|
||||||
|
|
||||||
#[ cfg( target_arch = "wasm32" ) ]
|
#[ cfg( target_arch = "wasm32" ) ]
|
||||||
pub( crate ) fn is_dir( path: impl AsRef<Path> ) -> bool
|
pub fn is_dir( path: impl AsRef<Path> ) -> bool
|
||||||
{
|
{
|
||||||
let path = path.as_ref();
|
let path = path.as_ref();
|
||||||
files().keys().any( |file| file != path && file.starts_with( path ) )
|
files().keys().any( |file| file != path && file.starts_with( path ) )
|
||||||
|
|||||||
Reference in New Issue
Block a user