State restored on an ordinary launch for apps that ask, focus that follows its widget's id, buttons that do not take the focus when pressed, a focus ring only after the keyboard, and a browser app's surfaces copied rather than transferred, which WebKit got wrong
Restore on launch. `restore_state` follows Android's saved-instance-state contract: the saved bytes come back after a crash or a session restore, and a launch from the launcher starts fresh. Some apps keep state their users expect to find however the last run ended, as an Android browser keeps its tabs in its own storage even when closed from the app switcher. `App::restore_on_launch`, false by default, has the runtime restore on a plain launch too; `SessionRuntime::bootstrap` asks it before deciding. Saving, the file layout and the window geometry are the same either way. Its documentation explains when it is asked, that it is not when persistence is off, that `restore_state` must then merge what the launch itself asks for, such as a URL on the command line, and that what `save_state` returns now outlives every exit; it carries a compiled example. `restore_state`'s documentation, `docs/onboarding.md` and the cookbook's session state recipe mention it. Focus on a press. `Button::focusable( false )` and `WindowButton` kept out of the Tab cycle but still took the focus when pressed, and Enter then pressed them again, which closed a browser's tab when the user pressed Enter in a page after closing another tab. `LaidOutWidget` gains `focus_on_press`, from `Element::focus_on_press`: a button's `focusable`, a `WindowButton`'s, a pressable's new `Pressable::focus_on_press`, true by default, and true for every other widget. `focus_pressed` replaces `set_focus` for pointer and touch presses and leaves the focus where it was when the widget does not take it. Enter's fallback to the widget under the pointer, which `hovered_idx` follows, skips such widgets too, and a rebuild that leaves the focus on one drops it (`drop_misplaced_focus`). The close buttons of `tab_strip`, on the desktop and on the phone's cards, are not focusable; `symbol_button` takes whether its button is. Focus by id. The focus was held as the index of a widget in the laid-out tree, so a rebuild that inserted or removed widgets before it moved it to whatever widget took that index. `WidgetId` becomes a name and a key, `WidgetId::keyed( name, key )` telling apart widgets of the same kind, such as the tabs of a strip; `WidgetId( "name" )` still builds one, through a `const fn` of that name, so the calls in every app compile unchanged, and only code that read `.0`, the toasts, the combo and the tooltip here, moves to the derived `Hash` or `name()`. After each layout `follow_focused_id` finds the focused id and moves the focus to its new index with the field's pending text, cursor, selection and scroll; it drops the focus when the widget is gone, keeps it when a scroll only clipped it, and leaves it when the widget at the old index still has the id. `draw_surface` returns whether the focus moved, and its callers draw the surface again rather than marking it done, since the frame just drawn showed the focus in the old place. Widgets without an id keep the index. Each tab and each card of `tab_strip` carries `WidgetId::keyed` with the tab's key, so a chosen tab keeps the focus while others close. Focus ring. A press that focused a button, a toggle or a list item left its ring drawn, which browsers have not done since `:focus-visible`. `SurfaceState::focus_visible` is set by Tab and Shift+Tab and cleared by a press that moves the focus, `show_focus_ring` redrawing the whole surface when it changes, since the partial redraw only sees indices. The draw is given the focused index only while the ring shows or the widget is a text field, which draws its cursor however it was focused; the focus itself is unchanged, so Enter and Space still press the widget. Key repeat. A key repeated by the platform rather than by ltk's own timer reached the focused widget even while `App::claims_keyboard` was true; `key_repeated` checks it as `key_press` does. Browser surfaces. Every surface is painted through the app's one WebGL context on one `OffscreenCanvas`, and was handed to its own canvas, a `bitmaprenderer`, with `transferToImageBitmap`. WebKit reuses the buffer of a bitmap it was handed, so in a WebKit browser each surface showed another's frame, or none, and only the last one painted was right: forge/web's background, dock and windows came and went in a WPE browser while Chromium showed them all. A test page with two canvases fed from one offscreen canvas showed their colours swapped in WPE and right with a copy. Each surface's canvas is now a `2d` one, and `present_copy` copies the offscreen canvas with `drawImage` in `copy` composition, set before every copy since resizing a canvas resets its context, so a transparent frame replaces the last rather than leaving trails behind an animation. Subsurfaces go the same way. `WebSurfaceState` loses `bitmap` and `new` its `gpu` argument, and web-sys loses the `ImageBitmap` and `ImageBitmapRenderingContext` features. CHANGELOG records each of these. Not done. The tests that build `LaidOutWidget` by hand set the new field, but no test covers a press on a non-focusable button, the focus following an id or the ring. For one frame after a rebuild moves the focused widget, its ring and its cursor may show in the old place.
This commit is contained in:
@@ -76,9 +76,17 @@ All notable changes to `ltk` are documented here. The format is based on [Keep a
|
||||
- **`make stylecheck` / `scripts/style-check.sh`** — mechanical checks for the grep-verifiable subset of the style guide (tab indentation, spaces inside attribute brackets), wired into CI. The whole tree was normalized to pass (82 attribute sites).
|
||||
- **`ltk::orientation()` / `viewport_size()` / `set_viewport_size`** and the `Orientation` enum — the runtime records the main surface's physical dimensions on every configure, so `view()` can branch a layout on portrait vs landscape (`match ltk::orientation() { … }`) without tracking `on_resize` by hand; the portrait/landscape rule matches `Length::orient` (square counts as portrait). Embedders driving `core::UiSurface` directly call `set_viewport_size` themselves. `examples/clip_path.rs` demonstrates it.
|
||||
- **`test-support` Cargo feature** gates the `test_support` module so third-party builds never see it (ltk's own `make test` enables it).
|
||||
- **`App::restore_on_launch`** — `true` hands the saved state to `restore_state` on an ordinary launch too, for an app whose users expect to find what they left open however the last run ended, as a browser's tabs; by default a plain launch still starts fresh.
|
||||
- **`WidgetId::keyed( name, key )`** — the id of one of many widgets of the same kind, such as the tabs of a strip, told apart by a key that stays the widget's own. `WidgetId( name )` is the same with key 0.
|
||||
- **`Pressable::focus_on_press( false )`** — a press on the pressable leaves the focus where it was.
|
||||
|
||||
### Changed
|
||||
|
||||
- **`WidgetId` is a name and a key** — `WidgetId { name, key }` with private fields, read through `name()` and `key()`, in place of the tuple struct `WidgetId( pub &'static str )`. `WidgetId( "name" )` still builds one, through a `const fn` of that name, so existing calls compile unchanged; only code reading `.0` must use `name()`.
|
||||
- **The focus follows its widget's id** — the focus was held as the index of a widget in the laid-out tree, so a rebuild that inserted or removed widgets before it moved it to whatever widget took that index, such as the close button of the next tab after a tab closed. When the focused widget has a `WidgetId`, the focus, with the field's pending text, cursor, selection and scroll, now moves to wherever that id is after each layout, and is dropped when the widget is gone; a widget only scrolled out of view keeps it. The surface is drawn again when the focus moved. Widgets without an id keep the index as before.
|
||||
- **The focus ring shows only after the keyboard moved the focus** — a press that focused a button, a toggle or a list item left its ring drawn, as a browser does not since `:focus-visible`. The ring now shows after Tab or Shift+Tab and goes with the next press; a text field still draws its cursor however it was focused. The widget keeps the focus either way, so Enter and Space still press it.
|
||||
- **A button that is not focusable does not take the focus when pressed** — `Button::focusable( false )` and `WindowButton`, which is not focusable by default, kept out of the Tab cycle but still took the focus on a press, so a later Enter or Space pressed them again. A press on one now leaves the focus where it was, Enter no longer falls back to such a widget under the pointer, and a rebuild that leaves the focus on one drops it. The close buttons of `tab_strip` are not focusable; a tab takes the focus when chosen and keeps it by its key.
|
||||
|
||||
- **A key that types nothing reaches the app from a text field** — with a text field focused, a key without Ctrl that types no text, such as F3, was dropped; it now goes to `App::on_key_with_modifiers`, as it does with no field focused, so an editor can search again with F3 and back with Shift+F3.
|
||||
- **A long multiline value no longer slows every frame** — wrapping measured each character by shaping it alone, on every draw, click and caret motion. Each character's advance is now measured once per font and size (`Canvas::measure_char`), and the last layouts are kept by the value, width and font, so a value is wrapped again only when one of them changes.
|
||||
- **A surface's scale may be fractional.** `SurfaceState::scale_factor` is an `f32`, the physical pixels per logical one, 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 (`physical_width`, `physical_height`), 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`. `App::on_scale_changed` keeps its `u32`, the scale rounded, and `App::on_resize` gives the exact physical size.
|
||||
@@ -102,6 +110,8 @@ All notable changes to `ltk` are documented here. The format is based on [Keep a
|
||||
|
||||
### Fixed
|
||||
|
||||
- **A browser app's surfaces in WebKit** — every surface is painted through one WebGL context on one `OffscreenCanvas` and was handed to its own canvas, a `bitmaprenderer`, with `transferToImageBitmap`. WebKit reuses the buffer of a bitmap it was handed, so in a WebKit browser each surface showed another's frame, or none, and only the last one painted was right: forge/web's background, dock and windows came and went. Each surface's canvas is now a `2d` one that copies the offscreen canvas with `drawImage`, in `copy` composition so a transparent frame replaces the last; subsurfaces too. `ImageBitmap` and `ImageBitmapRenderingContext` leave web-sys's features.
|
||||
- **`claims_keyboard` covers the platform's key repeat** — a key repeated by the platform rather than by ltk's own timer still reached the focused widget while `App::claims_keyboard` was `true`.
|
||||
- **A close the app refuses shows what it asks** — 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 now goes through `AppData::request_close`, which draws the view again when the app refused it.
|
||||
- **A multiline field keeps the view it shows** — the first line on screen was worked out on every frame from the last one scrolled to by hand, or from the top, and never kept, so once the cursor had gone below the first screen it stayed on the last row: the arrows moved the text instead of the cursor, and a click brought the line clicked to the bottom. `TextEdit::draw` now returns the view it showed, which the frame keeps, so the view moves only when the cursor leaves it, and by the least; a `TextCommand::Select` no longer resets it. A press, the wheel, the scroll bar and the page keys act on the lines on screen as the last frame showed them (`Window::shown`): 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. A long press that holds the scroll bar no longer opens the Copy / Cut / Paste menu.
|
||||
- **A multiline field scrolled by hand stays where it was taken** — focusing a field puts its cursor at the end through a sentinel past the value, and a scroll by the wheel kept that sentinel while the view compared it with the cursor taken back to the value's length: they never matched, so the view went back to the cursor at once and the wheel seemed to do nothing. The kept cursor is now taken back the same way.
|
||||
|
||||
Reference in New Issue
Block a user