02ab10b3ef0b758311ceddcc8c11c0a36097453c
22
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
02ab10b3ef |
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. |
||
|
|
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.
|
||
|
|
3edd822918 |
Toplevel events name the window, apps hear the pointer hover a surface and where their blurred cards were laid out, and rows can align to the bottom
A shell could not tell the windows of an app apart, follow the mouse over one of its overlays, or tell its compositor where the translucent cards of a surface ended up. crustace's floating dock needs all three, to activate and close single windows, to magnify the icons under the pointer and to have forge blur behind its pill and its Quick Settings cards.
`ToplevelEvent::Opened` carries the handle's `identifier`, the compositor's token for that one window, and its `title`, both from `ForeignToplevelInfo`; `toplevel_opened` builds the event for the new and the updated handle. Matches on `Opened { id, app_id }` have to add the fields or `..`.
`App::on_pointer_hover( SurfaceTarget, Option<( x, y )> )` is called on every mouse motion over a surface, in its physical pixels, and with `None` from the pointer's `leave`, which ltk did not handle before. `SurfaceFocus::target` gives the `SurfaceTarget` of a focus.
`Container::blur_behind()` marks a container whose rect and largest corner radius, capped at half its shorter side, the layout pass records in surface coordinates through the `DrawCtx::origin` offset of the sub-canvases, clipped by the viewports it sits in. `commit_draw_ctx` keeps them in `FrameState`, and after a frame where they changed `draw_frame` hands them to `App::on_blur_regions( SurfaceTarget, &[ ( Rect, f32 ) ] )`, for the main surface and each overlay.
`Row::align_bottom()` pins the children to the bottom edge, so taller siblings grow upward. `docs/architecture.md` lists `on_pointer_hover` among the input hooks.
Not done. `on_blur_regions`, `blur_behind` and `align_bottom` are not in the documentation, and there are no tests for them. The regions are reported after the frame is committed, so a compositor that receives them over IPC is one frame behind a surface whose cards move.
|
||
|
|
dcb1c7dff3 |
A tab strip that scrolls to the selected tab and while a tab is dragged against its edge, and hooks for every mouse button and for the modifiers, for an application that hands the mouse on
A strip with more tabs than fit left the selected one wherever it was, so a tab opened at the end or chosen with the keyboard could stay out of sight, and a tab could only be dragged as far as the visible part reached. A terminal that passes the mouse to the program in it, as xterm does, also needs every click with its modifiers, which ltk kept to itself apart from the middle button, and the modifiers held while the wheel turns, which the wheel does not carry. Scrolling to the selected tab. `TabRow` now knows the key of the selected tab, and in the layout it asks the `TabMotion` where the scroll of the tabs should go before drawing it; the answer is written into the scroll's own offset, which is keyed by the same index. When the selection changes the strip scrolls just enough to show that tab, through the scroll's `offset_into_view`, and keeps following it while it grows from nothing, so a new tab at the end is shown whole once it has opened. After that the strip is left where the user scrolls it. Dragging against the edge. While a tab is dragged and its lifted copy touches either edge of the visible part, the strip scrolls 8 widget pixels further that way on each frame (`EDGE_SCROLL`), and every layout places the dragged tab under the pointer again, so it travels with the strip although the pointer holds still and drops among the tabs that come into view. Pointer coordinates. A scroll and a viewport draw their child on a canvas of their own, so the tabs were laid out in coordinates that no longer matched the pointer once the strip scrolled. `DrawCtx.origin` now holds where the canvas being drawn on sits on the surface, set around the child of every `Scroll` and `Viewport`, and `TabCells` and `TabCards` hand it to the `TabMotion`, which takes it off the pointer in `drag_move`. The mouse. `App::on_mouse_button( &MouseButtonEvent )` reports every press and release of the left, middle and right buttons, with the position in physical pixels and Ctrl, Shift and Alt, before ltk handles it; `MouseButton` and `MouseButtonEvent` are exported. `App::on_modifiers( ctrl, shift, alt )` is called whenever the modifiers held change. Both do nothing by default. Tests cover a selected tab scrolled into view once and back, and a tab dragged against the edge scrolling the strip. The documentation of `tab_strip` describes the scrolling. |
||
|
|
ce98c7b82c |
TabStrip, the open documents of an application as a browser or a terminal shows them, with tabs that grow, shrink and can be dragged, an overview of cards for a phone, a widget anchored under another one and list items in colours of their own
Trust and prompt each drew their own strip of tabs, their own phone bar with a counter and their own grid of cards, and neither animated anything nor let a tab move. `tab_strip` does all of it once, in colours the application chooses, so the same widget serves a black terminal and a browser that follows the theme. Two things the menus of those applications needed come with it: a way to place a menu under the button that opens it, and list items that are not drawn in the theme's colours. The widget. `tab_strip( tabs )` takes `TabItem`s, made with `tab( key, label )`, where the key stays the same while the tab is open, plus `.image( ImageData )` for a page's icon, `.symbol( name )` for a theme icon drawn in the tab's ink, and `.closable( bool )`. The builder sets the `selected` tab, `on_select`, `on_close`, `on_overview`, the buttons before the tabs (`leading`), right after the last one (`after_tabs`) and at the far right (`trailing`), the phone bar's `mobile_center`, the `TabStripColors`, the `TabLayout`, `reorderable` and the `TabMotion`. `TabStripColors` sets the strip's background and ink and the fill, edge and ink of the active tab and of the others; the default follows the theme. On a desktop. `[leading] [tabs…][after_tabs] clear [trailing]`. The tabs share the width between 90 widget pixels and a fifth of the screen's width and scroll sideways when even the minimum does not fit, and 7.5 % of the width between the leading and trailing buttons is always left clear, also while they scroll. A horizontal scroll measures its child with no width limit, so the tabs inside it cannot see how much room there is; two new elements solve that. `TabRow`, outside the scroll, works the tab width out of its own rect, less the clear part and the buttons after the tabs, and hands it to the `TabMotion`; `TabCells`, inside the scroll, places the tabs, the gaps of closing ones and the buttons where the motion says. Each tab is clipped by a `viewport`, so it can grow from nothing and shrink away. On a phone. `TabLayout::Mobile` is a bar with the leading buttons, the active tab's icon and title or the application's `mobile_center`, a counter that sends `on_overview` and the trailing buttons. `tab_overview( tabs )` is the grid of cards it opens, with a `title`, a `back` message and header `action`s: `build` gives the whole page, header and scrolling cards, and `cards` the grid alone for an application that lays its page out itself. The cards are placed by a third new element, `TabCards`, every one as tall as the tallest, so a dragged card can follow the finger. The motion. `TabMotion` is the only state, kept by the application, handed to every view and reported from `App::is_animating`; it is an `Arc<Mutex>` because `view` only gets `&self`, and clones share it. A tab that opens grows from nothing while the others narrow, and one that closes leaves a gap that shrinks away while the others keep their width for a second, so the close button of the tab that slides in lands under the pointer; another close starts the second again. Changes take 180 ms, ease-out. The first layout and a resize alone place the tabs without moving them. Reordering. With `reorderable( true )` each tab, and each card of the overview, carries its select message as `on_drag_start`, so it is dragged after 24 px of mouse motion or a long press, and dragging a tab selects it. The drag reaches the application, which forwards `App::on_drag_move` to `TabMotion::drag_move` and answers `App::on_drop` with the `( from, to )` that `TabMotion::drag_end` reports. The motion remembers where it drew every tab: the first move takes the tab under it and returns `false` when there is none, the tab then follows the pointer within the tabs and is drawn on top, and the others slide out of its way as its centre passes theirs, or as it enters another cell of the overview. On release it slides into its place. A strip and its overview are never shown at once and may share one motion. Anchored. `AnchoredOverlay`, which only the combo used, is public as `anchored( child, id )`: it draws its child under the widget built with that `WidgetId`, as it was laid out on the previous frame. `align` lines it up with the anchor's left edge, its right edge or its middle, `gap` sets the space under it, and the child is now kept within the rect its parent gives it, which also keeps a combo's list inside the window. List items and icons. `ListItem::ink( label, secondary )` draws the label, the subtitle and the trailing text in colours of the application's own and tints hover and press with the label's, for rows on a surface that is not the theme's. A button's icon is now fitted into its square without being stretched: a wide icon such as `actions/remove`, a 25 by 4 minus, was drawn as a block as tall as it was wide. Documentation, example and tests. `docs/widgets.md` describes `tab_strip` and `anchored` and the new `ink`. `cargo run --example tab_strip` opens three documents that can be added, closed and dragged, on a desktop strip or a phone bar with its overview. The tests cover the share of the width, its limits and the clear part, the buttons after the tabs, the drawing order of a dragged tab, the builder's defaults, the overview's rows, and in `motion.rs` the growth, the gap, the wait and its restart, a resize, and dragging a tab and a card; `anchored` and the icon fit have tests of their own. Not done. A strip that scrolls does not scroll to the active tab or follow a tab dragged towards its edge, and the overview does not scroll while a card is dragged to the edge of the screen. |
||
|
|
a1a4d51677 |
An on-screen keyboard of ltk's own, driven by layout files; TextEdit::purpose for the row a terminal needs; the wheel scrolls a multiline field; a notebook page fills its height
Text entry on a phone comes from `zwp_text_input_v3`: a focused field enables the protocol and the compositor's input method, squeekboard today, draws a keyboard and commits text through it. Eydos wants to drop that process, so the keyboard has to come from somewhere else, and the natural place is the toolkit that already draws everything else and knows which field has the focus and what it is for. This commit puts one in ltk, as data files plus a panel any host can drive: ltk's runtime shows it under a focused text field when no input method is there, and forge will draw the same panel in its chrome and deliver the keys to the focused client, which is the second half and is not in this commit. Along the way the demo that exercises it exposed two gaps in the widgets, fixed here too.
Layouts are data. `keyboards/<language>.json`, one file per layout, searched in `LTK_KEYBOARDS_DIR` when set, then `$XDG_DATA_HOME/ltk/keyboards` or `~/.local/share/ltk/keyboards`, then `/usr/share/ltk/keyboards`; `osk::layout::search_paths()`, `available()` and `load( name )` are the API, and the English layout is compiled in as the last resort. A file has `name`, a `label` for the layout-switch key, `layers` and `variants`. A layer is a list of rows; a row a list of keys; a key is a bare string for the text it types, or an object with an `action` (`shift`, `backspace`, `enter`, `space`, `layer`, `layout`, `hide`), the target `layer`, a `label`, and a `width` in key units, `1` by default, which share the row's width so a row of nine keys comes out centred under one of ten. `base` is mandatory; a missing `shift` layer is derived from it by uppercasing. `variants` maps a key's text to what a long press offers, and the shifted form of a key uses its base's, uppercased. A key may also be a named key, `{ "key": "Escape" }`, `Tab`, the arrows, `Home`, `End`, `Page_Up`, `Page_Down`, `Delete`, `Insert`, `F1` to `F12`, or a sticky modifier, `{ "modifier": "ctrl" }` or `alt`. `en.json` and `es.json` ship, with symbol layers and the accented variants each language needs. The files go into the registry copy through `make install` and into `ltk-theme-default` under `/usr/share/ltk/keyboards`.
The terminal row. A terminal needs Escape, Tab, Ctrl, Alt, the arrows and the shell's symbols, and no other application does, so those are not a layout but a row laid above any language's layout: `keyboards/bar-terminal.json`, made of the named keys and modifiers above plus `-`, `/`, `|`, `\` and `~` with their variants. Files named `bar-*.json` are rows, not layouts, and `available()` leaves them out, so the layout-switch key does not cycle onto one; the first version listed it and a tap on that key made the keyboard vanish behind a duplicated bar. Which applications get the row is decided by what the field says it is for: `TextEdit::purpose( InputPurpose::Terminal )`, snapshotted into the handlers like `secure`, which the built-in panel reads through `set_purpose` and which also goes out as the `terminal` content purpose of `zwp_text_input_v3`, the same one VTE-based terminals declare, so a compositor hosting the panel can pick the row from the protocol; for a client that declares nothing it can decide by `app_id`.
`osk::Panel` is a state machine a host drives. It holds the loaded layouts, the current layer, shift as off, one-shot or locked, the sticky modifiers, the purpose and its extra row, the geometry of all of it inside the rectangle the host gave it with `set_rect( rect, scale )`, and the press in flight. The host feeds it `press_down`, `press_move`, `press_up` and, from its own timer, `tick`, each returning an `Event`: `Text`, `Backspace`, `Enter`, `Hide`, `Key( keysym, modifiers )` for what text cannot carry, an arrow, Escape, Tab, or a character while Ctrl or Alt is held, or `Changed` when only the panel's look changed. A key types on release; a press held 450 ms opens its variants, which the finger slides onto and the release picks; backspace fires on press and repeats every 60 ms after the same delay; shift is one-shot and locks on a double tap within 350 ms; a modifier stays down for the next key and a character typed under it goes out as a `Key`, since text cannot carry Ctrl. `next_deadline()` says when `tick` next has work, `height( scale )` what the panel needs, `press_down` answers `None` for a press outside it so the host treats that as its own. `draw( canvas )` paints it with the active palette; the action keys show theme icons rather than glyphs, `keyboard/shift`, `keyboard/backspace`, `keyboard/enter`, `general/down-simple` for hide, `accessibility/languages` for the layout key, the `general` arrows for the terminal row, rasterised once per size and tint, because the first version drew `⇧` and `🌐` and the fallback chain has no glyph for the latter, which showed as a question mark; a layout key with no label of its own shows the active layout's. Nothing in `Panel` knows about text fields or Wayland, which is what lets a compositor host it. Fifteen tests cover the layout parser, the derived shift layer, variants following case, the bar filtering, and the panel's taps, one-shot and locked shift, variants, repeat, layer and layout switches and the terminal row.
The runtime shows it. `event_loop/osk.rs` puts a `Panel` on a surface when a text field takes the focus and `builtin_osk` holds: the compositor advertised no `zwp_text_input_manager_v3`, or `LTK_BUILTIN_OSK=1` is set, which is how to try it on a desktop that has an input method, or the app returns `true` from the new `App::builtin_keyboard()`. The panel is placed at the bottom of the surface by both renderers, and the widget tree is laid out in what is left above it, so the focused field is never covered. Presses, motion and releases from the pointer and from touch are offered to the panel before the gesture machine, a press on the already focused field brings a hidden panel back, and the run loop's timeout now takes the minimum of the long-press, tooltip and panel wakeups so variants and repeat fire on time. `Text` goes through the same insertion as a physical key, `Key` through the same dispatch, with Ctrl applied for its duration, `Enter` submits or inserts a newline in a multiline field, `Hide` keeps the focus and takes the panel away; losing the focus removes it. The layout is picked from `$LANG`. `ltk::InputPurpose` is exported, and `docs/architecture.md` gains the section that says all of this, with the compositor's part sketched.
The demo. `examples/inputs` becomes a notebook with the login page it had and a Shell page: one multiline field that holds a transcript, prompt included, declared a terminal, so the row appears; Enter runs the line under the last prompt (`echo`, `ls`, `pwd`, `clear`), the arrows move through the transcript, and a Ctrl combination interrupts the line the way a terminal shows it. Escape quits only from the login page, because a key the app answers never reaches the field, which is what first made the arrows dead.
Two widget gaps the demo found. `Notebook` gave its page the page's preferred height, so a page that fills its space, a scrolled list or a flex, got none and showed nothing: the active page is now wrapped in `flex` and gets the height under the strip. And the multiline field had no way to scroll: it chose its first visible line from the cursor alone on every draw, so the transcript ran under the keyboard and the wheel did nothing. `text_edit/scroll.rs` gives it a `TextScroll` the runtime keeps per field beside the cursor and the selection, in the frame state, the draw context and the `core` runtime alike: the first line asked for by hand and the cursor at that moment. The view holds there while the cursor stays put and, once it moves, follows it by the least it has to, as an editor does; without a request the result is exactly the old rule. `Window::resolve` computes the visible lines once for drawing, hit testing and the wheel, so they cannot disagree, and replaces the copy each of the first two had. In `on_pointer_axis` the vertical motion is offered to the multiline field under the pointer first, three lines per notch from `value120` or `discrete`, touchpad pixels accumulated until they add up to a line, and only when the field cannot move that way does it go to a `scroll` around it. `TextScroll` is public but opaque, since `Element::draw` is public and needs it; `Default` is the cursor-following behaviour. Six tests cover the hold, the follow, the clamping, the notch and the accumulation. A read-only field is not interactive and does not take the wheel; touch drags inside a field do not scroll it yet.
Not done. Forge does not host the panel yet, so on the phone squeekboard is still what appears. The panel's look is a first pass, sized in logical units with the theme's palette, to be restyled from mockups. Only `en` and `es` ship.
|
||
|
|
53e7ccc46e |
Keep the GLES path when the scale arrives first, stop re-rendering what does not change, and let the compositor skip what the surface covers
Measured on a Librem 5 (Cortex-A53, GC7000Lite, 720×1440 at scale 2) with a frame trace on both sides of the socket: the shell's main surface repainted in 110 to 136 ms, quick settings drew at 20 fps with the GPU saturated, and every panel open uploaded its wallpaper and icons again. Most of it was the toolkit doing work the frame did not need. Backend choice. When the compositor announces the output scale before the first `configure`, `scale_factor_changed` allocated an SHM pool for the surface and `on_configure` took that pool as the sign that the software path had been chosen, locking the surface to tiny-skia for its whole life. Every ltk main surface on a scale-2 phone ran in software while its overlays, which inherit the scale from the main and never receive that event, ran on the GPU. The scale handler now only reallocates a pool that already exists; before the first configure neither path is chosen. Wallpapers. `WallpaperBundle::for_size` returned the crop at the source resolution, a 1103×2160 slice of a 4K image for a 720×1440 panel, 10 MB uploaded per texture and a hundred milliseconds each time. The crop is now downscaled to the surface (bilinear, once per size, cached), also for landscape surfaces smaller than the source. Viewport. `Element::Viewport` created a fresh sub-canvas every frame, on GLES a framebuffer, a 2048² glyph atlas and empty image and glyph caches, so a panel clipped to a height that animates re-uploaded its wallpaper and re-rasterised every glyph on every frame. Without a bottom fade the clip is now a scissor on the parent canvas (a mask on the software backend): no offscreen pass, no blit, and the child's textures stay in the parent's caches. `local_viewport` is honoured through a temporary override of the layout viewport. Viewports with a fade keep their sub-canvas, which, like the scroll sub-canvases, is now kept across frames and resized in place instead of reallocated when its size changes. Caches. The image texture cache moves from the canvas to the GL context: every canvas and sub-canvas shares one content-keyed LRU (32 MiB), so an overlay or subsurface that is destroyed and recreated finds its images resident. A GLES canvas dropped with an overlay is pooled in `AppData::canvas_pool` and handed to the next overlay, with its compiled programs and glyph atlas, which took 130 to 290 ms to build on that GPU. Subsurface slots keep their scroll and viewport sub-canvases between rasters. Surfaces. A `container.surface()` with shadows costs a Gaussian drop-shadow pass, a fill and one pass per inset shadow, all full-quad fragment shaders; a dozen glass cards were 40 ms of GPU per frame for chrome that never changes. `Canvas::fill_surface` on GLES now renders the chrome once per size, corners and style into a texture held by the context cache (`gles_render/surface_cache.rs`, 16 MiB LRU) and blits it, drawing directly only for surfaces with `PlusLighter` or `Overlay` insets, which depend on the backdrop. Opaque regions. ltk never called `wl_surface.set_opaque_region`, so the compositor blended every ltk surface and repainted everything underneath a full-screen homescreen. A surface whose `background_color` is opaque now declares itself opaque, and the new `App::overlay_opaque_region( id )` lets an overlay declare the physical rects it paints opaque, such as a panel column over an opaque wallpaper. `sync_opaque_region` sends the rects only when they change. Frame pacing. `eglSwapBuffers` blocked 13 to 37 ms per frame waiting for Mesa's own throttle callback; ltk paces itself on `wl_surface.frame`, so `make_current` sets the swap interval to 0. A subsurface move is now applied only when its parent has no frame callback outstanding, and the parent commit that applies it requests the next callback: a drag commits once per vblank with the latest finger position instead of once per input sample, without the app throttling samples itself. While the app reports `subsurface_motion_only`, the main's callback, and the callback of an overlay that hosts a subsurface, no longer force a full repaint; they mark the surface and the reconcile keeps the cadence with a bare commit when nothing moved. Swipe releases arm a settle the same way instead of dirtying every cache. Two hooks, `on_overlay_frame_committed` and `on_subsurface_committed`, let an app hand a panel over between its host and a sliding subsurface on real commits rather than loop ticks. Lines. A diagonal `draw_line` on GLES stamped one `fill_rect` per pixel of length, 190 draw calls for a spinner and over 2000 for a pattern-lock stroke; it is now a single rotated quad through the rect shader, with round caps and antialiased edges. A pixel test covers the scissor viewport, `for_size` gets a downscale test, and `docs/architecture.md`, `docs/widgets.md` and `docs/backends.md` describe the new hook, the viewport paths, the shared caches and the line rasteriser. |
||
|
|
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. |
||
|
|
492553829e |
widgets, input: PatternGrid for pattern unlock, framed icon buttons, and text keys that bubble past a focused non-text widget; Debian 0.3.0-2
PatternGrid is a 3×3 drag widget for the unlock pattern. It is stateless: the app owns the visited nodes and passes them back in for drawing; a press or a drag that reaches a node within the snap radius fires on_node with the index, 0 to 8 row-major, and release fires on_release, so the crossing and minimum-length rules stay in the app. Geometry lives in widget/pattern_grid/theme.rs (size, dot and node radii, stroke width, snap radius, ring alpha), and the snap radius is resolved through the widget-scaling mode at layout time like the slider thumb (set_pattern_snap_px). The gesture machine reuses the slider's drag ownership: WidgetHandlers::is_drag_target covers both, so hover painting, pointer capture and release behave the same, and hover_affects_paint no longer special-cases sliders. The a11y tree maps it to GenericContainer, since a drawn gesture has no assistive equivalent and the app must offer another method. Exported as pattern_grid / PatternGrid, documented in widgets.md, theming.md and architecture.md, with tests for node centres, a wide rect, snapping, the builders, the handler snapshot and map_msg, and a Security tab in examples/widgets.rs that shows the drawn path. Icon buttons given a width or height are framed keys: preferred_size honours the explicit size instead of the icon size, and drawing paints the variant's background, border and focus ring, shared with text buttons through paint_frame, then centres the icon. An icon button without a size keeps the circular hover overlay it had. This is what lets the PIN keypad's backspace and OK keys sit in the same circles as the digits. Keyboard routing now matches what App::on_key documents. handle_key_default, Backspace, Delete and Space checked focused_idx.is_some() and sent the key into text insertion whenever any widget had focus, which is a no-op on a button, so a focused button or pressable swallowed typed characters. focused_text_input replaces the five copies of that check; characters, Backspace and Delete stay in a focused text input and reach the app otherwise, Return and Space still go to the focused widget's submit or press message when it has one, and the arrow, Home and End handlers use the same helper. The lockscreen relies on this to take PIN digits with its dots row focused. debian/changelog: 0.3.0-2. |
||
|
|
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. |
||
|
|
ce893ac776 |
responsive fluid/physical scaling, widget-API stabilization, and perf guardrails
Responsive scaling. ltk now offers two first-class ways to size a UI so it adapts across screens, chosen per process via `WidgetScaling { Fluid, Physical }` (`set_widget_scaling` / `widget_scaling`, default `Fluid`). Fluid sizing (`Length::fluid( px )`) makes a design pixel a proportion of the surface's smaller side, calibrated against a reference width (`set_fluid_reference` / `fluid_reference`, 412 px default) and bounded by `FLUID_MIN` / `FLUID_MAX`; physical sizing (`Length::dp( px )`) is a constant-physical-size pixel scaled by display density (`set_density` / `density`). `Length` gains `orient( portrait, landscape )` — resolve one value in portrait, another in landscape — plus `widget( px )`, which picks fluid or dp per the active mode. Canvas exposes `geom_px` (geometry, resolved in physical layout space) and `font_px` (font size, bridging logical / physical per mode) so widgets and apps share one resolution path. Note the rename: `set_design_reference` / `design_reference` became `set_fluid_reference` / `fluid_reference`, and `Length::dp` changed meaning — the old surface-proportional behaviour now lives on `Length::fluid`.
Widgets. Every stock widget resolves its default geometry and font through the widget-scaling mode instead of frozen pixels, so a whole UI scales coherently without per-call units. New size builders where they were missing: `button` gains `font_size` / `height`, `text_edit` gains `height` / `font_size_fluid`, `separator` gains `pad_v`, and assorted widgets accept a `Length` where they previously took only `f32`.
Overlays. `OverlaySpec::size` is now `( Length, Length )` instead of `( u32, u32 )`, resolved against the main surface when the overlay is materialized, so overlays can scale with the display; `Length::px( … )` reproduces the old fixed sizing.
API stabilization (toward 1.0). Widget struct fields are now `pub( crate )` — they are configured through builders, not field access — except the value / state types apps genuinely read or construct (`Time`, `Date`, `ComboState`), which stay public. The internal `test_support` helpers move behind a `test-support` Cargo feature (off by default, so third-party builds never see them; ltk's own `make test` enables it). `Separator` drops its `0.0`-means-mode sentinel for `Option<Length>`, so an explicit `pad_v( 0.0 )` is a real flush divider distinct from the mode-following default.
Performance guardrails. Opt-in diagnostics via `LTK_PERF_WARN=1` warn about stuck animations, sustained software-render animation, and low `poll_interval`; software-rendered animation is capped near 30 Hz to spare CPU on machines that fall back off EGL. Apps can override the cap with `App::cap_software_animation`.
Docs and build. The two scaling modes are documented in README, onboarding and architecture, with the earlier gradient / backdrop doc drift cleaned up. The Makefile now ships the `locales/` directory into the packaged crate (fixing i18n keys rendering raw for downstreams), builds the new `responsive` example, and runs tests with `--features test-support`.
|
||
|
|
fb3552e9f7 |
Embedder primitives: placed-child clipping, offscreen RGBA readback, standalone text measurement
Three additions an embedder needs to drive ltk as the render backend for a retained, externally-owned widget tree, each kept general rather than tied to one consumer. Stack placed-child clipping. `Stack::push_placed_clipped(e, rect, clip)` places a child at an exact rect like `push_placed` but clips its subtree's drawing to `clip` (in the Stack's coordinate space) — Android's clipChildren: content that overflows, such as a scrolled list row reaching above the list or an inner card past a rounded bubble, is not painted. The Stack child tuple grows an 8th `Option<Rect>` for the clip, and `layout_and_draw` brackets a clipped child with `set_clip_rects`/restore around the recursion. The clip is shifted by the Stack's own origin to match the placed rect (which `Stack::layout` already offsets), so a Stack laid out at a non-zero origin clips in the right place rather than off by that origin. Offscreen RGBA readback. `Canvas::read_rgba_pixels(out)` reads any canvas into tightly packed straight-alpha RGBA8, top-left row first. Unlike `read_gles_rgba_pixels` it also serves the software backend, un-premultiplying its pixmap, so an offscreen software canvas (an embedder's scratch bitmap) can be read back into a straight-alpha buffer. `Canvas::is_software()` lets a caller branch on the backend — e.g. to honour a real path clip on software but only a bounding rect on GLES. Standalone text measurement. `measure_text(text, size)`, re-exported at the crate root, measures one line with the default UI font and the system fallback chain, returning `(width, line_height)` in pixels without a live `Canvas` — for an embedder's measure pass that must produce the same metrics the renderer will later use. It is backed by `system_fonts::primary_handle()`, a process-wide cached handle for the primary UI font (the same default a canvas loads, with the bundled-font fallback), which widens `render::helpers::load_default_font_bytes` to `pub(crate)`. |
||
|
|
b00cf460bb |
Mature ltk to host an externally-laid-out Android view tree
Add the primitives rustdroid needs to project an Android view hierarchy onto an ltk surface, all kept general rather than Android-specific. Canvas gains arbitrary vector path fill and stroke: `Canvas::fill_path` / `stroke_path` over a new `PathCmd` command list (MoveTo/LineTo/QuadTo/CubicTo/Close in surface coordinates). The software backend rasterises directly with tiny-skia; the GLES backend rasterises into a tiny-skia pixmap and blits it (CPU fallback, no GPU path shader). This is what renders an Android `Path`, a `VectorDrawable`, or a Lottie frame. `ExternalSource::Cpu` (and the `External::cpu` constructor) adds an immediate-mode CPU drawing closure, invoked once per frame with the canvas and the widget's laid-out rect, working on both the GLES and software backends. It hosts a custom `View.onDraw` straight onto the ltk canvas without a GL texture round-trip, unlike the existing `Texture` source which only renders on GLES. `Stack::push_placed` appends a child at an exact rect, bypassing alignment and intrinsic sizing. This lets a view tree whose geometry is computed elsewhere — Android's measure/layout pass, which yields an absolute rect per view — be projected onto a Stack in paint order. New `RichText` widget: wrapped paragraph text carrying a message per clickable link range, with the layout pass emitting one hit rect per link line so taps land on the link rather than the whole paragraph. It is the ltk side of an Android `Spanned` carrying `URLSpan` / `ClickableSpan`. gesture: only drive the horizontal pager once the axis locks horizontal `on_move` emitted horizontal swipe progress whenever `swipe_axis != Vertical`, which includes the pre-lock window where `swipe_axis` is still `None` (the first ~8 px of travel). But `horizontal_drag_started` only flips once `dx.abs() > 8`. A vertical gesture that opened with a few pixels of lateral drift — a swipe-up to the launcher, a scroll — therefore emitted a tiny horizontal progress sample, which armed the consumer's pager, then locked vertical without `dx` ever passing 8, so `horizontal_drag_started` stayed false. On release the horizontal branch was skipped, no `on_swipe_horizontal_progress(0.0)` fired, and the pager stayed stuck active — on the crustace homescreen that froze the surface (the main stays motion-only behind stale page subsurfaces) until an unrelated gesture reset it. Emit horizontal progress only once `swipe_axis == Some(Horizontal)`. Locking onto the horizontal axis already implies `dx.abs() > 8`, so `horizontal_drag_started` is set in the same step, restoring the invariant that a frame which drives the pager always has a matching release event to settle it. The cost is that the first ~8 px of a horizontal drag no longer move the page, which is the same deadband the axis lock already imposes on the vertical panels. Adds `pre_lock_lateral_drift_does_not_drive_horizontal_pager`. |
||
|
|
cfa0faff26 |
ltk: subsurface slides over overlays, axis-locked swipes, physical-space layout, touch reset on resume
Subsurfaces can now be parented to an overlay surface, not just the main surface. `SubsurfaceSpec` gains `parent: SubsurfaceParent { Main, Overlay(id) }` so a sliding panel can ride above app windows the way an overlay panel does, and an optional `gpu: bool` so content that uses the `surface-panel` backdrop-filter glass (a GLES-only pass) keeps it while sliding instead of dropping to the software rasteriser. `reconcile_subsurfaces` resolves each spec's parent independently — skipping an `Overlay` parent that is absent, unconfigured, or zero-sized — tracks the rastered size on the slot, and commits each touched parent once per frame. The ~14 GLES shader programs are compiled once into a shared `AppData::subsurface_gles_canvas` reused across every subsurface, so a lazily re-created sliding panel never recompiles them (hundreds of ms on a mobile GPU).
Vertical and horizontal swipes are now mutually exclusive: a gesture locks onto its dominant axis within the first 8 px of travel (new `SwipeAxis`) and ignores the perpendicular axis for the rest of the gesture, so a vertical swipe that drifts sideways no longer also drives the pager (and vice-versa). The upward swipe progress is no longer clamped at 1.0 — follow-the-finger panels can keep tracking the finger past the commit threshold — and a release below threshold delivers a final `progress = 0.0` cancellation pulse. A vertical swipe also no longer re-rasters the full-screen main surface on every motion event: only the overlays it drives are refreshed, while the horizontal pager (which does move the main surface) still redraws it. Re-rastering the main on every frame of a vertical drag was wasted work that stalled the loop and made the gesture feel laggy to start on a slow GPU.
Layout-affecting `Length` values (widths, paddings, gaps, widget sizes, including `Vw` / `Vh`) now resolve against the physical viewport via the new `Canvas::viewport_layout()` — the space the layout tree is actually computed in — so a `Vw(100)` fills the surface on a HiDPI (scale 2) output instead of covering half of it. Font sizes are unchanged: they still resolve against the logical viewport and are scaled at raster time. Switched column, row, spacer, wrap_grid, container, image, and vslider over to it; `img_widget` and `vslider` now document `Length::vw` / `vh` sizing and the showcase example demonstrates a viewport-relative image.
Touch gesture state is reset when the touch capability is added or removed — suspend / resume on devices that power the touchscreen down. A yanked capability never delivers the pending `up` / `cancel`, so the shared `reset_touch_state()` (also used by the `wl_touch.cancel` handler) drops the stranded `primary_touch_id` / slot state across the main surface and every overlay, keeping the first post-resume gesture clean. Also drops an accidental duplicate `on_scale_changed` call from the scale-change handler.
|
||
|
|
9ca3b60f3a |
ltk: responsive padding/spacing and scrolling, expanded theme palette, and bundled Adwaita cursors
A mixed pass over the default theme and the layout/input core, plus the toolkit's own cursor set. Grouped by area below. == Responsive sizing == Add `Length::dp( px )` — a "design pixel". It interprets `px` against a configurable reference vmin (default 412 px, the eydos mobile reference width) and returns `Vmin( px / reference * 100 ).clamp( px * 0.7, px * 1.5 )`, so a value authored against a mock-up scales with the surface without collapsing on tiny screens or ballooning on a 4K desktop. The reference is process-global, set via `set_design_reference()` and read via `design_reference()` (stored as f32 bits in an AtomicU32); both are re-exported from `lib.rs`. Make container and grid insets relative. `Container`'s four padding fields become `Length` instead of `f32`; every setter (`padding`, `padding_h`, `padding_v`, `padding_top`/`right`/`bottom`/`left`) now takes `impl Into<Length>`, so existing `f32` call sites keep compiling via the `From<f32>` shim. The values are resolved against the viewport in `Container::preferred_size` and in the container draw path (`draw/layout.rs`). `WrapGrid`'s `spacing_x`, `spacing_y` and `padding` get the same treatment, with a `resolved( canvas )` helper funnelling the per-frame resolution and `grid()` seeding `Length::px` defaults. Container tests now compare against `Length::px( … )`. == Scrolling == `Scroll::preferred_size` is now axis-aware. A horizontal-only scroll reports its child's natural height rather than claiming all remaining vertical space, so it no longer steals Y from its siblings when it sits inside a `Column`; vertical and both-axis scrolls keep the spacer-like `( max_width, 0.0 )`. `Column`'s space-distribution correspondingly treats a `Scroll` as a vertical space-claimer only when its axis allows Y. Disambiguate nested scroll viewports by direction. On press the gesture state now collects every scroll viewport under the point (`scroll_candidates`, innermost first) instead of committing to one; on the first 8 px of motion it locks onto the candidate whose axis matches the dominant direction (`scroll_locked`), so a horizontal scroller nested inside a vertical list no longer grabs the wrong axis. The pointer scroll hit test is aligned to the same innermost-first ordering. == Theme palette == `themes/default/theme.json` gains named colours (green / green-deep, yellow, orange / orange-deep, pink / pink-soft, sky-deep, error / error-soft, neutral-tertiary) and new semantic slots in both light and dark modes: `danger`, `text-tertiary`, `accept`, `chip` / `chip-active` / `chip-active-fg`, and `avatar-1` … `avatar-9`. == Cursors == Bundle GNOME's Adwaita cursor theme — the cursors GNOME Shell uses — into `themes/default/cursors/` so a Wayland compositor can draw consistent, complete pointers for ltk applications without the toolkit rasterising cursors itself and without depending on adwaita-icon-theme being installed on the target. The cursors are copied verbatim in XCursor binary format: 35 image files, one per CSS/freedesktop cursor name (default, text, pointer, *-resize, …), plus 27 customary X11 alias symlinks (arrow → default, hand2 → pointer, …); a sibling `cursor.theme` makes the tree a valid XCursor theme. The existing `ltk-theme-default.install` copies `themes/default` recursively, so the directory ships with no packaging change. Applications keep declaring a `CursorShape` per widget over `wp_cursor_shape_v1`; the compositor resolves it against the active theme's `cursors/` directory by name, and the set covers all 34 `CursorShape` variants. Document the set in `themes/default/cursors/README.md` (what it is, the XCursor layout, the full shape list, how the compositor consumes it, guidance for forks) and `themes/default/cursors/LICENSE.md` (attribution and licence options, modelled on the icons catalogue LICENSE). `lib.rs` lists the cursors in its third-party-assets section. Close out licensing in `debian/copyright`: a `Files: themes/default/cursors/*` paragraph records the upstream dual offer (CC-BY-SA-3.0 or LGPL-3, and CC-BY-SA-4.0 for the newer assets) attributed to the GNOME Project, with standalone CC-BY-SA-3.0, CC-BY-SA-4.0 and LGPL-3 paragraphs (summary-plus-canonical-URL for the CC licences, matching the existing CC-BY-4.0 entry; LGPL-3 referencing /usr/share/common-licenses/LGPL-3). The files are unmodified from upstream, so there is nothing to declare under the ShareAlike "indicate if changes were made" clause. Add `tests/cursor_assets.rs`: every `CursorShape` name resolves to a valid XCursor file (Xcur magic, following symlinks), `cursor.theme` is present, no entry is a dangling symlink, and the expected-name list stays in sync with the enum's 34 variants. |
||
|
|
88385e14b2 |
add Carousel widget and WrapGrid::centre_last_row
Forge's app switcher needs two layouts the existing widget set didn't cover. The desktop grid wants a partial last row centred under the rows above (3 tiles → row 1: two, row 2: one centred) so a 7-of-9 leftover band reads balanced rather than left-aligned. The mobile variant wants a horizontal carousel where the focused tile sits centred in the viewport at a configurable fraction of its width and its neighbours peek out on the sides at a fixed gap.
Extend `WrapGrid` with `centre_last_row( bool )`. When set, layout offsets a row that has fewer than `columns` children by `(missing * (cell_w + spacing)) / 2` so it stays centred inside the content rect. Defaults to false; every existing call site continues to land tiles flush-left. Covered by three layout tests (centred partial row, full row no-op, off-by-default).
Add the `Carousel` widget at `src/widget/carousel/`. It is a pure layout primitive: `focused_width_frac` (0.05–1.0, clamped), `gap` and `offset` are owned by the caller, leaving drag / inertia / snap policy to the host so the compositor can plug in its existing touch pipeline. Each child gets a rect at `base_x + idx * (child_w + gap)` and the full viewport height; `snap_offset( viewport_w, idx )` translates index to centring offset and `focused_index( viewport_w )` rounds the current offset back to the nearest tile. Plumbed into `Element::Carousel` with the matching arms in `widget/element.rs` and walker in `draw/layout.rs`; re-exported as `ltk::{ Carousel, carousel }`. Covered by nine unit tests (layout, offset shift, snap / focus round-trip, frac clamp, child height) plus a `cargo run --example carousel` demo with Prev / Next / arrow-key navigation against an external offset state. The example is wired into the `examples` Makefile target.
Updates the widget catalogue and the `widget/mod.rs` landing comment to list the carousel under "Clipping wrappers" and to mention `centre_last_row` in the grid section.
|
||
|
|
4a80165428 |
event_loop, a11y, text_shaping: AccessKit AT-SPI2 bridge, cross-app clipboard, xdg-activation, HarfBuzz shaping, multi-touch hooks
Five orthogonal capabilities land together because they share the same `try_run` plumbing: an optional global is bound at startup, a piece of state is added to `AppData`, the run-loop iteration drains an inbox / pushes a frame snapshot, and the public surface gains a small set of opt-in `App` hooks. Nothing here breaks an existing app — every new path degrades to a no-op when the compositor does not advertise the relevant global or when the platform adapter cannot start. AT-SPI2 accessibility via AccessKit. A new `src/a11y/` module owns the platform adapter and the inbound `ActionRequest` channel. `A11yState::try_new` constructs an `accesskit_unix::Adapter`; when the AT-SPI2 daemon is not on the session bus (headless CI, locked-down compositors) the constructor returns `None` and the rest of the pipeline runs unchanged. After every successful `draw_frame`, the run loop builds a fresh `accesskit::TreeUpdate` from `widget_rects` and pushes it through the adapter — main surface plus every visible overlay, each translated to global coordinates via `surface_offset_for` so screen readers report positions in the same frame the user sees. Buttons / toggles / checkboxes / radios / list items / sliders / text edits map to the matching `Role`s; `Click` and `Focus` actions are advertised on every interactive node; inbound action requests are drained at the top of each iteration and translated into a synthetic press / focus on the matching widget. The integration is documented as best-effort in `docs/architecture.md` under "Known gaps and non-goals": hierarchical nesting, per-widget accessible names, live regions and `Action::SetValue` are listed as the natural follow-ups that the foundation now supports but does not yet wire. Cross-application clipboard via `wl_data_device_manager`. A new `src/event_loop/data_device.rs` bridges the existing process-local `clipboard: String` to the Wayland selection. Outbound (Ctrl+C / Cut): after the local clipboard is populated, `publish_clipboard_selection` creates a `CopyPasteSource` offering `text/plain;charset=utf-8` and installs it as the seat's selection; `DataSourceHandler::send` writes the cached string into the fd the peer hands us. Inbound (Ctrl+V from another app): `DataDeviceHandler::selection` asks for the offered text via `WlDataOffer::receive`, spawns a tiny worker thread to drain the read pipe with a 16 MiB cap to prevent paste-bomb DoS, and posts the result back through an `mpsc::Sender` that the run loop drains each iteration into `data.clipboard`. The `clipboard:` field's doc-comment is updated to reflect the new behaviour: process-local when the compositor does not advertise the global, synchronised with the seat selection otherwise. External drag-and-drop reception. The same `data_device` module handles `DragOffer` enter / motion / leave / drop_performed: `on_drop_motion( x, y )` fires while the drag hovers over the surface, `on_drop_leave()` when it withdraws without dropping, and `on_drop_received( x, y, mime, text )` when an external payload (`text/uri-list`, `text/plain`, …) is released on top of an ltk window. The receive path reuses the same worker-thread / channel pattern as the clipboard so the run loop never blocks on the read fd. Three new `App` hooks expose the events with no-op defaults; apps that ignore them get the previous behaviour. `xdg-activation-v1`. The global is bound optionally; when it is present, `try_run` reads `$XDG_ACTIVATION_TOKEN` from the environment, removes it immediately (single-use; preventing leaks into child processes) and stashes it on `AppData::activation_token_pending`. After the first successful configure of the main surface — the earliest point at which `xdg_activation_v1.activate` is meaningful — the token is consumed once and the surface raised to focus. Compositors without the global leave `activation_state` as `None` and the inbound path silently degrades. An `App::request_activation_token` outbound path is reserved on the trait but not yet exercised here. HarfBuzz shaping. A new `src/text_shaping.rs::shape_line` drives both renderers: the logical-order string is run through `unicode-bidi`, split into per-font sub-runs, and shaped through `rustybuzz`. Each `PositionedGlyph` carries the per-font `glyph_id`, the visual advance and the ink offsets — exactly what `fontdue::Font::rasterize_indexed` needs to render Arabic connected forms, Devanagari clusters and CJK shaped glyphs correctly. The GLES atlas is re-keyed on `(glyph_id, size_bits, font_id)` so glyphs from different fonts at the same size no longer collide, and the atlas format is selected per ES profile (`GL_R8` / `GL_RED` on ES3, `GL_LUMINANCE` on ES2) — the fragment shader samples `.r` for both, since `GL_LUMINANCE` replicates the coverage byte into `.r=.g=.b`. Software path follows the same key. New `Cargo.toml` deps: `unicode-bidi = "0.3"`, `rustybuzz = "0.14"`. Multi-touch hooks. `App::on_touch_down / on_touch_move / on_touch_up( id, x, y )` expose the raw `wl_touch.id` of every secondary finger. The first finger to land remains the *primary slot* and is fed through the regular gesture machine (`on_pointer_*`, swipe, scroll, long-press, drag-and-drop). Every additional finger fires the new callbacks instead, leaving the existing single-slot behaviour untouched for apps that do not override them. This is the substrate for app-defined pinch-zoom / two-finger pan; the toolkit itself does not yet ship a built-in pinch gesture (called out in the same "Known gaps" doc section). `event_loop::frame` extracted from `draw/mod.rs`. The `draw_frame` orchestrator and its per-format SHM helper (`pick_shm_format`) move into `src/event_loop/frame.rs`, leaving `draw/` strictly responsible for per-surface paint primitives. The import in `event_loop/run.rs` is rewritten accordingly; `draw/mod.rs` shrinks from 192-line orchestrator to a thin module index. Overlay teardown safety. `AppData::discard_overlay( id )` synchronously removes a destroyed overlay from the map and rewrites every per-device focus that pointed at it (pointer, keyboard, every touch slot), migrating an in-flight long-press drag to the main surface the same way `reconcile_overlays` does. Used by the compositor-driven destruction paths (`PopupHandler::done`, `LayerShellHandler::closed`) where waiting for the next reconcile would leave a window in which `surface()` / `surface_mut()` panic. The non-panicking siblings `try_surface` / `try_surface_mut` are added for callers on async dispatch paths (IME `Done`, tooltip arm) that may race a teardown. Miscellaneous. CI: `master` → `main` to match the actual default branch. `Makefile` adds `cargo run --example dialog` to the examples target. `src/lib.rs` re-exports `widget::scroll::ScrollAxis` so apps can configure a `scroll()` axis without reaching into a `pub(crate)` module. `Cargo.toml` adds `accesskit = "0.17"` and `accesskit_unix = "0.13"`. `docs/architecture.md` gains the "Known gaps and non-goals" section that enumerates the new capabilities, what still ships flat, and what is deferred (per-widget a11y labels, primary selection, intra-process multi-touch gestures, `wp_fractional_scale_v1`). |
||
|
|
bfe27b6fef |
event_loop, widget, input: pointer-dwell tooltips, global drag coords, foreign-toplevel name cascade
`Button::tooltip( text )` registers a hint string that fires after a 600 ms pointer dwell. `LaidOutWidget` gains a `tooltip: Option<String>` field, `Element::tooltip()` exposes it to the input layer, and the existing pointer-hover path now calls `arm_tooltip` on hover-enter and `cancel_tooltip` on hover-leave / touch. The deadline is polled alongside `next_long_press_wakeup` in `try_run` so an idle pointer still gets a wake-up at the firing instant; on fire, `tooltip_overlay()` synthesises an `OverlaySpec` — a rounded `text_primary @ 95%` pill drawn with `bg` text — anchored above the hovered widget, flipping below or clamping inside the screen if it would clip, and pushed alongside the app's own overlays both in the redraw path and in `reconcile_overlays` so the layer surface is created the same frame the tooltip becomes visible. Pointer-only by design: touch events explicitly cancel because a tap-and-release should never linger into a hint. The `showcase` example wires `.tooltip(..)` on the three button variants as a smoke test. The drag pipeline now reports positions in main-surface (global) coordinates instead of per-surface. `surface_offset_for( focus )` derives the top-left of an overlay surface from `SurfaceState::layer_anchor` — newly stored at `reconcile_overlays` time from `OverlaySpec::anchor` — combined with the main surface's dimensions; `on_drag_move`, `on_drop`, the synthetic move emitted on drag-promotion in `pointer.rs`, and the `pending_drag_inits` push site in `gesture::start_drag` all translate before handing coordinates to the app. The motion and release paths additionally `request_redraw()` every overlay so a dock-style drop target painted on an `Anchor::Bottom` layer surface gets repainted as the drag moves — without that, the visible drop indicator only updates when the cursor re-enters the main surface. Drops still target whichever surface fired the release; only the coordinates are unified. `ForeignToplevelListHandler` previously read `app_id` directly via `ForeignToplevelList::info()`. Clients that never set `app_id` (some winit-windowed compositors, simple test clients) were silently invisible to crustace-style docks because the empty string fell through `unwrap_or_default()` and the dock then keyed entries off `""`. `toplevel_display_id()` cascades: prefer `app_id` for desktop-entry matching, fall back to `title` for human-readable identification, and finally to the protocol-issued `identifier` which is always present and unique per handle. Applied to both `new_toplevel` and `update_toplevel`. `theme::system_fontdb()` lazily loads the system font database once via `OnceLock` and reuses the `Arc` for every `decode_svg_bytes` call. resvg's default `Options::fontdb` is empty, so any SVG containing `<text>` rendered with the built-in fallback font or no font at all; with the system DB attached, icons and decorative SVGs with embedded labels now resolve glyphs correctly. Cached because `load_system_fonts()` walks every font path on the system and is comfortably tens of milliseconds on a cold cache — not something to repeat per icon decode. `themes/default/theme.json` tweaks one variant's slot palette: `surface-alt` from `@indigo/D9` to `@white/D9` and `text-primary` from `@white` to `@navy`, plus a cosmetic re-alignment of the `"value"` columns in the same slots block. |
||
|
|
821037f509 |
container, text, date_picker: width-aware sizing pass
`Container::max_width(px)` mirrors the same flag on `Column` / `Row` — the container reports `min( offered, px )` upwards and the draw pass caps `rect.width` to `px`, so a decorated child wrapped in `container().max_width(260)` no longer needs a `column()`-of-one shell just to access the cap. Propagated through `map_msg`, covered by a test, and documented under the `container` section of `docs/widgets.md`.
`Text::no_truncate()` opts out of the default ellipsis behaviour: when `truncate = false` the draw pass paints the full string even if `measure(text) > rect.width`. Useful for very short labels (calendar days "1"–"31", day-of-week stubs "Lu" / "Mi" / "Sá") inside grid slots whose width is dictated by the parent — a couple of pixels of overflow centred in the rect is invisible, while "..." in place of a single-digit number is loud.
`DatePicker::width(px)` is a layout hint: when set, `build()` derives `header_fs`, `dow_fs` and `day_fs` from the slot width so the worst-case label in each row ("September 2026" in the header, "Mié" / "Sáb" in the DOW row, "30" in the day cell) fits at a sensible size; the design defaults stay as upper bounds. Day and DOW cells also flip on `Text::no_truncate()` as a safety net — heuristic font sizing can't perfectly predict glyph widths across families, and overflowing one pixel beats truncating to "...".
|
||
|
|
39fbafec24 |
container: generalise background from Color to Paint
`Container::background` now stores `Option<Paint>` instead of `Option<Color>`, and the builder accepts `impl Into<Paint>` so callers can pass a plain `Color` (auto-wrapped in `Paint::Solid` via the trait impl) or an explicit `LinearGradient` / `RadialGradient`. `layout_and_draw` switches from `canvas.fill_rect( rect, bg, corners )` to `canvas.fill_paint_rect( rect, &bg, corners )` to consume the wider type. No behaviour change for existing call sites — solid-colour containers keep working unchanged thanks to the `Into<Paint>` for `Color`. |
||
|
|
bbab5e238d | First commit. Version 0.1.0 |