da34eee9e6ad716e33965e2280b567cec480f905
28
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
8882078743 |
A text field that serves as an editor, a file chooser, ltk::files with callbacks, Text::fit, App::window_title, and a stack that keeps its children inside it
A multiline text field was a box for a few lines: it wrapped its whole value by shaping each character alone on every frame, every click and every caret motion, so a file of some size made it crawl; it drew a tab as the font's glyph for it, had no scroll bar, no page keys and no way to number its lines, kept no record of the view it showed, so once the cursor had left the first screen it stayed on the last row and the arrows moved the text, and an app could neither colour its text, type a tab with Tab, give it a face of its own, ask it to copy, cut, paste or select, nor hear where its cursor was. Notes, the Eydos text editor, needs all of it, and every app that opens or saves the user's files needed a dialog of its own.
Speed. `Canvas::measure_char` measures a character's advance once per font and size and keeps it (`text_shaping::char_advance_cached`, emptied with the shaped lines), and `compute_visual_lines` keeps the last four layouts by the value's hash and length, the row width, the size and the face, so a value is wrapped again only when one of them changes, and drawing, hit testing and caret motion over the same value wrap it once.
The view. `TextEdit::draw` returns the view it showed, which the frame keeps in `text_scroll`, so the view moves only when the cursor leaves it, and by the least; a `TextCommand::Select` no longer resets it. `Window::shown` gives the lines as the last frame showed them, wherever the cursor went since, and a press, the wheel, the scroll bar and the page keys act on them: a press that gave a field the focus, which puts its cursor at the end, landed where the view would have gone to show that cursor, near the end of the text, instead of on the character pressed. The cursor a scroll keeps is taken back to the value's length as the view's is, so a scroll made with the cursor at its end sentinel holds instead of going back to the cursor at once.
Tabs. In a multiline field a tab reaches the next stop, every four digits' widths, as four monospace columns are (`theme::TAB_COLUMNS`, `text_edit/tabs.rs`), when it is drawn, selected, clicked and crossed with the arrows. `TextEdit::tab_text( text )` has Tab type `text` while the field has the focus instead of moving the focus on; Shift+Tab still moves it back and Ctrl+Tab still reaches the app.
The scroll bar. A field whose lines do not all fit shows a scroll bar along its right edge (`Window::scrollbar`): its thumb is held and dragged, or a press on its track brings the thumb there, by pointer or touch, without moving the cursor (`AppData::text_scrollbar_grab`, `handle_text_scrollbar_press`, `handle_text_scrollbar_drag`); the pointer over it is the arrow rather than the text cursor, and a long press that holds it no longer opens the Copy / Cut / Paste menu.
Page Up and Page Down move the cursor of a multiline field by the lines on screen but one, at the same height on screen, and the view with it, to the start or the end from the first or the last page (`cursor_visual_page`, `handle_cursor_page`); Shift extends the selection, and with Ctrl, or in any other widget, the keys still reach the app. A key without Ctrl that types no text, such as F3, was dropped while a text field had the focus; it now goes to `App::on_key_with_modifiers`, as it does with no field focused.
The text as an editor's. `TextEdit::highlights( Arc<Vec<TextHighlight>> )` draws runs of a multiline value, byte ranges in order, in colours of their own, what no run covers in the field's colour and a run off a character boundary left out. `TextEdit::font( face )` lays a multiline field's text out in a face of its own at the field's size: wrapping, drawing, the selection, the cursor, clicks, tab stops and the arrows all measure through one `TextFont`, the size and the face, which `text_input_geometry` now gives the runtime's paths where it gave them the size; `Canvas::measure_char_with_font` is `measure_char` in another face. `ltk::monospace_font()` finds a monospace face, DejaVu Sans Mono from the app's resources, which a page and an APK carry, else the first of the system's installed, loaded once. `TextEdit::line_numbers( true )` numbers the lines in a column at the field's left, in the monospace face when there is one, as wide as the number of the last line needs, a wrapped line numbered on its first row and the cursor's in the text colour while the field has the focus; the text starts right of the column alike when drawn and in every runtime path, since they all lay it out in the rect `gutter::text_rect` gives. `borderless()` now also drops a multiline field's background and border.
Talking to the field. `App::take_text_command` hands a field, by its `WidgetId`, a `TextCommand` after the next frame: `Copy`, `Cut`, `Paste` and `SelectAll` through the paths the keys take, and `Select { start, end }`, which focuses the field, selects the range with its ends taken back to the value and to its characters' starts, and scrolls to it. `Reveal { start, end }` does the same without moving the focus, as a search does while its own field keeps the keys, and a multiline field without the focus now shows its selection dimmed (`theme::selection_inactive`). `TextEdit::on_cursor( |at| … )` sends the cursor's byte offset once it is placed and whenever it moves or the value changes length (`report_text_cursors`, after every frame), as an editor's status bar showing the line needs.
A file chooser. `ltk::FileChooser` (`src/file_chooser/`) picks a file to open or a folder and a name to save one as, over `ltk::files`, alike natively and in a browser. The app holds it, puts its `view()` in its own and hands its `FileChooserMsg`s to `update()`, which returns `Chosen::Path` or `Chosen::Cancelled` once the user chose; `wrap` makes the app's message of the chooser's, so its listings reach it through the app's `ChannelSender`. It offers the home folder, the xdg-user-dirs folders that exist, the root and the folders mounted in a browser, with a press there to mount another; it lists folders first, the hidden entries left out, each with its type's icon, a file with its size and the date of its last access or change, sorted as Crate sorts, by name, last access or size, each key in a direction of its own (`Sort`, `SortKey`, `Directions`). `dir` starts elsewhere, `filter( &[ "text/*" ] )` shows only some types of files, `compact( true )` puts the places behind a button for a phone, `sort` starts in an order kept from `current_sort`, and `take_focus_request` gives the field to focus. Saving asks before replacing a file and can create a folder; its errors are in the user's words, and its strings in ltk's seven locales. `files::mounts()` gives the folders mounted in a browser as the desktop last said, which the web backend now keeps. `docs/widgets.md` describes it and `examples/file_chooser.rs` shows it, in `make examples`.
ltk::files::callback. The operations of `ltk::files` with a function in place of a message (`list`, `stat`, `read`, `read_range`, `write`, `create_dir`, `rename`, `remove`, `remove_all`, `copy`, `copy_all`, `mount` and `unmount`), for code with no run loop's `ChannelSender` to take one, such as Prompt's session daemon, which in a browser runs in the page beside the window. `done` is called on ltk's worker thread natively and on the page's thread in a browser.
Closing. An app that keeps its window open in `on_close_requested`, to ask whether to save, was not drawn again, so its question appeared only at the next event, such as a press. Every close request, from the compositor, the title bar's close button, back and the browser, now goes through `AppData::request_close`, which draws the view again when the app refused it.
`App::window_title`. The main window's title, read when the window is made, which the title bar, the task switcher and the dock show; by default the `app_id`. A `ShellMode::Window` app was titled "ltk" on Wayland, whatever the app, with no way to name it but the deprecated `window_config`; in a browser its title was its `app_id`, and is now this.
`Text::fit` and `Text::valign`. `fit()` draws a text at the largest size up to its own at which it fits the rect the layout gives it, on one line within its width and height or, with `wrap`, all its lines within its height, so a text whose length varies, such as a calculator's display, neither overflows a box the window sizes nor needs its size worked out from the window and its content; its preferred size is still the one at its own size. `valign( VAlign )` places the lines at the top, the centre or the bottom of the rect.
A stack keeps its children inside it. A child that `Stack` aligned to an edge or the centre took its preferred size even when that was larger than the stack, so one aligned to the bottom started above the stack's top and one aligned to the right left of its left edge, drawn over whatever was there. Each child's preferred size is now capped by the stack's, less its margin, before it is aligned; children placed with `push_placed` and the offsets of `push_translated` are as they were.
Tests cover Tab with and without `tab_text`, the text commands, `Reveal` leaving the focus alone, a highlight into a character, `on_cursor` reporting once, a refused close drawing again, a key that types nothing reaching the app from a field, tab stops, a click on a tab, the kept layouts, the scroll bar, a view kept while the cursor moves inside it and after a scroll with the cursor at its sentinel, a click landing on the view shown, the page keys, the width of the line numbers, the file chooser's listing, filters, names, user folders, sizes, sorting and its open, save and replace flows, `Text::fit` and a stack's aligned children. `CHANGELOG.md` and `docs/widgets.md` describe the changes.
|
||
|
|
18a0e02249 |
A pull from the top that is unwound back to where it started still reports its release, so an app that followed the pull hears that it was not committed
A scroll view at its top turns a downward drag into overscroll, which `on_move` reports as `MoveOutcome::OverscrollDown { progress }`, and on release `ReleaseEvent::OverscrollDown { committed }` tells the app whether the pull went past its threshold, as pull-from-top-to-dismiss needs. The release was reported only while `overscroll_y` was above zero. A pull dragged back up until it was unwound to zero therefore ended without any release event, and an app that had moved its view with the pull's progress was left waiting for one, its view where the last progress put it.
`GestureState::overscroll_engaged` is set once a gesture has reported overscroll progress and cleared with the rest of the gesture's state. On release, a gesture that reported progress now always gets its `OverscrollDown`, uncommitted when nothing is left of the pull. A gesture that never reached overscroll still reports nothing.
Tests cover a pull unwound to zero, which reports an uncommitted release, and a drag that only scrolled, which reports none.
`CHANGELOG.md` describes the change.
|
||
|
|
8f689ec479 |
An ltk app pastes what other applications copied, the middle button pastes the selection just made, a browser desktop's applications share the clipboard and the primary selection, and the office icons are drawn like the terminal's
Pasting from other applications. Neither Ctrl+V nor the middle button pasted into an ltk app anything copied in another application; only what was copied within the same process pasted. The Wayland backend bound its `wl_data_device` and its primary-selection device in `SeatHandler::new_seat`, which smithay-client-toolkit calls only for seats announced after start-up: `SeatState::new` binds the seats already in the registry without calling it, and on every compositor the seat is there when the app connects. The app therefore never had either device, the compositor never offered it another client's selection, and what it copied did not reach other applications through the compositor either. A Wayland trace of prompt shows both managers bound and neither `get_data_device` nor `get_device` sent. Both devices are now bound in `new_capability`, which comes for every seat, the first time it does; `new_seat` does nothing. The middle button and a selection just made. A compositor offers the primary selection to the window it gives the keyboard, and gives it on the press, so a middle click on a window that did not have the keyboard arrives together with the offer. ltk reads an offer on a thread of its own and pasted at once what it held, so after selecting in another application an ltk app pasted the selection before that. `Platform::primary_selection_pending` says whether another application's selection is still being read: the Wayland backend counts the reader threads it starts and the selections that arrive (`PrimarySelection::pending`), and the other backends answer `false`. A middle-button paste, into the text field under the pointer (`PrimaryPaste::Field`) or for an app that asked with `SelectionKind::Primary` (`PrimaryPaste::App`), goes through `AppData::paste_primary`, which while a read is pending keeps it in `pending_primary_paste`; `check_primary_paste`, run before every draw, makes it once the selection has arrived or after 500 ms with what there is, and `next_primary_paste_wakeup` wakes the loop every 10 ms meanwhile, since the reading thread does not. A field paste is made only if the field still has the focus. The clipboard and the primary selection in a browser desktop. On a page with forge/web, what an app copies goes to forge/web as a `set_selection` request as well as to the browser's clipboard, and the selection the middle button pastes, which did nothing in a browser, as `set_primary_selection` (`Connection::set_selection`). forge/web's `selection` and `primary_selection` events, which every client hears when one sets them and each hears on connecting, become `PageEvent::Selection` and so `AppData::clipboard` and `AppData::primary_selection`, as another client's selection does on Wayland. Copying, and selecting and pasting with the middle button, thus work between the page's applications whether or not the browser lets a page write its clipboard. A `paste` from the browser still brings what other applications of the computer copied; a browser has no primary selection a page can reach. The office icons. The default theme's word processor, spreadsheet and presentation icons came from Figma with a `foreignObject` backdrop blur that resvg ignores, and showed as flat white discs with pale glyphs. They are drawn again as the terminal's are: a dark gradient disc in each one's colour (blue, green, orange), a bright glyph and the terminal's filter. The doc comment of `ltk::files` links `write()` as a function, since `write` alone was ambiguous with the macro for rustdoc. `CHANGELOG.md` describes the changes. |
||
|
|
cf187a3dcc |
The user's files through ltk::files, alike natively and in a browser, and a shell's requests answered in a browser through ltk::web::shell_request
An app read the user's files with `std::fs`, which a browser does not have: there a page reaches a folder of the user's computer only through forge/web, which holds it through the File System Access API and answers asynchronously (forge/web/DESIGN.md, section 11). An app that builds for both, such as Crate, had no way to list, read or write a file in a browser, and a page's memory cannot hold a large file whole. ltk::files. Asynchronous operations on the user's files whose results reach the app as messages through its `ChannelSender`: `list`, `stat`, `read`, `read_range`, `write`, `open_write`, `create_dir`, `rename`, `remove`, `remove_all`, `copy`, `copy_all`, `mount` and `unmount`, each taking the sender and a `done` that turns the `io::Result` into a message. Results come in no set order, so an app that has moved on checks that one is still the one it waits for. Errors are the same `io::ErrorKind`s on every platform, `Interrupted` for a cancelled copy. `Entry` and `Metadata` give a file's kind, size and times, the access time natively only. `ltk::vfs` stays what reads an app's own resources. Natively. `std::fs` on one worker thread of ltk's, in the order the app asked (`files/native.rs`), and each copy on a thread of its own, so a long one holds nothing else up (`files/copy.rs`). A file written whole or through a `Writer` goes beside its target as a temporary file, synced and renamed over it on commit and removed if dropped before, so a file being written is never seen half done. `rename` never replaces what is at `to`, as in a browser. In a browser. Each operation is one of forge/web's `fs_*` requests over the app's connection (`platform/web/files.rs`), carrying a number its `fs_reply` answers with; contents travel as a `Uint8Array` beside the JSON, the second argument of the connection's `request` and of the client's event handler, so `on_event` now takes it. What is asked before the app has connected is sent once it has; on a page without a desktop, or once the connection has closed, it fails with `Unsupported`. forge/web's error codes become their `io::ErrorKind`s, and `fs_progress` reaches a copy's `progress`. Without the web backend, on wasm32 every operation fails with `Unsupported` (`files/unsupported.rs`). Chunks. Contents never travel whole unless they are small: a large file is read with `read_range` and written through a `Writer`, chunk by chunk, `files::CHUNK` (1 MiB) at a time, the next sent once the last is answered. A `Writer` dropped without `close` is aborted and the file left as it was; in a browser a new file shows empty until closed. A copy runs where the files are, its contents never passing through the app: it reports its progress every 100 ms, never replaces an existing file, and, cancelled through its `Operation` or failed, leaves nothing behind. Mounts and the desktop's notices. `mount` asks the browser for a folder of the user's computer, from the user's press, and `unmount` lets it go; natively both fail with `Unsupported`. `App::on_files_changed`, `App::on_mounts_changed` and `App::on_open_files` hear of forge/web's `fs_changed`, `fs_mounts` and `open_files`, which the web backend queues as page events; natively they are not called yet. Types. `files::mime_type( name )` gives a file's type by shared-mime-info's `globs2`, with its weights, its case-sensitive globs and fnmatch's `*`, `?` and bracket classes, from the resource directory's `mime/` first, then the user's and the system's. `scripts/android-assets.sh`, and so `web-assets.sh`, now copies shared-mime-info's `globs2` and `subclasses` into the tree's `mime/` (`MIME_DIR` overrides where from), so a page has them. ltk::web::shell_request. A shell message that waits for forge/web's reply, such as `get_window_thumbnail`: sent as `shell_send` sends one and answered at once with forge/web's reply as JSON, or `None` before the app has connected, since a request is not kept for later, and when forge/web replies nothing. A shell's requests that wait for an answer natively got none in a browser. `shell.rs` parses a message once for both, and `send` returns forge/web's reply. Tests cover writing and committing, aborting, listing, ranges, removing, copying a tree with its progress, a cancelled copy, a copy that would replace, and the globs. `CHANGELOG.md` describes the changes. |
||
|
|
af4009374d |
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. |
||
|
|
cd1a233b2e |
A browser app joins the page's desktop: on a page with forge/web its main surface, overlays and subsurfaces are surfaces of that desktop, all painted through one WebGL context
The browser backend ran an app alone on a canvas over the page. An Eydos desktop in a browser needs crustace as its shell and other ltk apps as its windows on one page, managed by forge/web (forge's web/ directory), which publishes `globalThis.forgeWeb` and stacks, places and focuses the surfaces its clients ask for (forge/web/DESIGN.md). The desktop's client. On a page where `globalThis.forgeWeb` exists and `set_canvas` named no canvas, `platform/web/desktop.rs` connects as the app's `app_id` and asks for the main surface in the role the app's hooks give, as a Wayland compositor reads them: `shell_mode`, and for a layer `layer_anchor`, `layer_size`, `exclusive_zone` and `keyboard_exclusive`, for a window `app_id` and `window_size_hint`, and for `SessionLock` a full overlay that holds the keyboard. The app draws into the canvas forge/web gives and listens on the input element above it, takes its size and scale from forge/web's `configure` rather than from the window's `resize`, sends its input region in logical pixels when it changes, and leaves the keyboard focus to the desktop, which honours the surfaces that hold it exclusively. A `close` of the main surface reaches `App::on_close_requested`; on exit the surface is destroyed and the connection closed. The new `RunError::Desktop` says forge/web refused the main surface. Fullscreen inside a desktop does nothing yet. A page without a desktop, or one that named a canvas, runs the app as before. One WebGL context. A page holds few WebGL contexts, and the browser loses the oldest beyond them, while a shell has many surfaces. Every surface of an app now paints through one WebGL2 context on an `OffscreenCanvas`: the surface's FBO is blitted onto it, taken as an `ImageBitmap` and handed to the surface's own canvas, a `bitmaprenderer`, without a copy. Without WebGL2 each surface draws in software on a 2D context of its own. The context's loss is heard on the `OffscreenCanvas`, and now drops the GPU canvases of every surface, not only the main one's. Overlays. `platform/web/overlays.rs` keeps the app's overlays in step with `App::overlays()`, as the Wayland backend's `overlays_reconcile.rs` does with layer-shell: a new one becomes a layer surface or, for `Layer::Window`, a window, with its size and exclusive zone in logical pixels; a changed one gets an `update_role`; one that went is destroyed; forge/web's `close` of one delivers its `on_dismiss`. Popups anchored to a widget and the tooltip wait for a popup role forge/web does not have, and without a desktop an app still shows no overlays. 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`); Wayland's behaviour is unchanged. Subsurfaces. `platform/web/subsurfaces.rs` makes each of `App::subsurfaces()` a surface of the desktop sized to its parent with an empty input region, painted on one shared GPU canvas only when it appears, its size changes or its `content_version` does, its frame then staying on its own canvas, and otherwise moved with an `update_role`, which the browser composes without painting. `App::on_subsurface_committed` follows each paint. A subsurface whose parent overlay was destroyed and made again is made again, since forge/web destroys a subsurface with its parent. `frame_presented` now knows whether a surface hosts subsurfaces, so while the app animates by subsurface motion only its frames keep coming without painting the parent, as Wayland keeps a parent's frame callbacks. Painting a view with no focus, hover or press on a canvas of its own, which both backends' subsurfaces do, moved out of the Wayland backend into `draw::paint_detached`. Input per surface. `input::listen` takes the surface's `SurfaceFocus`, which pointer, touch and keyboard focus events carry; a surface's blur no longer takes the keyboard from the surface focused after it. Listeners remove themselves when dropped, and a surface's live in its `WebSurfaceState`, so they go with it. The frames owed at the next animation frame are a list of surfaces instead of one, which with several surfaces left the main one waiting on a frame that never came. `examples/web_shell.rs` is a shell for the desktop: a background, a bar in the top layer with an exclusive zone, a panel that takes the keyboard, and a card on a subsurface that slides. `make web-example` builds it with web_hello (`WEB_EXAMPLES`); forge/web's `make hello` serves both at /shell.html. web-sys gains `OffscreenCanvas`, `ImageBitmap` and `ImageBitmapRenderingContext`. `CHANGELOG.md` describes the change. Not done. Popups and the tooltip in a desktop, overlays and subsurfaces without one, fullscreen and window moves through forge/web, and the cursor's shape over an overlay, which is still set on the main surface's element. The offscreen canvas is resized for each surface it paints, which reallocates its drawing buffer when the surfaces differ in size. There are no tests for the browser backend. |
||
|
|
20dc21448e |
A browser backend: ltk runs on a page's canvas through WebGL2, on a core that builds for wasm32, and a surface's scale may be fractional
ltk ran on Wayland and Android only. Running crustace and its apps in a browser needs, first, ltk apps running there; a window manager for the page is the next step. The runtime was already split from Wayland by the platform layer, so what kept it out of a browser was what the core and the runtime took from the operating system: calloop's epoll loop, EGL, a monotonic clock, a file system, threads and environment variables. The core builds for wasm32-unknown-unknown. `ltk::ChannelSender` is ltk's own type instead of a re-export of calloop's `Sender`, with the same `send` and `Clone`, so apps that only send and clone it build unchanged. The runtime arms its timers and channels through `event_loop/sources.rs` instead of a calloop `LoopHandle`: calloop's natively, and in a browser a queue the backend runs, which takes the due timers out, runs them on `AppData` and hands them back, and does not arm again one removed meanwhile. calloop, khronos-egl and raw-window-handle are dependencies off wasm32 only, where `egl_context.rs`, `UiSurface::try_new_gles` and `from_current_gles_loader` and the native GL names of `BorrowedGlesTexture` are left out; `sub_canvas` clones the uniform locations, which are `Copy` natively and only `Clone` on wasm. The monotonic clock is `crate::time::Instant`, `web_time`'s on wasm32, where `chrono` takes its `wasmbind` feature; the one subtraction that could go below the clock's origin, a surface's first `last_draw`, saturates. Themes, fonts, keyboard layouts and images are read through `vfs.rs`, the file system natively and on wasm32 files held in memory, so a wallpaper or an `Image::from_path` is now decoded from the file's bytes and its format told from its contents rather than its extension. The text-scale watcher, which runs gsettings on a thread, is left out on wasm32, and the built-in keyboard takes its language from `platform::language()`, `LANG` or the page's. criterion is a dev-dependency off wasm32 only, since rayon does not build there. The browser backend is the `web` feature, `platform/web/`, with wasm-bindgen, wasm-bindgen-futures, js-sys and web-sys. `build.rs` refuses it with another backend or for another target. A page's main thread must not block, so `ltk::run` sets the app up and returns, and the browser drives the loop: `AppData` lives in a closure a thread-local loop holds, which a `setTimeout` runs for messages, timers and page events and a `requestAnimationFrame` runs to draw. Event listeners and `ChannelSender::send` only queue and schedule, never touching `AppData`, so nothing they set off runs inside 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, its frame kept in its FBO, or without WebGL2 through `putImageData`. A lost 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. The main surface's logical size is the canvas's CSS size; the window's `resize` refits it, and on ltk's own canvas a hidden element padded by `env( safe-area-inset-* )` gives the insets. Input comes from Pointer Events, a mouse as the pointer and fingers and pens as touches, the wheel, in detents when it scrolls lines, and keys, whose keysym comes from `KeyboardEvent.key` and code from `.code` as the Linux input code Wayland reports; keys repeat on the browser's clock, AltGr types its character, and on macOS Command is Ctrl. ltk takes every key the browser lets a page have, and while fullscreen those `navigator.keyboard.lock()` adds in Chromium. 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 with a Ctrl+V of its own. The cursor's shape is the canvas's CSS `cursor`. The built-in keyboard serves text where the page is used by touch. Panics go to the browser's console. When the app exits, the listeners are removed, the canvas receives an `ltk-exit` event and the app is dropped. The new `RunError::NoDocument` is returned where there is no page, as in a worker. Resources are the tree an Android package carries. `scripts/web-assets.sh` builds it through `android-assets.sh`, and `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 read through `resources::read_font`, so a path that does not exist is found by file name in the resource directory's `fonts/`, where a page bundles them. A surface's scale may be fractional. `SurfaceState::scale_factor` is an `f32`, which `Platform::prepare_canvas`, `Presentation::scale` and the paint path take as it is; a surface's physical size is its logical size times the scale, rounded once, and `buffer_scale()` gives the whole number Wayland's buffer scale and regions need. Wayland and Android still set whole scales, so nothing changes there; the browser backend sets the page's `devicePixelRatio`, so the canvas holds the display's own pixels at any zoom. `App::on_scale_changed` keeps its `u32`, the scale rounded, and `App::on_resize` gives the exact physical size. The fallback-theme banner placed its text as if `draw_text` took the top of the line rather than its baseline, so most of the text fell above the banner, out of the surface; it is now centred as the title bar's is. Building. `make web-check` typechecks the backend. `make web-deps` installs Debian's std for wasm32 and wasm-bindgen-cli, whose version must be the one of the wasm-bindgen crate in `Cargo.lock`: the crate is pinned to 0.2.117, the last whose CLI builds with Debian's rustc 1.85, and since the lock the CLI was published with needs a newer rustc and `cargo install` ignores rust-version when it resolves, the CLI is built from its sources with a lock resolved for the rust-version it declares, into `target/tools`, where the Makefile finds it. `make web-assets` builds the resources with the in-tree default theme and the fallback fonts `WEB_FONTS` names. `examples/web_hello.rs`, a counter and a text field, and `web/index.html` are the smoke test: `make web-example` builds them into `target/web/` and `make web-serve` serves them. `make install` now copies `web/` into the registry. `README.md`, `docs/architecture.md` and `CHANGELOG.md` describe the backend, and `docs/cookbook.md` points to `ChannelSender`'s new home. Not done. In a browser there are no overlays, secondary surfaces or subsurfaces, no dead keys or input methods, no accessibility tree and no primary selection, and a canvas that changes size while the window does not is not refitted. Pasting relies on the browser sending `paste` to a focused canvas, which Chrome and Firefox do. The page fetches its whole resource tree, about 7 MB, before the app starts. There are no tests for the browser backend, `Sources`' queue or `vfs.rs`'s in-memory files, and fractional scales may leave one-pixel artefacts where edges fall between pixels. |
||
|
|
cacf871ddd |
An Android backend, a platform layer that keeps Wayland out of the runtime, the built-in keyboard for an app's own text input, and APKs packaged with Debian's own tools
ltk only ran on a Wayland compositor: the runtime, the input handlers and the draw paths named smithay-client-toolkit's types throughout, so no other windowing system could be added without touching all of them. Prompt, the Eydos terminal, has to run on an Android phone, with its sessions and its sharing, and needs a toolkit that runs there. The platform layer. Input reaches the runtime as platform-neutral events: `input/event.rs` defines `KeyInput`, `PointerInput` and `TouchInput`, and the pointer, touch and keyboard handlers take them through a few entry points (`pointer_input`, `touch_input`, `key_press`, `key_release`, `key_repeated`, `modifiers_changed`, `keyboard_enter`, `keyboard_leave`, `set_repeat_info`). The sctk handler implementations moved to `platform/wayland/input.rs`, which records the serials later requests need, resolves surfaces and translates. What the runtime asks of the windowing system (moving, resizing, minimising and fullscreening the window, a bare frame request, the cursor's shape, the input method, publishing the clipboard and the primary selection, preparing a canvas, presenting it and its damage) forms the `platform::Platform` trait, with its surface type as `Platform::Surface`. The protocol state moved from `AppData` into `platform::wayland::WaylandPlatform`, held as `AppData::platform`, and each surface's Wayland half (its kind, SHM pool, EGL window surface and what the compositor was last told) from `SurfaceState` into `WaylandSurfaceState`, held as `SurfaceState::platform`. The compositor, shell, seat and output handlers, the data device, the primary selection, the run loop, overlay and subsurface reconciliation, session management, output probing and window fitting moved from `event_loop/` to `platform/wayland/`. Drawing is split from presenting: `draw/paint.rs` paints a surface in full or under its dirty rects, the same for the software and GPU canvases, in place of the four near-identical paths of `draw/software.rs` and `draw/gles.rs`, and the backend presents it (`platform/wayland/present.rs`, which also took the opaque and input regions and `pick_shm_format`). The runtime is generic over the platform, `AppData<A, P: Platform = NativePlatform<A>>` and `SurfaceState<Msg, S = NativeSurface>`, resolved at compile time. The backend-neutral half of a loop iteration is `event_loop/step.rs` (`process_messages`, `draw_due`, `next_wakeup`, `before_draw`, `after_draw`), and `event_loop/setup.rs` builds the state and the event sources every backend has; the accessibility tree and its actions moved to `event_loop/a11y.rs`. `platform/mock.rs` is a platform that logs what is asked of it, and `event_loop/tests.rs` drives the runtime on it through input, update, painting and presentation. The behaviour on Wayland is unchanged, except that a frame whose presentation fails leaves the next one to repaint in full. `ltk::Keysym` now comes from `xkeysym`, the crate sctk re-exported it from, so it is the same type. Features. `wayland`, on by default, brings the Wayland backend and its dependencies, now optional, AT-SPI2 accessibility among them. `android` brings android-activity 0.6, ndk 0.9 and jni 0.21. A new `build.rs` refuses both at once and sets `cfg( backend )` when one is on, which builds the runtime behind `run` and `try_run`. Without a backend only the runtime-free core builds: `UiSurface`, widgets, layout, theming and rendering. The Android backend. `ltk::run` runs inside a NativeActivity once `android_main` has handed its `AndroidApp`, re-exported as `ltk::AndroidApp`, to the new `ltk::set_android_app`; without it `run` fails with `RunError::NoAndroidApp`. The first call in the process sends standard output and error, panics included, to logcat, sets `HOME` to the app's internal storage and, unless set, `SSL_CERT_DIR` to Android's CA certificates and `LANG` to the device's language, and unpacks the bundled resources; every call keeps the latest activity, since Android starts the next one in the same process. The backend draws through EGL on the activity's window, or in software into a locked window buffer, and sizes the main surface at the display's density in whole multiples of 160 dpi. Fingers and styluses become touches, a mouse the pointer, and key events keysyms and text through the device's character map. calloop's descriptor rides on the activity's `ALooper`, so the app's channel and timers keep working. Text is typed on ltk's own keyboard, since a NativeActivity gets only key events from the system's. Through JNI on the activity (`platform/android/java.rs`): copies go to `ClipboardManager` and its text is read back whenever the app gains the focus; back with nothing left to go back to minimises the app with `moveTaskToBack` or closes it, as `App::back_at_root` says, which the new `Platform::back_leaves` decides per backend; and the window's insets come from `getRootWindowInsets()` on Android 11 and later, the status bar and any cutout on top and only bars that take taps on the other edges, so there is no gap above the view nor below it in gesture navigation, and a bar of buttons still keeps its room; earlier releases fall back to the content rect. When the app's last surface closes, ltk calls `finishAndRemoveTask` and waits for the activity to be destroyed, since a second activity started in a process whose `android_main` has returned never came up; an app should end the process after `run` returns. `ltk::keep_running( Some( text ) )` starts a foreground service with `text` on an ongoing notification, so Android does not freeze the app once it leaves the screen, and `None` stops it. Surface insets. `SurfaceState::insets` holds the logical pixels the system covers along each edge; the view is laid out inside them while the background fills the whole surface, and positions stay absolute, so hit testing is unchanged. The built-in keyboard is placed inside them too, above a bar of navigation buttons. On Wayland they stay zero. Resources in a package. `ltk::set_resource_dir( dir )` names an app's own copy of ltk's resources and `ltk::resource_dir()` returns it, so an app also finds its own bundled files there. Its `themes/` is searched after the user's themes and before `/usr/share/ltk/themes`, its `keyboards/` before `/usr/share`, and a font a theme names by an absolute path that does not exist is read from its `fonts/` by file name, so a theme written against Debian's paths works unchanged. On Android `set_android_app` unpacks `assets/ltk/` into the internal storage and uses it as the resource directory; `assets/ltk/MANIFEST` lists the tree, since the asset manager cannot list subdirectories, and its first line, a hash of the contents, decides whether to unpack again. On Android the per-script fallback fonts come from `/system/fonts` and the primary face is the embedded Sora. APKs. `scripts/android-assets.sh` builds that tree from installed themes, without their cursors, with the fonts each `theme.json` names and ltk's keyboard layouts. `scripts/android-apk.sh OUT.apk LIB.so PACKAGE LABEL [ASSETS_DIR]` packages an app built as a `cdylib` with Debian's aapt, zipalign and apksigner, with no Gradle and no SDK download: it writes the manifest, with the library loaded as `libmain.so`, optional permissions and a theme without an action bar, which the content rect would otherwise leave room for; links it against `android-framework-res`; adds the assets and the library; aligns and signs, creating a debug keystore when there is none. `ICON` takes an SVG, rasterised with `rsvg-convert` for every density, or a PNG. `KEEP_ALIVE=1` compiles `java/net/liberux/ltk/KeepAlive.java` with javac and `dx` from art-standalone against the platform 34 `android.jar`, against which aapt then links too, since Debian's framework-res predates the `specialUse` service type, and declares the service with `KEEP_ALIVE_USE` as its reason and the permissions it needs. `examples/android_hello.rs` is the smoke test: `make android-check` typechecks the backend with Debian's rustc, `rust-src` and NDK through `-Zbuild-std`, `make android-apk` and `make android-install` package and install the example, `make deps` and `make android-deps` install what the targets need, and `make install` now copies `examples`, `scripts`, `java` and `build.rs` into the registry. The built-in keyboard for 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`, so 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, it only served ltk's text fields, so such an app had no keyboard where the compositor offers no input method. Clippy. `make clippy` is clean again: derived `Default`s replace the hand-written ones, `div_ceil`, `then_some`, struct update syntax for strokes, the map entry API in the text renderer, function paths in place of closures in the examples and tests, and fewer casts and explicit lifetimes. `README.md`, `docs/architecture.md` and `CHANGELOG.md` describe the features, the backend, the events and their units, and what stays with a backend. Not done. On Android there are no overlays, secondary surfaces or subsurfaces, no dead keys from a physical keyboard and no saved state for the activity. Why a second activity in the same process never comes up was not found. The keep-alive service does not ask for the notification permission, so on Android 13 and later its notification may not show until it is granted in the app's settings. |
||
|
|
2af281520b |
External text input for fields ltk does not own; TextEdit::color and TextEdit::on_focus
Two additions driven by apps that host another UI inside an ltk surface: trust, whose pages have their own form fields, and rustdroid, which projects Android views into ltk widgets. External text input. ltk only ever enabled `zwp_text_input_v3` for its own `TextEdit`s, so nothing told the compositor that an editable inside an embedded web view had the focus: the on-screen keyboard never came up inside a page, which makes a browser unusable on a phone without a hardware keyboard. Three hooks on `App` close that. `external_text_input() -> Option<u64>` asks for the input method on behalf of such a field: while it returns `Some( serial )` and no ltk text widget holds the focus, the runtime keeps a text-input enabled, and the compositor raises its keyboard. `on_external_text_commit( text )` and `on_external_text_delete( before, after )` receive what the input method sends, the latter in UTF-8 bytes as the protocol counts them. Keys such as Backspace and Enter keep arriving as key events, which hosts already forward. The request is a serial rather than a `bool` so that it can be renewed. A compositor that folds its keyboard while the field keeps the focus, which forge does on back, has nothing to observe when the user taps that same field again, because the text-input never stopped being active; a change of serial makes the runtime send `enable` again through `reenable_text_input`, the same signal ltk now sends for its own fields. The host chooses what bumps it; trust counts taps on the page, so dragging to scroll does not bring a folded keyboard back. `AppData::sync_external_text_input`, in `text_editing/ime.rs`, is polled after every batch of `update`s next to `sync_fullscreen`. An ltk text widget always wins: while one has the focus the function only clears its record, and `set_focus` manages the text-input as before. When that widget loses the focus `set_focus` destroys the text-input, and the next poll finds the request still standing with no text-input and enables a new one, so the two owners hand it back and forth without either knowing about the other. The `zwp_text_input_v3` dispatch routes `commit_string` and `delete_surrounding_text` to the app only when the external request is held and no ltk text field is focused; that second check covers the one iteration between a widget taking the focus and the next poll. The empty-string filter on `commit_string` is folded into the `Option` it already lived in. `TextEdit::color( c )`. The colour of a field's value always came from the theme. A field placed over a surface the theme knows nothing about, an Android `EditText` with its own text colour on its own background, could end up unreadable. `color` sets the value's colour in both draw paths, single-line and multiline; the placeholder keeps the theme's, and a field without it draws exactly as before. `TextEdit::on_focus( msg )`. `App::on_text_input_focused( bool )` says that some text field is focused, not which. An app that mirrors focus into another model needs to know which: rustdroid keeps Android's single focused view in step, and could only learn the field from its first `on_change`, so `View.isFocused()` stayed false until the user typed. A `WidgetId` cannot carry that, being a `&'static str` while such handles are created at run time. A per-widget message can, since the app builds it and puts in it whatever it needs. It rides the `WidgetHandlers::TextEdit` snapshot, and `set_focus` pushes it when the focus actually moves to the field, from a press, Tab or `App::take_focus_request`, and not again for a press on the field that already holds it. Every other match on that handler variant uses `..`, so the new field touches its definition, its clone and the snapshot in `element.rs`. Documented in the `App` rustdoc, the trait surface list of `docs/architecture.md`, the `text_edit` entry of `docs/widgets.md` and the changelog. Neither path has a test: both need a live Wayland connection and a compositor to exercise. |
||
|
|
96a9e88051 |
Back as a navigation request: App::on_back, BackAtRoot, NavStack and docs/navigation.md; renew the text-input enable on a press; fit an oversized window_size_hint to the screen
Three changes that came out of making the shell's back button work across Eydos the way it does on Android. The first is the feature; the other two are gaps it exposed. Back is no longer Escape. forge turns its back gesture and button into the `XF86Back` key for the focused client, and ltk treated that key as an alias of `Escape`: close transient UI, else drop the focus and reach `on_key_with_modifiers`. That is "cancel", and it is all an app could get: nothing navigated, and nothing ever left the app. `XF86Back` is now a back request with its own order, one layer per request, top-most first: transient UI the runtime owns (open xdg-popup overlays, the built-in context menu, a text selection, the top-most pressable carrying an `on_escape`, i.e. a dialog's cancel), then text-input focus, then the new `App::on_back`, then `on_key_with_modifiers( XF86_Back )` so an app that bound the key itself keeps receiving it (crustace does, for its launcher), then `App::back_at_root`. The first step is what Escape already did; it moves out of `handle_key_escape` into `AppData::dismiss_transient` in the new `input/keyboard/back.rs`, which both paths call, so Escape behaves exactly as before. Text focus comes before the app on purpose: with a keyboard covering half the screen, back means "put the keyboard away", and navigating instead would discard what the user was typing. A compositor that folds its keyboard itself, as forge now does, only lets the next request through, which makes this step the fallback for the ones that do not. `App::on_back` returns a `BackResponse`: `Message( msg )` with the message that undoes the top-most piece of navigation state, `Unhandled` at the root, or `Consumed` for a host whose back stack lives elsewhere and cannot be inspected ahead of time. `Consumed` exists for rustdroid: an Android app keeps its activities, fragments and back callbacks in its own code, so the host can only hand the request over. `App::back_at_root` then picks `Minimize` (the default), `Close` (through `on_close_requested`, which can still veto) or `Ignore`. Minimising is what a root activity does on Android since version 12 and what suits an app the user comes back to: a browser keeps its tabs, a call goes on. It is also the single point where the two stacks meet: the navigation stack belongs to the app's model, the task stack to the compositor, and `xdg_toplevel.set_minimized` is the one standard request between them, so no custom protocol carries navigation state out of the process and clients that are not ltk apps keep working. Only `ShellMode::Window` surfaces react at the root; a panel or a lock screen that minimised itself would be a bug. Three consequences of back being a navigation request rather than input: the key leaves the default `App::key_repeats` set, since a held button would empty the whole stack; it is no longer reported through `App::on_raw_key`, so a host forwarding raw keys into an embedded web view or guest toolkit stops leaking it as a keystroke; and the mouse's back button (`BTN_SIDE` / `BTN_BACK`) enters through the same `handle_back`. `focused_text_input` becomes `pub( crate )` for the new module. The answer is derived, not recorded, and that is the design decision worth writing down. The runtime keeps no navigation stack of its own. Its entries would have to be messages, which are not serialisable (the trait puts no serde bound on `Message`, by design), so it could not survive a relaunch; and a list of "things to undo" drifts the first time the user leaves a screen some other way, through its own close button or a link, leaving a back press that does nothing visible. An answer computed from the model cannot disagree with it. This is also what ties back to the session persistence added earlier: navigation state is both what `on_back` reads and what `save_state` should persist, and keeping the two the same is what makes back retrace the same places after a session restore or a crash, with nothing extra to serialise. Persist less and a restored app opens on the right screen with nothing behind it; persist more and it restores a menu the user had closed. `NavStack<R>` packages that rule for the common case, an app made of screens. It is a plain value the app keeps in its model, never seen by the runtime: `push`, `pop`, `replace` (the sideways move back should not retrace), `pop_to_root`, `reset`, `current`, `current_mut`, `iter`, `depth`, `can_pop`, and `back( msg )`, which answers `on_back` directly. It is stored as a root plus the routes above it rather than as a `Vec`, so "never empty" holds by construction, including after deserialising an old or damaged state file, and it derives serde whenever `R` does. Six unit tests cover it, a serde round trip among them, and its rustdoc example is a complete `App` where one object answers `on_back`, `save_state` and `restore_state`. Documentation, since the point is for app authors to take navigation state into account. The new `docs/navigation.md` is the reference: transient UI versus navigation state and the test that separates them, the back order and the reason for each step, why the answer is derived, how it lines up with session state, `NavStack`, apps that embed a web view (whose page history is navigation state too, to be persisted with ltk-webkit's `session_state`), hosts with a foreign back stack, the root, who owns which stack, and an author's checklist. `architecture.md` gains a group in the trait surface and a paragraph in the state patterns, `onboarding.md` and the README index link to it, and in the cookbook the multi-screen recipe now uses `NavStack` with `on_back` and persists the stack without serde, which keeps it compiled by the Markdown doctest harness (that harness links `ltk` alone, so the serde example in `navigation.md` is marked `ignore` and compiled as `NavStack`'s own doctest instead). `BackResponse`, `BackAtRoot` and `NavStack` are exported at the crate root and in the `ltk::window` facade. Behaviour change for existing apps, also recorded under Changed: a window app that implements neither `on_back` nor the key is now minimised on back at its root, where nothing happened before; `BackAtRoot::Ignore` restores that. With a text input focused, back only drops the focus and the app sees the next request, where it used to get the key in the same press. Renew the text-input enable on a press. Moving the focus between two text fields, or pressing the one already focused, only committed the refreshed content type on the existing `zwp_text_input_v3`. `activate_text_input` now sends `enable` first, as GTK does when a focused entry is clicked. A compositor that folded its on-screen keyboard while the field kept the focus, which is what back now does, has no other way to learn that the user wants it again: the text-input never stopped being active, so there was nothing for it to observe. ltk sends neither surrounding text nor a cursor rectangle, so the only state `enable` resets is the content type set right after it. Fit an oversized `window_size_hint` to the screen. The hint is pinned as `min_size == max_size` before the first commit, when no output is known yet, so an app asking for a phone-shaped 720x1440 window on a 1080p desktop got a toplevel taller than the screen; rustdroid carried a dead branch that tried to avoid this from a screen size nothing could give it. The first configure is the earliest point the hint can be checked, and the protocol has the right input for it: `xdg_toplevel.configure_bounds`, exposed by sctk as `WindowConfigure::suggested_bounds` and ignored until now. The new `event_loop/window_fit.rs` holds `fit_within`, which scales a size down keeping its aspect ratio until it fits and leaves one that fits untouched (four unit tests), and `AppData::fit_size_hint`, run once behind the new `pending_size_hint_fit` latch: it uses the compositor's bounds when sent and otherwise 90 % of the first output's logical size, since panels and decorations are unknown from the client side. When the hint has to shrink, `min_size` (and `max_size` for a non-resizable window) is re-pinned to the fitted size, which is kept in `fitted_size_hint` and replaces the app's hint as the fallback for later configures that carry no size; without that the window would grow back on the next one. On that one configure the fitted size wins over the compositor's `new_size`, which was derived from the oversized pin. `App::window_size_hint` documents the rule with the worked example (720x1440 opens as 486x972 on a 1080p output). The back order needs a live Wayland connection to exercise and has no test; the unit tests cover `NavStack` and `fit_within`. |
||
|
|
2252a63f16 |
App: claim raw touch per press with claims_raw_touch_at, and toggle fullscreen at runtime with fullscreen
Two additions to the `App` trait, both needed by an application that embeds a self-contained input consumer next to regular widgets. The case at hand is trust, the Eydos browser: an ltk-webkit `WebView` below a toolbar built from stock widgets, with an F11 fullscreen toggle. Raw touch per press. `App::claims_raw_touch` is all or nothing: once it returns `true` every finger, the primary included, bypasses the gesture machine, so widget presses, taps and swipes stop working on touch. That suits an app whose whole surface is one WebView (the eydos-settings portal), but not one that shows the view under its own chrome: claiming raw touch made the address box and the toolbar buttons dead to the finger, and not claiming it fed the page a mouse-like primary slot, so a drag selected text instead of scrolling and pinch-zoom was impossible. The new `App::claims_raw_touch_at( x, y )` is consulted on `wl_touch.down` with the press position in physical pixels of the main surface; a finger landing where it returns `true` goes through the raw `on_touch_down` / `on_touch_move` / `on_touch_up` stream while fingers landing elsewhere keep driving the gesture machine. The default is `false`, so existing apps are unaffected, and it is only consulted for the main surface: overlays have their own coordinate space and the hook carries no surface argument. The implementation reuses the auxiliary-slot path instead of adding a second notion of raw ownership. In `TouchHandler::down` the position is now resolved before the takeover check; a claimed finger is recorded in `touch_slots` without becoming `primary_touch_id`, which is precisely the state of a second finger today, so `motion`, `up` and `reset_touch_state` already route it to the app and report its release point with no change. A claimed press skips the plain-press takeover (it must not commit a tap in progress on the chrome) and calls `set_focus( focus, None, qh )`, mirroring what a pointer press outside any widget does: without it a focused text field would keep the text-input protocol, the on-screen keyboard and the key stream after the user had moved on to the page. `pointer_pos` is still assigned after the takeover, since `release_primary` uses it as the release point of the finger being committed. Runtime fullscreen. `App::start_fullscreen` only covers the first commit; there was no way to enter or leave fullscreen afterwards. `App::fullscreen() -> Option<bool>` declares the desired state and is polled after every batch of `update`s, next to `requested_exit`. `AppData::sync_fullscreen` compares it with `applied_fullscreen`, the last value sent, and calls `xdg_toplevel.set_fullscreen( None )` or `unset_fullscreen` only on a change; `None`, the default, leaves the window state alone. `applied_fullscreen` is seeded with `start_fullscreen()`, so an app that starts fullscreen and reports `Some( true )` does not send a redundant request, and one that reports `Some( false )` is not pushed out of a fullscreen it asked for a moment earlier. It is a no-op for layer-shell and session-lock surfaces, which have no toplevel. The `configure` handler now also zeroes `titlebar_height` while `WindowConfigure::is_fullscreen()` holds, as it already did for server-side decorations. The built-in titlebar was otherwise painted over a fullscreen surface, which is what the `start_fullscreen` doc comment described as happening "unless the app opts out" without there being a way to opt out. The height follows the compositor's reported state rather than the app's request, so it stays correct when the compositor refuses the request or leaves fullscreen on its own. Both hooks are documented on the trait and listed in the changelog under Unreleased. |
||
|
|
58b8dd7b90 |
Focus requests reach widgets clipped by a scroll viewport, and stop retrying after a few frames
A scroll viewport keeps in `widget_rects` only the widgets at least partly inside its visible area (`draw/layout.rs`, the translation of the sub-canvas rects to surface space). A focus request whose target sits below the fold therefore never finds it: `App::take_focus_request` was honoured only for widgets already laid out on screen. The Wi-Fi page of settings is one such case by construction: tapping a secured network inserts the password form under the row and asks for focus on the field, so for a row in the lower part of the list the field is born outside the viewport and the request misses. Worse, the miss branch parked the id in `focus_retry`, marked the view dirty and requested a redraw on every iteration, so the application rebuilt and repainted at the compositor's frame rate for as long as the widget stayed hidden. The layout pass now records the widgets it drops for carrying a `WidgetId`: `DrawCtx::scroll_clipped` holds `(id, scroll flat_idx, rect)` with the rect in the scroll's content coordinates, the same frame the navigable-item list uses, and `draw_frame` hands it to `FrameState`. When a focus request misses, `AppData::reveal_clipped_widget` looks the id up in that list on the main surface and the overlays, finds the viewport in `scroll_rects`, and moves its offset on the axes the scroll allows so the rect lands inside; the next frame lays the widget out and the retry focuses it. Nested scrolls resolve one viewport per frame. The rule that computes the new offset is `scroll::offset_into_view`, extracted from the keyboard navigation in `input/keyboard/nav.rs`, which now calls it instead of carrying its own copy; unit tests cover the three cases. `focus_retry` carries a budget of drawn frames (`FOCUS_RETRY_FRAMES`, four), decremented only on iterations that actually drew, so a request made while a frame callback is pending does not lose its chance. A fresh request from the application replaces a pending retry with a full budget. When the budget runs out the request is dropped, which ends the redraw loop for a widget that never appears. Documented in the `take_focus_request` doc comment, the cookbook and the changelog; Debian revision 0.3.0-3. |
||
|
|
5b8ae35830 |
[ltk — src/input/gesture/mod.rs, src/input/gesture/tests.rs, src/widget/handlers.rs, src/input/keyboard/dispatch.rs, src/render/image.rs, docs/architecture.md, CHANGELOG.md] Keep a tap on a button while the finger rolls inside it, treat XF86Back like Escape, and convert ARGB8888 by whole pixels
Any travel past the slop turned a press into a vertical or horizontal drag and its release skipped the button, so a thumb that rolled on a large key lost the tap. `GestureState::on_move` now stays idle while the pressed widget fires on press (the new `WidgetHandlers::has_press`) and the finger is still inside its rect; the swipe machinery takes over from the original press point once the finger leaves. Presses inside a scroll viewport are unaffected, since a list of full-width rows is scrolled by dragging on them. Two tests cover the tap that survives motion inside the button and the swipe that arms once the finger leaves it; the architecture notes and the changelog describe the rule. The compositor now sends XF86Back for the Android back gesture, an inward swipe from either side edge. With a text input focused that keysym fell into `handle_key_default` and was swallowed, so an app never saw it. `XF86Back` now dispatches through `handle_key_escape`, so every ltk app gets the back gesture with Escape's layering: open popups, then the context menu, then a selection, then a dialog's cancel, and finally the focus drop and `on_key_with_modifiers` with the keysym, where an app can act on it. `SoftwareCanvas::write_to_wayland_buf` swapped red and blue with a byte-by-byte loop, unrolled by hand over sixteen bytes. It now reads each pixel as a `u32`, exchanges the two channels with masks and shifts, and leaves the vectorisation to the compiler. Compositors that offer `ABGR8888` never took this path. |
||
|
|
75be800390 |
input, render: keep fast touch typing whole, fire pattern nodes once, and make the software image path cheap
Two ways a tap was lost on a phone. The gesture machine serves one finger: a second finger landing while the first was still down went to the auxiliary slot and never reached it, so with two thumbs every overlapping tap on a keypad vanished. 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 on_touch_up, and a second finger during a pattern stroke or a slider drag stays out as before. The touch-up body moves to release_primary so both paths share it. The other loss needed one finger: the 8 px travel that turns a press into a scroll, swipe or drag, and the 6 px stray that cancels a touch long-press, were compared against physical coordinates, so at buffer scale 2 they were 4 and 3 logical px and an ordinary thumb tap became a vertical drag whose release skipped the button. SwipeConfig carries the surface's scale and the machine reads the thresholds through slop() and stray(), which are logical. Pattern grids fired on_node on every motion event within a node's snap radius, and the Slider move outcome requested a repaint each time, so drawing a pattern rebuilt and re-rastered the surface at the touchscreen's motion rate. The machine remembers the node under the finger (last_pattern_node) and emits only on change; the handlers split pattern_node_at from pattern_msg, with pattern_node_msg kept on top of them; a move outcome without a message no longer requests a redraw. SoftwareCanvas::draw_image_data premultiplied the whole source in floating point on every call, so a full-surface backdrop paid a pass over every pixel per frame before the blit. Premultiplied copies now live in a 32 MiB least-recently-drawn cache keyed by size, content fingerprint and alpha, the key the GLES texture cache already used; fingerprint_rgba moves to render::helpers and both backends share it. The entry records whether the source was fully opaque, and an opaque image drawn 1:1 with no clip and full alpha is copied row by row into the canvas instead of going through tiny-skia's bilinear pipeline. docs/architecture.md describes the primary-slot hand-over, docs/widgets.md the once-per-node contract, and the CHANGELOG lists the five fixes. |
||
|
|
c9ae5438f8 |
pattern_grid: explicit side through PatternGrid::size
The grid's preferred size was the theme's fluid constant (264 px at the reference surface) capped by the width the parent allocates, so a caller could only shrink it: a lock screen that wants the grid to span most of a phone's width had no way to ask for it, and a container's max_width did nothing past the default. PatternGrid gains a size field with a size( impl Into<Length> ) builder; preferred_size resolves it through Canvas::resolve_geom like the other geometry overrides and still caps it at max_width, and map_msg carries it across. Without the builder the fluid default applies as before. docs/widgets.md describes the builder next to hide_stroke and error; CHANGELOG lists it under Unreleased. |
||
|
|
3046d86337 |
Layer::Window overlays: an OverlaySpec can now be an ordinary xdg toplevel, named through App::overlay_title; showcase persists the last pressed button too
A shell built on ltk had no way to show something as a regular window: every overlay is a layer-shell surface, and layer-shell surfaces are either below all application windows or above all of them. crustace's "closing applications…" card ran into exactly that — on the overlay layer it covered the applications' own "save changes?" prompts, on the bottom layer it vanished behind their windows — and what that card wants is to be one window among the others: decorated by the compositor, stackable, listed in the switcher and the dock. `Layer` gains a `Window` variant. An `OverlaySpec` carrying it is materialised by the reconciler as an `xdg_toplevel` (sctk `Window`, server-side decorations) instead of a layer surface: `size` is its fixed size, applied through min/max size (a zero component falls back to a default, since a toplevel cannot "fill"); anchor, exclusive zone and keyboard exclusivity are ignored. The window's app_id is `App::app_id()` and its title comes from the new, defaulted `App::overlay_title( id )`, so the compositor's window lists show something meaningful. `WindowHandler::configure` and `request_close` now resolve which surface a window belongs to: the main window keeps its behaviour, an overlay window is configured through its own `SurfaceState` (with the window geometry set to the configured size) and the compositor's close button delivers the spec's `on_dismiss` instead of exiting the app. Layer-shell applications now bind `xdg_wm_base` when the compositor offers it, so they can open such a window later. Adding a variant keeps every existing `OverlaySpec` literal compiling; the new trait method has a default. The showcase example now also saves and restores `last_pressed`, bumping its private state format to version 2 with the note kept last so its line breaks survive. |
||
|
|
ccf07de593 |
Session management: xdg-session-management-v1 client, mandatory App::app_id / save_state / restore_state, runtime-managed state persistence and clean exit on signals (0.3.0)
Applications built on ltk had no way to come back where the user left them: the toolkit hardcoded `app_id = "ltk"` on every toplevel, never wrote anything to disk, and died on SIGTERM without a chance to save. This release gives the runtime the whole plumbing and asks each application only for the bytes worth keeping, in the spirit of Android's saved-instance state. The `App` trait gains three mandatory methods, deliberately without default bodies so every application states its position: `app_id()` (reverse-DNS, used for `xdg_toplevel.set_app_id`, the AccessKit application name and the state directory — the `app_id` element of the deprecated `window_config` tuple is now ignored and a one-time warning reports a mismatch), `save_state() -> Option<Vec<u8>>` and `restore_state(Vec<u8>)`. The bytes are opaque; the trait carries no serde bound. Their rustdoc is the contract: when the runtime saves, where the files live, when the bytes come back and when they do not, what must never go in them, and a worked serde_json example. The runtime persists under `$XDG_STATE_HOME/<app_id>/` (falling back to `~/.local/state`): `session.json` holds the compositor session id, a clean-exit marker and the writer's pid; `state.bin` holds the application bytes. Writes are atomic (temp file + rename, mode 0600, directory 0700) and best-effort. State is saved every 30 s when the bytes changed, once after the event loop exits (which covers `on_close_requested`, `requested_exit` and lost connections), and on SIGTERM/SIGINT — a calloop signal source, installed before any thread exists, now turns those into a clean exit of the loop instead of process death. `restore_state` runs synchronously in `try_run` before the window is created and before the first `view()`, and only when the process is relaunched as part of a session restore (`LTK_SESSION_RESTORE=1`, removed from the environment before the app can spawn children) or when the previous run left `clean_exit: false`; a plain launch starts fresh. A second concurrent instance detects the live pid and runs with persistence disabled rather than clobbering the first. The compositor side of geometry restore goes through `xdg-session-management-v1`. Neither wayland-protocols nor sctk ship generated code for it yet, so the XML is vendored under `protocols/` and `wayland-scanner` generates the client module in-tree (`src/protocol/`), resolving the crate names through sctk's reexports so the bindings stay on the crate instances sctk links. Before the first commit of a `ShellMode::Window` toplevel the runtime binds `xdg_session_manager_v1`, calls `get_session(reason, stored_id)` and `restore_toplevel(toplevel, "main")`; the three window-creation paths in `run.rs` are folded into one `make_window` helper so the attach always sits immediately before `commit()`. `created` persists the id, `replaced` destroys the objects and stops persisting. Compositors without the global lose only the geometry half. Layer-shell and session-lock surfaces skip the whole machinery. Every `App` implementor in the tree is updated: the twelve examples (`showcase`, `scroll` and `mini_shell` persist real state; the rest return `None`), both integration tests (`event_loop_flow` gains `save_restore_round_trip`), the in-source and markdown doctests, README, onboarding, cookbook (new recipe "Surviving relaunch: session state") and architecture docs, and the changelog. `src/session_state.rs` carries unit tests over a temporary state directory. `Makefile install` now copies `protocols/` into the cargo registry — without it downstream builds would fail inside the proc-macro — and `debian/copyright` covers the vendored XML. The trait change is breaking, hence 0.3.0. Also fixes the pre-existing `viewport_tests` module in `render/mod.rs`, which used `Length` without importing it and broke `cargo test`. |
||
|
|
1290f9400e |
row, button, widget: distribute a row's width shortfall instead of overflowing, elide button labels into the rect they are granted, icon_size takes a Length
Row deficit distribution (layout/row.rs). `Row` knew how to hand out leftover width but had no notion of a shortfall: `leftover` was floored at zero and every child was laid out at its preferred width, so a cluster wider than its rect simply overflowed — and symmetrically, because the no-spacer branch centres the block, which is why both end labels of a segmented control were clipped at once rather than only the trailing one. The shortfall now comes off the widest children first, by water filling: `width_cap` sorts the flexible widths and returns the largest per-child cap `c` for which `sum( min( w, c ) ) <= available`, so a long title absorbs the whole deficit while an icon button beside it keeps its size, and equal siblings — a segmented control — share it evenly. A uniform scale-down, which was the first attempt, was wrong precisely there: it thinned a header's back arrow along with the title it sat next to. Spacers and flex children stay out of the flexible set, so a pinned gap keeps the width it was given — an explicit spacer is a decision, not slack — and `Row::no_shrink()` opts a row out entirely, for strips deliberately wider than their viewport such as a carousel rail meant to be scrolled. `align_right` and the centring branch now measure the laid width rather than the preferred one, so a shrunken row is positioned against what it actually occupies. Button label elision (widget/button/mod.rs, widget/mod.rs, widget/list_item/mod.rs). Shrinking a row is only half the fix: a leaf handed less width than it asked for still painted its full string, so the deficit came out clipped instead of truncated. `draw_text_button` now elides the label against the rect the layout granted, less the horizontal padding, for all three variants. The truncation rule moves out of `ListItem`, where it was a private helper, into a single crate-internal `widget::elide` the two share. `Text` keeps its own inline copy for now: it measures through an optional font override that this signature does not carry, and folding the two together is a separate change. icon_size as a Length (widget/button/mod.rs). `Button::icon_size` was the one geometry setter that ignored the widget-scaling mode — it took a bare `f32`, pinned it, and used `0.0` as the "unset" sentinel. It now takes `impl Into<Length>` behind an `Option` and resolves through `Canvas::resolve_geom`, the way `height`, `width` and `font_size` already do. A bare number still means `Length::px`, so every existing call keeps its exact size; what it adds is `Length::widget( n )`, which follows the active mode the way stock icons do. Without it a back arrow pinned at 21 px sat next to a `list_item` chevron that fluid sizing had grown well past 21, and read as visibly smaller on the same row. |
||
|
|
530a5696b9 |
event_loop, slider, list_item: overlay exclusive zones follow the surface, on_release for sliders, list labels elide against the trailing slot
Exclusive zones (event_loop/overlays_reconcile.rs, event_loop/surface.rs). `OverlaySpec::size` is documented as physical pixels and converted to logical for `layer_surface.set_size` by dividing by the parent's integer scale; `exclusive_zone` sat right beside it in the same `LayerConfig` and was passed through raw, so at scale 2 the reserved band was expressed in a unit twice as coarse as the surface it is meant to match. Worse, `set_exclusive_zone` appeared exactly once in the whole crate — at materialize time — while the reconcile loop propagated later size changes through `last_requested_size`, so an overlay whose size kept being recomputed carried a reservation frozen at whatever the first frame produced. Crustace's dock is the visible case: it derives both numbers from the same `desktop_pill_height`, and with the zone stuck at the density-1 value (icon at its 40 px floor, 40 × 1.70 = 68 logical) while the surface grew to 87, a maximized window overlapped the top fifth of the dock — measured on a 1.75 output as 151 physical px of painted dock against a 120 px reserved band. The zone now goes through the same divisor as the size, with `-1` (ignore other zones) and `0` (reserve nothing) passing through untouched as the sentinels they are, and `SurfaceState` tracks `last_requested_zone` so the reconcile loop re-sends it whenever the spec moves, committing once for both. Slider::on_release / VSlider::on_release (widget/slider, widget/vslider, widget/handlers.rs, widget/element.rs, input/gesture). Sliders only had `on_change`, which fires on every motion event, so an app whose commit is expensive — a subprocess, a D-Bus round trip, a compositor reconfigure — paid for it per pixel of travel: dragging the text-scale slider in Eydos Settings wrote `gsettings` once per motion and swept the whole desktop through a repaint each time. The new builder fires once with the final value when the drag ends, leaving `on_change` to move the thumb and nothing else; `on_change` alone behaves exactly as before, and the mapped variant propagates through `map_msg` like its sibling. The handler snapshot carries the callback next to `on_change` and exposes `slider_release_msg`; the gesture machine's slider branch of `on_release`, which previously returned an empty event list, now resolves the widget through `find_widget`, recomputes the value from the release position with the same `slider_value_from_pos` the drag path uses, and pushes `ReleaseEvent::PushMsg` — the variant whose documentation already described "button press or final slider value on release". A unit test covers the new emission; the existing one asserting an empty release still holds, because it passes an empty widget list and the lookup finds nothing. ListItem elision (widget/list_item/mod.rs). The label and subtitle were painted with `draw_text` and no width budget, so a row title longer than its width ran under the trailing text or the disclosure icon instead of truncating. The trailing slots are now measured and positioned before the text is painted — their extent is what decides how much room the label has — and both lines are elided with an ellipsis against the space left over, accumulating per-character widths the way `Text` already did, with one icon gap kept between the text and whatever follows it. `Text` keeps its own inline copy of that algorithm for now; folding the two into a single crate-internal helper is the natural follow-up, and a precondition for teaching `Button` to elide once `Row` learns to distribute a width deficit instead of only leftover space. |
||
|
|
d9652dce98 |
accessibility text scale: global font multiplier synced to the desktop's text-scaling-factor
New set_text_scale / text_scale process global (clamped [0.5, 3.0]) multiplied into every resolved font size — Canvas::resolve_font for explicit Lengths and the Physical branch of font_px (the Fluid branch routes through resolve_font) — so the whole tree's text follows the accessibility "large text" factor while geometry stays untouched, mirroring GNOME's text-scaling-factor semantics. Unit test covers fonts-scale-geometry-doesn't. The run loop keeps the factor synced on its own: a watcher thread (event_loop/text_scale.rs) reads org.gnome.desktop.interface text-scaling-factor via gsettings get at startup and streams external changes from gsettings monitor into a calloop channel; on a change the loop stores the factor, invalidates the view caches and repaints main and overlays. Since fonts resolve at paint time nothing else needs rebuilding. Every ltk app tracks the settings slider live with zero app-side wiring, the same way GTK apps follow the key; missing gsettings degrades silently to a fixed 1.0. Embedders driving core::UiSurface (forge) call set_text_scale themselves — the multiplication only runs at widget font resolution, so raw Canvas::draw_text callers keep hand-computed sizes. architecture.md documents the multiplier in the font-space paragraph and CHANGELOG gains the entry. |
||
|
|
e343142347 |
viewport: local_viewport() opt-out of root-viewport inheritance; list_item: height/font_size builders
`Viewport::local_viewport()` resolves the child's viewport-relative (vw / vh / vmin) and fluid Lengths against the viewport's own rect instead of the root layout viewport the sub-canvas inherits since the fluid-resolution inheritance change. That inheritance is right for scroll-like clips (content renders the same size inside and outside), but it is exactly wrong for a fixed-size floating mini-UI — crustace's phone-shaped quick-settings pill pinned to a corner of a desktop-wide surface resolved its vw text and fluid stock geometry against the whole monitor, inflating the content past the pill's fixed clip and cutting off the bottom stripe. The flag pins the sub-canvas's layout viewport to its own size via the new crate-internal Canvas::set_local_layout_viewport, nested sub-canvases keep propagating the pinned value, and a unit test covers the override plus propagation. `ListItem::height( impl Into<Length> )` and `ListItem::font_size( impl Into<Length> )` mirror the Toggle / Radio height() builders: override the theme row height (floored at the label's rendered height so the text never clips) and the primary-label font size (subtitle and trailing keep their theme sizes), letting dense context menus trade the stock touch-target generosity for row density. Docs: widgets.md gains both builder sets, architecture.md documents the layout-viewport inheritance model next to the per-canvas density it parallels, the cookbook slide-in panel recipe notes when the pill needs local_viewport(), and the toggle / radio height() rustdoc demotes its private theme::HEIGHT link to a code span so cargo doc is warning-free again. CHANGELOG entries added. |
||
|
|
806dee5167 |
render, types, ci: resolution-time dp with per-canvas density, bounded GLES image cache, clippy gate, backend capability matrix
Length::dp no longer collapses to absolute pixels at construction: the design value travels in a new LengthBase::Dp variant and the density multiplication happens when the length is resolved. Previously dp( n ) baked in whatever density() returned at view-build time, so correctness across output changes depended on the view being rebuilt after set_density and in that order; now a density change is picked up by the very next paint with no reconstruction. Length::resolve keeps its signature (process density), and the new Length::resolve_with_density takes an explicit factor. dp becomes const in the bargain. Density also becomes overridable per canvas, the first step towards surface-local responsive state. SoftwareCanvas and GlesCanvas carry a density: Option<f32> analogous to the layout_viewport introduced for sub-canvas fluid resolution: None means "use the process global", Canvas::set_density pins a local factor, and sub-canvases inherit it. All canvas-routed resolution honours it — geom_px / font_px for stock-widget design pixels, and the new Canvas::resolve_geom / resolve_font for explicit Length values, which every widget now uses in place of the raw l.resolve( canvas.viewport_layout(), EM ) pattern (row, column, wrap_grid, spacer, container, separator, button, text, rich_text, text_edit, list_item, vslider, image, and the container draw path). Overlay sizing keeps resolving against the main surface with the global density, which is what it describes. New tests cover explicit-density resolution, resolution-time application, the local-over-global override and sub-canvas inheritance. The GLES image texture cache is now bounded. It was content-keyed but unbounded and never evicted, so a stream of distinct buffers — a photo carousel, video thumbnails — grew GPU memory for the lifetime of the canvas. The cache now tracks an estimated byte total (RGBA8, w × h × 4) against a 32 MiB budget and evicts least-recently-drawn textures on insert; the most recent entry is never evicted, so a single texture larger than the whole budget still draws and simply owns the cache until replaced. Drop-time cleanup is unchanged: drain deletes whatever the map holds. CI gains a Clippy step (workspace, all targets, test-support, -D warnings) sharing the build cache of the test job, with make clippy mirroring the invocation locally and CONTRIBUTING listing it. Run make clippy locally before pushing the first time — the gate has not seen the tree yet and pre-existing lints will fail CI until addressed. docs/backends.md formalises the software/GLES capability matrix that was previously scattered across per-method rustdoc: parity set (fills, strokes, text, images, paths, path clips), graceful degradations on software (flat-fill gradients, no shadows, no backdrop blur, hard bottom edge), GPU-only features (external textures), the shared Oklab-fallback limitation, and the cross-backend blit panic. Linked from README, onboarding and architecture's known-gaps list, which now states the parity gaps explicitly. The dp/density prose in architecture.md, lib.rs and the Length rustdoc is updated for resolution-time semantics and the per-canvas override. |
||
|
|
a7f953ca42 |
layout: adaptive grid and vertical flex; enforce the mechanical style rules
grid_min_cell( width ) adds the adaptive mode WrapGrid was missing: instead of a fixed column count, the count is derived at layout time from the available width so every cell is at least `width` wide (any Length, so a fluid threshold works; never fewer than one column), re-derived on every resize. Cells then share the width equally, and WrapGrid::max_columns( n ) caps the derived count so cells grow instead of multiplying on very wide surfaces — the cap only applies to the adaptive mode, grid( n ) keeps the fixed behaviour untouched. Four new layout tests cover width derivation, spacing accounting, the one-column floor and the cap; the row-wrap assertions check X positions rather than Y because zero-height spacer children produce zero-height rows. flex( child ) now distributes leftover height inside a Column, mirroring its leftover-width behaviour in a Row: flex children join the weight pool alongside weight-only spacers and y-scrolls, draw their child inside the allocated share at full inner width, and contribute zero to the column's natural height exactly like a row counts flex width. This closes the documented "vertical flex is not yet implemented" gap; the Flex rustdoc and the widgets.md entry now describe both axes. Add scripts/style-check.sh and a make stylecheck target, wired into CI: mechanical verification of the grep-checkable subset of code_style_guide.md — tab indentation and spaces inside attribute brackets — with comment lines skipped so prose may cite raw attribute syntax. To turn the check on green, the 82 unspaced attribute sites across 24 files (#[test], #[derive(...)], #[cfg(...)], #[allow(...)]) were normalized to the spaced form. CONTRIBUTING and the style guide reference the new target; brace placement and paren spacing remain review concerns. |
||
|
|
1fd697aa6d |
docs overhaul, orientation API, fluid-sizing fixes, examples made honest
Documentation pass: every claim in docs/ and the meta files was audited against the source and the drift fixed — around ninety corrections. CONTRIBUTING and the CI workflow now run cargo test with --features test-support (the gated test_support module made both the documented commands and the CI build fail to compile), make example becomes make examples, make doctest-md and the debhelper requirement of make clean are documented, and patch shape asks for a CHANGELOG entry. theming.md loses the nonexistent surface.backdrop, gains the real gradient defaults (linear-rgb, oklab), the six slot variants including typography, the ten-field palette, a truthful effects-consumer table, the ThemePreference/from_hour API and a responsive-sizing note; the stale docstrings in src/theme that fed the drift are fixed too. architecture.md's "Known gaps" section is rewritten against reality (multi-touch slots, xdg-activation, a11y live regions and SetValue/Increment/Decrement are implemented), gains a module map, subsurfaces and window-lifecycle coverage, and correct crustace/loginmanager paths. widgets.md fixes the ten factual errors (stateless spinner, toast/combo via overlays(), tooltip hover contract, row has no max_width, scroll axes, multiline text_edit, dialog panic wording) and now states the column() 16 px default padding — the recurring ambush — plus row's differing 0 default and dialog's max_width. onboarding, README and cookbook get the remaining sweep: build/test instructions, complete example lists, img_widget, clipping-parity honesty, ~30 Hz software cap, read_rgba_pixels signature, tab indentation in snippets, and rustdoc-style links that rendered literally are gone everywhere. CHANGELOG is restructured per Keep a Changelog with the missing entries (window_resizable, claims_raw_touch, Row::align_top/fill_height, caret fixes, dependency pins) and the pad_v Added/Changed contradiction resolved. New adaptive-layout API: ltk::orientation() with the Orientation enum, backed by viewport_size()/set_viewport_size — the runtime records the main surface's physical dimensions on every configure, before App::on_resize, so view() can branch a layout on portrait vs landscape without hand-tracking resizes. The portrait rule matches Length::orient (square counts as portrait); embedders driving core::UiSurface call set_viewport_size themselves. Documented in the crate root's responsive-design section and architecture.md. Fluid-vs-fixed sizing fixes in widgets, all the same disease — fluid content inside a fixed-pixel box. TextEdit::fixed_width takes impl Into<Length> (f32 call sites keep compiling as px) and the time picker's digit fields move to Length::fluid( 72.0 ), matching their fluid font so digits can no longer outgrow the box. Dialog::max_width takes impl Into<Length> with a Length::fluid( 480.0 ) default so the card scales with the stock buttons inside it, and the card's interior no longer stacks the column() default 16 px padding on top of CARD_PADDING — that double inset squeezed the action row until its buttons clipped on narrow windows. App::on_pointer_axis now triggers a view rebuild and repaint; previously state mutated in the hook did not paint until the next unrelated event. Examples reworked to be honest demos: responsive's mode/density controls become stock buttons in a grid/column so they follow the modes they demonstrate instead of overflowing; dialog's openers stack vertically, and the example gains the app-level ESC handler so the ESC chain closes an open dialog first and quits second; widgets' tab strip now switches real per-tab pages; carousel gains pointer/touch drag through the horizontal-swipe hooks (crustace's pager pattern), one-tile-per-detent mouse wheel, and snap math driven by the real surface width from on_resize instead of a hardcoded 800; clip_path arranges its cells by ltk::orientation() and sizes them from the counter-axis of the flow. |
||
|
|
14572ebfb6 |
render: sub-canvases inherit the root layout viewport for fluid resolution
Content drawn inside a scroll rendered smaller than identical content outside it: the scroll viewport draws its child into a sub-canvas sized to the viewport rect, and Canvas::viewport_layout / viewport_logical — the viewports that geom_px and font_px resolve fluid Lengths against — returned the canvas's own size. Inside a 312 px sub-canvas on a 360 px surface every fluid geometry (icon sizes, row heights, paddings) and font size resolved against 312 instead of 360, shrinking the scroll content by the width ratio, ~13 % in a phone-sized window with 24 px margins. First observed in Eydos Settings' Wi-Fi list, where the connected network row sits outside the scroll and the rest inside: the same full-fan signal icon measured 21x18 px outside and 18x16 px inside. Add a layout_viewport field to SoftwareCanvas and GlesCanvas, None on root canvases and set by sub_canvas to the parent's effective layout viewport, so nested sub-canvases keep propagating the root surface size. viewport_layout and viewport_logical consult the inherited value before falling back to the canvas size. Root canvases are untouched, and the GLES clip layers, which reuse sub_canvas, get the same correction. |
||
|
|
d31e7222da |
list_item: trailing icon slot and horizontal inset override
Disclosure arrows in list rows could only be text: the trailing slot draws a string, so apps fell back to the "›" glyph at 14 px, which cannot use the theme's arrow SVGs and looks thin next to 24 px leading icons. Add ListItem::trailing_icon( rgba, w, h ): a right-aligned icon drawn at the new theme::TRAILING_ICON_SIZE (21 px, 1.5x the old glyph em box), vertically centered. It coexists with trailing text, which shifts to the icon's left. Like the leading icon, symbolic assets are pre-tinted by the caller (tint_symbolic), so the widget stays colour-agnostic. Add ListItem::pad_h( impl Into<Length> ), a per-item override of the horizontal inset between the row edge and its content (leading icon/label on the left, trailing text/icon on the right). Resolution follows the Separator pattern: an explicit Length wins, otherwise the theme default (16 px through geom_px) applies, so existing rows are unaffected. This lets an app whose enclosing view already provides the margin bring row content flush instead of stacking both insets. Document both builders in docs/widgets.md and the changelog, and rework the list_item rustdoc example to point at trailing_icon with a theme icon for disclosure arrows instead of the text glyph. |
||
|
|
8762ab9ce0 |
text_edit font sizing on Length; add Button::width and Text::line_height
Unify TextEdit's font size with the button label. The button resolved its `font_size` Length in font space (`viewport_logical`, so the `× dpi_scale` at raster lands the right physical size), while `TextEdit::font_size` took a raw `f32` that the draw / hit-test paths treated as a logical size. A caller that resolved a Length against the physical surface and passed the result as that `f32` therefore double-counted `dpi_scale` and got a font that rendered too large. `TextEdit::font_size` now takes `impl Into<Length>` and is resolved against `viewport_logical` exactly like the button, so the two paths agree. The field stores `Option<Length>` (`None` follows the widget-scaling mode at the theme default), retiring the old `f32` sentinel (`0.0` = mode, negative = fluid design px). `font_size_fluid` becomes shorthand for `Length::fluid( n )`. The `Option<Length>` flows through the `WidgetHandlers::TextEdit` snapshot and `text_input_geometry` and is resolved at draw / hit-test time, both of which carry a canvas; the inner measure helpers (`wrapping`, `hit_test`) keep `f32` because they receive the already-resolved value. Backward compatible: `f32` call sites still compile via `f32: Into<Length>` (→ `Length::px`), with the same result as before. Add `Button::width( impl Into<Length> )`. Text buttons size to their label plus padding; some layouts need a pinned width instead — a full-width or surface-proportional button. The new builder mirrors `height`: resolved in physical layout space, clamped to the available `max_width`, propagated through `map_msg`, and a no-op for icon buttons. Add `Text::line_height( mult )`. Wrapped multi-line text used the font's declared leading (`new_line_size`), which is tight for some labels; the multiplier scales the gap between wrapped lines (`1.0`, the default, keeps the natural leading — every other `text` is unchanged — and a `0.5` floor keeps lines from overlapping). Applied uniformly in `preferred_size` and `draw` so the reported height and the drawn baselines stay consistent. Tests: `button` gains a pinned-width and a size-to-content case; `text` gains a line-height default / clamp test and a check that doubling the line height doubles a wrapped block's reported height. Docs: `docs/widgets.md` `text` / `button` / `separator` sections updated for the new builders, and a `CHANGELOG.md` "Unreleased" section covering this batch alongside the responsive work already landed. |
||
|
|
d4d7ee742e |
Bump to 0.2.0: SW/GLES paint parity, shared font resolution, FrameState refactor, docs and packaging fixes
Rendering parity (software ↔ GLES). The software backend now rounds glyph pen positions and image destinations to the nearest integer pixel, matching what the GLES backend already did; previously it truncated, so text and 1:1 images could land up to half a pixel off between the two backends and the bilinear sample read ~1 px softer than the source. Gradients and shadows are deliberately left unimplemented on the software backend, and the GLES multi-rect `glScissor` clip is left coarse on purpose: making it exact would need stencil bits the EGL config does not carry, or routing the partial-redraw path through the offscreen clip layer, which would break its `fill` / `clear_rects_transparent` scissor semantics. Adds software-backend pixel tests covering the snapping. Font resolution unification. The system-font candidate chain, `find_font_opt` and `load_default_font_bytes` lived in two copies (`render/helpers` and `gles_render/helpers`) that had already diverged — one resolved through `find_font_opt`, the other inlined the candidate loop — and now live once in `system_fonts`. The two per-backend `OnceLock` default-font caches and `primary_handle` collapse into a single `system_fonts::default_handle`. Module docs and the `font_registry` caller are updated accordingly. Shared image validation and rect inflation. `draw_image_data`'s dimension check and its one-line warning were byte-duplicated across both backends and are now `render::helpers::validate_rgba_dims`. The six manual symmetric `Rect`-inflate literals in the GLES primitives reuse the existing `Rect::expand`. FrameState and DrawCtx de-duplication. The eleven `SurfaceState` fields the draw pass owns and threads through `DrawCtx` — `widget_rects`, the cursor / selection maps, the scroll state, `accessible_extras`, `prev_focused` / `prev_hovered` / `prev_pressed` — move into a `FrameState` sub-struct. The per-frame `build_draw_ctx` / `commit_draw_ctx` helpers can then borrow `&mut ss.frame`, disjoint from `ss.canvas` and `ss.pool`, so the four frame paths (software / GLES × full / partial) replace their duplicated `DrawCtx` construction and write-back with a single helper call each. A whole-`SurfaceState` borrow could not express this (partial borrows do not cross function boundaries), which is why the helpers take the sub-struct. `content_dirty` stays on `SurfaceState` — it is an invalidation flag, not frame state — and is reset at the call site. rich_text tests. Adds the previously-missing `tests.rs` for the `RichText` widget: one hit rect per visual line a link spans (the widget's core invariant), the single-line and no-link cases, preferred-size growth with hard line breaks, and `map_msg` range preservation — all headless against a software `Canvas`, with line counts forced by `\n` so they do not depend on any system font's measured width. Documentation. Fills the rustdoc gaps on the embedder-facing surface: `core::UiSurface` accessors, the `egl_context` public API, the `GlesCanvas` methods, `theme::typography` and `theme::error`, and the `RichText` / `Text` builders; adds a crate-level "Rendering backends" overview. `CHANGELOG.md` is added (0.2.0 / 0.1.0), and `docs/widgets.md` / `docs/cookbook.md` gain `rich_text`, `external`, and the CPU-draw / path-clip / externally-laid-out-tree recipes. `debian/changelog` gets the 0.2.0-1 entry. Private intra-doc links to `system_fonts` are demoted to code spans so `cargo doc` is warning-free. Packaging. The `libltk-dev` registry crate shipped a `Cargo.toml` declaring the `lookup` bench while the `Makefile` install copied only `src/`, so Cargo refused to parse the manifest over a missing `benches/lookup.rs`; the install now ships `benches/` as well (the file alone satisfies the parse — criterion is a dev-dependency and is not resolved when the crate is consumed as a library). `Cargo.toml` is bumped to 0.2.0 to match the package version and the `ltk-0.2.0` registry directory. |