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.
72 KiB
Changelog
All notable changes to ltk are documented here. The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
Unreleased
Added
-
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 theapp_id. AShellMode::Windowapp was titled "ltk" on Wayland, whatever the app, with no way to name it but the deprecatedwindow_config; in a browser its title was itsapp_id, and is now this. -
Text::fitandText::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 the rect's width and height, or, withwrap, all its lines within its height. A text whose length varies, such as a number, a total or a title, in a box whose size the window sets, so neither overflows the box nor needs its size worked out from the window and the length of its content. Its preferred size is still the one at its own size.valign( VAlign )places the lines at the top (the default), the centre or the bottom of a rect taller than they are, as a fitted text's may be. -
A text field as an editor:
TextEdit::highlights,TextEdit::tab_textandApp::take_text_command—highlights( Arc<Vec<TextHighlight>> )draws runs of a multiline value, byte ranges in order, in colours of their own, as a syntax highlighter marks keywords, strings and comments; what no run covers keeps the field's colour, and a run that does not fall on character boundaries is left out.tab_text( text )has Tab typetext, such as four spaces, 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.App::take_text_commandreturns once aWidgetIdand aTextCommandfor that field,Copy,Cut,Paste,SelectAllorSelect { start, end }, which the runtime carries out after the next frame, focusing the field first: a field's cursor and selection are the runtime's, so an app's Edit menu or its search had no way to copy what was selected or to select what it found.Selecttakes its offsets to character boundaries within the value and scrolls a multiline field to them.borderlessnow also leaves out a multiline field's background and border, as an editor filling its window wants. Tests cover Tab with and withouttab_text, selecting and copying, offsets inside a character, cutting, selecting all, and a run inside a character. -
A file chooser:
ltk::FileChooser— picks a file to open (FileChooser::open), or a folder and a name to save one as (FileChooser::save), overltk::files, alike natively and in a browser, so that an app need not make its own. The app holds it, putsview()in its own view and hands itsFileChooserMsgs toupdate(), which returnsChosen::PathorChosen::Cancelledonce the user chose;wrapmakes the app's message of the chooser's, so its listings and folders reach it through the app'sChannelSender. It shows 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; folders come first, then files by name with their size, each with its type's icon, the hidden ones left out.dir( path )starts elsewhere,filter( &[ "text/*" ] )shows only files of those types, andcompact( true )puts the places behind a button for a narrow screen. A button of its header sorts the folder as Crate does, by name, last access or size, each key in a direction of its own, folders always first, showing beside each file its size and the date of its last access or change;sort( Sort )starts in an order the app kept fromcurrent_sort(). Saving asks before replacing a file, and can create a folder;take_focus_request()gives the field to focus. Its strings are in ltk's locales;docs/widgets.mddescribes it andexamples/file_chooser.rsshows it.files::mounts()gives the folders mounted in a browser as the desktop last said, which the web backend now keeps. -
TextCommand::Reveal { start, end }—Selectwithout moving the focus: the field's bytesstart..endare selected and a multiline field scrolls to them while the keys stay where they were, as a search needs while its own field keeps them. A multiline field without the focus now shows its selection, dimmed (theme::selection_inactive), where it showed none. -
TextEdit::font( face )— a multiline field's text in a face of its own, such asltk::monospace_font()for an editor of code, at the field's size. Wrapping, drawing, the selection, the cursor, clicks, tab stops and the arrows all measure in it: they lay the text out through oneTextFont, the size and the face, whichtext_input_geometrygives the runtime's paths as it gave them the size.Canvas::measure_char_with_fontismeasure_charin a face other than the canvas's. -
ltk::monospace_font()— a monospace face for text that lines up in columns: DejaVu Sans Mono from the app's resources, which a page and an APK carry, else the first of the system's monospace faces installed (DejaVu, Hack, Noto Sans Mono; Android's Droid Sans Mono), loaded once;Nonewhen there is none. -
TextEdit::line_numbers( true )— a multiline field numbers its lines in a column at its left, in the monospace face when there is one, as wide as the number of the last line needs, a line that wraps 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, clicked, scrolled and moved through with the keys, since they all lay it out in the same rect (gutter::text_rect, whichtext_input_geometrygives). -
A multiline field's tabs, scroll bar and cursor:
TextEdit::on_cursor— a tab in a multiline value reaches the next stop, every four digits' widths, as four monospace columns are (theme::TAB_COLUMNS), when it is drawn, selected, clicked and crossed with the arrows, where it was drawn as the font's glyph for it, sotab_text( "\t" )makes an editor of tabs. A field whose lines do not all fit shows a scroll bar along its right edge: its thumb is held and dragged, or a press on its track brings the thumb there, by pointer or touch, without moving the cursor, and the pointer over it is the arrow rather than the text cursor. Page Up and Page Down move the cursor 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; Shift extends the selection, and with Ctrl, or in any other widget, the keys still reach the app.on_cursor( |at| … )sends the cursor's byte offset once it is placed and whenever it moves or the value changes length, as an editor's status bar showing the line needs. -
ltk::files::callback— the operations ofltk::fileswith a function in place of a message (list,stat,read,read_range,write,create_dir,rename,remove,remove_all,copy,copy_all,mountandunmount), for code with no run loop'sChannelSenderto take one, such as Prompt's session daemon, which in a browser runs in the page beside the window.doneis called on ltk's worker thread natively and on the page's thread in a browser. -
The clipboard and the primary selection between a browser desktop's applications — on a page with forge/web, what an app copies (
publish_clipboard) goes to forge/web as aset_selectionrequest as well as to the browser's clipboard, and the selection the middle button pastes (publish_primary_selection, which did nothing in a browser) asset_primary_selection. forge/web'sselectionandprimary_selectionevents, which every client hears when one sets them and each hears on connecting, becomeAppData::clipboardandAppData::primary_selection, as another client's selection does on Wayland, so copying, and selecting and pasting with the middle button, work between the page's applications whether or not the browser lets a page write its clipboard. Apastefrom the browser still brings what other applications copied; a browser has no primary selection a page can reach. -
The user's files, alike natively and in a browser:
ltk::files— asynchronous operations on the user's files whose results reach the app as messages through itsChannelSender:list,stat,read,read_range,write,open_write,create_dir,rename,remove,remove_all,copy,copy_all,mountandunmount, each taking the sender and adonethat turns theio::Resultinto a message. Natively they runstd::fson a worker thread of ltk's, and each copy on a thread of its own; in a browser they are forge/web'sfs_*requests over the app's connection, contents travelling as aUint8Arraybeside the JSON as the second argument of the connection'srequestand of the client's event handler, and what is asked before the app connects is sent once it has, or fails withUnsupportedon a page without a desktop. Contents travel in chunks: a large file is read withread_rangeand written through aWriter, chunk by chunk,files::CHUNK(1 MiB) at a time, and its contents appear only once the writer is closed, a new file showing empty until then in a browser; natively it is written beside the file and renamed over it, and a writer dropped unclosed leaves the file as it was. A copy runs where the files are, reports its progress every 100 ms, never replaces an existing file, and, cancelled through itsOperationor failed, leaves nothing behind. Errors are the sameio::ErrorKinds on every platform.EntryandMetadatagive a file's kind, size and times, the access time natively only.mountasks the browser for a folder of the user's computer, from the user's press, and fails withUnsupportednatively.App::on_files_changed,App::on_mounts_changedandApp::on_open_fileshear of forge/web'sfs_changed,fs_mountsandopen_files.files::mime_type( name )gives a file's type by shared-mime-info'sglobs2, the resource directory'smime/globs2first, whichscripts/android-assets.sh, and soweb-assets.sh, now copies into the tree with shared-mime-info'ssubclasses(MIME_DIRoverrides where from), then the system's.renamenever replaces what is atto, natively as in a browser. Tests cover writing, aborting, listing, ranges, removing, copying a tree with its progress, a cancelled copy, a copy that would replace, and the globs. -
ltk::web::shell_request( json )— a shell message that waits for forge/web's reply, such asget_window_thumbnail: sent asshell_sendsends one and answered at once with forge/web's reply as JSON, orNonebefore the app has connected, since a request is not kept for later, and when forge/web replies nothing. A shell's requests that wait for an answer natively got none in a browser. -
Canvas::draw_image_keyed( key, rgba, w, h, dest, opacity )—draw_image_datawithkeynaming the pixels in place of a fingerprint of them, so a caller that draws the same images on every frame, such as a terminal's glyphs, does not hash every one of them each time. Every draw with the samekeymust pass the same pixels, which are read only when the image is not cached; a key never names an image drawn by content. -
Fullscreen in a browser's desktop — on a page with forge/web,
App::fullscreenandApp::start_fullscreenask forge/web for the main window to fill the desktop with aset_fullscreenrequest, as Wayland'sxdg_toplevel.set_fullscreen, where they did nothing; alone on a page the app's canvas still takes the browser's fullscreen. -
A browser app hears of the desktop's windows — on a page with forge/web, the web backend asks for the list of toplevels when it connects and turns its
toplevel_openedandtoplevel_closedevents intoApp::on_toplevel_event, as the Wayland backend does withext-foreign-toplevel-list-v1, so a shell's dock knows which applications have windows open and names each by itsidentifier. -
A browser app tells the desktop it is shown — on a page with forge/web, each surface's first presented frame is reported with a
presentedrequest, as a Wayland client's first commit followsxdg_activation_v1.activate, so forge/web knows when a window it launched has shown and reportsapp_activatedwith its token, which dismisses crustace's splash. -
An app's own files in a browser:
ltk::vfsandltk::web::load_tree—vfs, through which ltk reads its themes, fonts and layouts, is public:read,read_to_string,read_dir,is_fileandis_dirare the file system's natively and, in a browser, read the files the page loaded into memory.load_tree( base, root ).awaitloads any tree served with aMANIFEST, as ltk's resources are, underroot, andload_resourcesis nowload_treeinto/ltkand that directory made the resource directory. crustace loads its catalog of applications and their icons this way. In a browser the activation tokens an app asks for withApp::take_activation_requestsare now issued: forge/web's, through itsactivation_tokenrequest, or empty without a desktop, as on Wayland without xdg-activation, handed toApp::on_activation_tokenand their messages processed before the app draws. Before, a browser app that launched others through tokens waited for one that never came. -
The shell protocol in a browser:
ltk::web::shell_sendandltk::web::set_shell_listener— a shell such as crustace speaks to forge over a socket in JSON; on a page with forge/web the same JSON travels over the app's connection to it,shell_send( json )as ashellrequest and eachshellevent's message to the listenerset_shell_listenergives, as JSON. What is sent before the app has connected is sent once it has, so a shell can tell forge/web its theme while it starts. An overlay's role now carries itsApp::overlay_namespace, as its layer-shell namespace does on Wayland, so forge/web can tell crustace's launcher and Quick Settings apart. -
scripts/wasm-bindgen-cli.sh TOOLS_DIR— builds wasm-bindgen-cli intoTOOLS_DIR/binat the version of thewasm-bindgencrate ltk pins in itsCargo.toml, from its sources with a lock resolved for Debian's rustc, which is whatmake web-depsdid inline.make web-depsruns it, and sincemake installcopiesscripts/into the registry, a project that builds for the browser against the installed ltk, such as forge/web or crustace, gets the same CLI without a checkout of ltk. -
ltk::time::Instant— ltk's monotonic clock,std::time::Instantnatively andweb_time::Instanton wasm32, wherestd::time::Instant::nowpanics, so an app that also builds for the browser measures time with the same clock as ltk and asosk::Panel::press_down, which takes it. -
A browser app joins the page's desktop — on a page where forge/web has published
globalThis.forgeWeb, the web backend connects to it as the app'sapp_idand asks for its main surface in the role the app's hooks give (shell_mode,layer_anchor,layer_size,exclusive_zone,keyboard_exclusive,window_size_hint), as a Wayland compositor would read them; it draws into the canvas forge/web gives, listens on the input element above it, takes its size and scale from forge/web'sconfigure, sends its input region when it changes, and leaves the keyboard focus to the desktop. AclosereachesApp::on_close_requested, and on exit the surface is destroyed and the connection closed. The app's overlays become surfaces of the desktop asApp::overlays()changes, layer surfaces or, forLayer::Window, windows, with their sizes and zones in logical pixels and anupdate_rolewhen one changes; forge/web'scloseof one delivers itson_dismiss. Popups anchored to a widget and the tooltip wait for a popup role. What every backend does as overlays come and go, resolving anOverlaySpec::sizeand moving a drag and the focuses off an overlay that goes, moved out of the Wayland backend intoevent_loop/overlays.rs(AppData::overlay_size,AppData::remove_overlay). The app's subsurfaces become surfaces of the desktop too, each sized to its parent and taking no input, painted on one shared GPU canvas only when it appears, its size changes or itscontent_versiondoes, and otherwise moved with anupdate_role, which the browser composes without painting;App::on_subsurface_committedfollows each paint, and while the app animates by subsurface motion only (App::subsurface_motion_only) a parent's frames keep coming without painting it. Painting a view on a canvas with no focus, hover or press, which both backends' subsurfaces do, moved out of the Wayland backend intodraw::paint_detached. A lost WebGL context now drops the overlays' and subsurfaces' GPU canvases too, not only the main surface's.examples/web_shell.rsis a shell for the desktop, a background with a bar in the top layer, a panel that takes the keyboard and a card on a subsurface that slides. A page without a desktop, or one that named a canvas withset_canvas, runs the app as before. The newRunError::Desktopsays forge/web refused the main surface. Every surface now paints through the app's one WebGL2 context, on anOffscreenCanvas, and its frame moves to the surface's canvas, abitmaprenderer, as anImageBitmap: a page holds few WebGL contexts, and a desktop's apps have many surfaces. -
Browser backend, the
webfeature —ltk::runon a page's canvas, built forwasm32-unknown-unknownwith wasm-bindgen, js-sys and web-sys, optional dependencies on that target only.build.rsrefuses it with another backend or for another target, so a web build usesdefault-features = false, features = [ "web" ]. A page's main thread must not block, sorunsets the app up and returns, andsetTimeoutandrequestAnimationFramedrive the loop;Sources(event_loop/sources.rs) gains a queue of timers and channels for it, and page events andChannelSender::sendonly queue and schedule an iteration. The app draws on a canvas laid over the whole page, or the oneltk::web::set_canvasgives, through WebGL2 with the GLES canvas unchanged, or without it in software throughputImageData; its logical size is the canvas's CSS size, its scale the page'sdevicePixelRatio, fractional as it may be, so the canvas holds the display's own pixels. Panics go to the browser's console. When the app exits, the canvas receives anltk-exitevent. The newRunError::NoDocumentis returned where there is no page, as in a worker. The built-in keyboard serves text where the page is used by touch. Input comes from the canvas's Pointer Events, a mouse as the pointer and fingers and pens as touches, atoffsetX/offsetYin CSS pixels; a press captures the pointer and focuses the canvas. A wheel in lines scrolls in detents, one in pixels continuously, and the page neither scrolls nor zooms under the canvas. Keys take their keysym fromKeyboardEvent.key, their code from.codeas the Linux input code Wayland reports, and repeat on the browser's clock; AltGr types its character rather than counting as Ctrl and Alt. ltk takes every key the browser gives a page, and while fullscreen also thosenavigator.keyboard.lock()gives where there is one. The cursor's shape is the canvas's CSScursor, and on macOS Command counts as Ctrl. Copies go tonavigator.clipboard.writeText; a page reads the clipboard only from apasteevent, so Ctrl+V is let through to the browser and held until itspastehas put the text inAppData::clipboard, and a paste from the browser's menu comes as a Ctrl+V of its own. A lost WebGL context drops the GPU canvases and draws nothing until it is restored, on a newglow::Context, so the glyph atlas and the caches of the old one are not reused. On ltk's own canvas the page'senv( safe-area-inset-* ), read from a hidden element, become the main surface's insets. Resources are the tree an Android package carries, whichscripts/web-assets.shbuilds throughandroid-assets.shandmake web-assetsbuilds intotarget/web/ltk/with the in-tree default theme and the fallback fontsWEB_FONTSnames;ltk::web::load_resources( base ).awaitfetches itsMANIFESTand every file it lists, with the manifest's hash in their URLs so a changed tree is not served from the browser's cache, into memory under/ltk, which becomes the resource directory. The per-script fallback fonts are now read throughresources::read_font, so a path that does not exist is found by file name in the resource directory'sfonts/, which is where a page's are.wasm-bindgen-futuresjoins thewebfeature.examples/web_hello.rsis the smoke test:make web-checktypechecks the backend,make web-examplebuilds the example and runs wasm-bindgen intotarget/web/,make web-serveserves it, andmake web-depsinstalls Debian's std for wasm32 and wasm-bindgen-cli at the version ofCargo.lock. The text-scale watcher does not run in a browser, and the built-in keyboard's language is the page's. Not yet: overlays, secondary surfaces and subsurfaces, dead keys and input methods, the accessibility tree, the primary selection, and a canvas that changes size while the window does not. -
The built-in keyboard serves an app's own text input — while
App::external_text_inputasks for text and no ltk text field has the focus, the built-in keyboard shows withApp::external_text_purpose(a terminal gets its row of Escape, Tab, Ctrl, Alt and arrows), comes back when the serial changes, and hides when the request ends. Its keys reach the app as physical ones, pressed and released throughApp::on_raw_key_eventwith the panel's Ctrl and Alt held for them. Before, the built-in keyboard only served ltk's text fields, so such an app had no keyboard where the compositor offers no input method. -
Android: the system clipboard, back that leaves, ltk's keyboard,
HOMEandLANG— copies go to Android'sClipboardManagerand its text is read back whenever the app gains the focus, through JNI on the activity (thejnicrate joins theandroidfeature). Back with nothing left to go back to minimises the app withmoveTaskToBack, or closes it, asApp::back_at_rootsays; the newPlatform::back_leavesdecides whether back may leave the main surface, which keeps Wayland's rule of the xdg-toplevel. Text is typed on ltk's own keyboard rather than the system's, from which a NativeActivity gets only key events.set_android_appsetsHOMEto the app's internal storage and, unless set,SSL_CERT_DIRto Android's CA certificates andLANGto the device's language, so desktop code finds its preferences, a TLS client its roots (rustls-native-certs knows no Android path) and the locale. -
Android: running in the background and closing —
ltk::keep_running( Some( text ) )starts the keep-alive service of an APK packaged withKEEP_ALIVE=1, a foreground service withtexton an ongoing notification, so Android does not freeze the app once it leaves the screen;Nonestops it, and without the service nothing happens. When the app's last surface closes, ltk callsfinishAndRemoveTaskand waits for the activity to be destroyed: a second activity that Android starts in a process whoseandroid_mainreturned never comes up, so the app should end the process afterrunreturns rather than wait to be started again.set_android_appsets up the process only on its first call and keeps the latest activity on every call. -
ltk::resource_dir()— the resource directoryset_resource_dirgave, so an app finds its own bundled files (a terminal's monospace face) among the resources unpacked from the APK.make installnow copiesscripts/andjava/into the registry, so apps package with the installedandroid-apk.shandandroid-assets.sh. -
Surface insets —
SurfaceState::insetsholds the logical pixels the system covers along each edge of a surface; the view is laid out inside them while the background still fills the whole surface, and positions stay absolute, so hit testing is unchanged. The built-in keyboard is placed inside them too. On Android 11 and later they come from the window'sgetRootWindowInsets(): the status bar and any cutout on top, and on the other edges only bars that take taps, so gesture navigation leaves no gap at the bottom while a bar of buttons keeps its room; earlier releases fall back to the activity's content rect. On Wayland they stay zero. -
APKs with Debian's own tools —
scripts/android-apk.sh OUT.apk LIB.so PACKAGE LABEL [ASSETS_DIR]packages an app built as acdylibinto a NativeActivity APK: it writes the manifest (the library loaded aslibmain.so, optional permissions, a theme without an action bar, which the content rect would otherwise leave room for), compiles it withaaptagainstandroid-framework-res, adds the assets and the library, aligns withzipalignand signs withapksigner, creating a debug keystore in~/.android/when there is none; no Gradle or Android SDK download is involved.ICONgives the launcher icon, an SVG rasterised withrsvg-convertfor every density, or a PNG.KEEP_ALIVE=1adds the only Java code,java/net/liberux/ltk/KeepAlive.java, compiled withjavacanddx(art-standalone) against the platform 34android.jar(google-android-platform-34-installer), which aapt then links against too, as Debian's framework-res predates thespecialUseservice type; the service is declared withKEEP_ALIVE_USEas its reason, with the permissions it needs.examples/android_hello.rs, a counter built as acdylibonly with theandroidfeature, is the smoke test:make android-apkbuilds and packages it with the in-tree default theme,make android-installinstalls it throughadb. On Android,set_android_appnow also sends standard output and error, panics included, to logcat under the process's name. -
ltk::set_resource_dir( dir )— an app's own copy of ltk's resources, for a package with no system to install them into. Itsthemes/is searched after the user's themes and before/usr/share/ltk/themes, and itskeyboards/before the system's layouts; a font a theme names by an absolute path that does not exist is read from itsfonts/by file name, so a theme written against Debian's font paths works unchanged from the bundle. -
Themes and fonts in an Android package —
set_android_appunpacksassets/ltk/into the app's internal storage and uses it as the resource directory.assets/ltk/MANIFESTlists the tree, one path per line, since the asset manager cannot list subdirectories; the tree is unpacked again only when the manifest changed, and its first line is a hash of the contents.scripts/android-assets.shbuilds the tree from installed themes, without their cursors, and copies the fonts eachtheme.jsonnames and ltk's keyboard layouts (KEYBOARDS_DIRoverrides where they come from);make android-assetsruns it on the in-tree default theme. On Android the per-script fallback fonts are read from/system/fonts, and the primary face is the embedded Sora.make android-checktypechecks the Android backend with Debian's own rustc,rust-srcandgoogle-android-ndk-r27c-installer, through-Zbuild-std;NDK,ANDROID_APIandANDROID_TARGEToverride its defaults. -
Android backend, the
androidfeature —ltk::runinside a NativeActivity through android-activity 0.6 and ndk 0.9, optional dependencies that build only for Android targets. The feature excludeswayland, andbuild.rsrefuses both at once, so an Android build usesdefault-features = false, features = [ "android" ]. The app'sandroid_mainpasses itsAndroidApp(re-exported asltk::AndroidApp) to the newltk::set_android_appbeforeltk::run; without itrunfails with the newRunError::NoAndroidApp. The backend draws through EGL on the activity'sANativeWindow, witheglSwapBufferswaiting for the display, or in software into a locked window buffer; sizes the main surface to the window at the display's density in whole multiples of 160 dpi; turns fingers and styluses into touches, a mouse into the pointer and key events into keysyms and text through the device's character map; and types on ltk's own keyboard. calloop's descriptor rides on the activity'sALooper, so the app's channel and timers keep working. Not yet: overlays, secondary surfaces and subsurfaces, dead keys from a physical keyboard and the activity's saved state.EglContextgainsnew_android,create_native_surfaceandrelease_current, and swaps with the interval its backend needs;EglSurface::egl_windowandresizeare Wayland's only. What a frame callback does is now the backend-neutralAppData::frame_presented, which both backends call.session_state.rsbuilds only withwayland, its only user, and so do the AT-SPI2 adapter's construction and action forwarding. The state only the Wayland backend reads moved out of the runtime's: the clipboard inbox, a drag's position and type, the first-configure latches (pending_fullscreen,pending_size_hint_unpin,pending_size_hint_fit,fitted_size_hint) and the overlay canvas pool fromAppDataintoWaylandPlatform, andtitlebar_basefromSurfaceStateintoWaylandSurfaceState. A build with another backend does not warn about runtime code only Wayland drives; the Wayland build still does. -
waylandfeature, on by default — the Wayland backend and its dependencies (smithay-client-toolkit, wayland-protocols, wayland-egl, wayland-scanner, calloop-wayland-source, and accesskit_unix for AT-SPI2 accessibility) are now optional. A newbuild.rssetscfg( backend )when some backend is enabled, which builds the runtime:run,try_run,RunErrorand everything behind them. With--no-default-featuresonly the runtime-free core builds (UiSurface, widgets, layout, theming, rendering,EglOffscreenContext);EglContextandEglSurfaceneedwayland. The backend in use is named byplatform::NativePlatformandplatform::NativeSurface.make installnow copiesbuild.rsinto the registry. Builds with default features are unchanged. -
TextEdit::color( c )/TextEdit::on_focus( msg )—colorsets the colour of the value, which always came from the theme; the placeholder keeps the theme's.on_focusemits a message when the field takes the keyboard focus (press, Tab,take_focus_request), once per change and not for a press on the field that already holds it.App::on_text_input_focused( bool )only says that some text field is focused; an app that mirrors focus into another model (rustdroid keeps Android's focused view in step) needs to know which, and a per-widget message carries that without aWidgetId, which is a&'static strand cannot name a dynamic handle. The message rides theWidgetHandlers::TextEditsnapshot and is pushed fromset_focus. -
External text input:
App::external_text_input(),on_external_text_commit,on_external_text_delete— lets an app hold the input method for a text field ltk does not own: an editable inside an embedded web view, a guest toolkit's field. Whileexternal_text_inputreturnsSome( serial )and no ltk text widget has the focus, the runtime keeps azwp_text_input_v3enabled, so the compositor raises its on-screen keyboard, and routescommit_string/delete_surrounding_textto the two new hooks instead of to a widget; a change ofserialrenews theenable, which is how a host asks for a keyboard the user folded to come back. Polled after every batch ofupdates throughAppData::sync_external_text_input; an ltk text widget that takes the focus wins and hands the text-input back when it loses it. Keys such as Backspace and Enter keep arriving as key events. DefaultNone. -
App::on_back()/BackResponse— the app's answer to a back request (the shell's back button or gesture, delivered asXF86Back, or a mouse'sBTN_SIDE/BTN_BACK). Back peels one layer per request: transient UI the runtime owns (xdg-popups, context menu, text selection, a dialog'son_escape), then text-input focus, thenon_back, thenon_key_with_modifiers( XF86_Back )for apps that bound the key themselves, thenback_at_root.BackResponse::Messagecarries the message that undoes the top-most piece of navigation state,Consumedis for hosts whose back stack lives elsewhere (an embedded Android runtime),Unhandledmeans the root. DefaultUnhandled. -
App::back_at_root()/BackAtRoot— what back does with nothing left to go back to:Minimize(default;xdg_toplevel.set_minimized, the app and its state stay alive),Close(throughon_close_requested) orIgnore. OnlyShellMode::Windowsurfaces react; layer-shell and session-lock surfaces never do. -
NavStack<R>— navigation state for an app made of screens, kept in the app's model:push/pop/replace/pop_to_root/reset/current/iter, never empty, serialisable wheneverRis.back( msg )answerson_back, and persisting the same value fromsave_statemakes back retrace the same screens after a session restore. -
docs/navigation.md— the navigation-state model: transient UI versus navigation state, the back order and its rationale, why the answer is derived instead of recorded, how it lines up withsave_state, embedded web views, foreign back stacks, and an author's checklist.architecture.md,onboarding.mdand the cookbook's multi-screen and session-state recipes point to it; the multi-screen recipe now usesNavStack. -
App::claims_raw_touch_at( x, y )— per-press variant ofclaims_raw_touch: a finger landing where the app returnstruerides the rawon_touch_*stream through the auxiliary-slot path and clears the widget focus, while fingers landing elsewhere keep driving the gesture machine. Lets an app embed a self-contained input consumer (a WebView) next to regular widgets. Only consulted for the main surface; defaultfalse. -
App::fullscreen()— desired fullscreen state of the toplevel at runtime, polled after every batch ofupdates; a change is forwarded asxdg_toplevel.set_fullscreen/unset_fullscreen. The built-in titlebar is not painted while the compositor reports the window as fullscreen. DefaultNone(window state left alone). -
PatternGrid::size( impl Into<Length> )— explicit side for the square grid, resolved throughCanvas::resolve_geomlike the other geometry overrides; without it the theme's fluid default still applies. A lock screen can now size the grid from the surface (a share of the width in portrait, of the free height in landscape) instead of being capped at the default. -
xdg-session-management-v1(client) — before the first commit of aShellMode::Windowtoplevel the runtime bindsxdg_session_manager_v1, issuesget_session( reason, stored_id )andrestore_toplevel( toplevel, "main" ), so a supporting compositor restores window geometry on every launch. Bindings are generated in-tree from the vendored XML underprotocols/withwayland-scanner(src/protocol/). -
Runtime session persistence —
$XDG_STATE_HOME/<app_id>/session.json(compositor session id, clean-exit marker, pid) plusstate.bin(the bytes fromApp::save_state), written atomically with mode0600; saved every 30 s when the bytes changed, on close and on signal; handed back throughApp::restore_statebefore the first frame only on a session restore (LTK_SESSION_RESTORE=1) or after an unclean exit. Modulesrc/session_state.rs. -
Layer::Windowoverlays — anOverlaySpecwithlayer: Layer::Windowis materialised as an ordinaryxdg_toplevelof the given fixed size instead of a layer-shell surface: decorated, stacked and listed by the compositor like an application window. Anchor, exclusive zone and keyboard exclusivity are ignored; the compositor's close button deliverson_dismiss.App::overlay_title( id )(defaulted to the app id) names it. Layer-shell apps now bindxdg_wm_basewhen available so they can open one. -
Clean exit on
SIGTERM/SIGINT— the runtime installs a calloop signal source and leaves the event loop instead of dying, sosave_stateruns andltk::runreturns. -
Slider::on_release/VSlider::on_release— fired once with the final value when the drag ends, so an app can keep an expensive commit (a subprocess, a D-Bus round trip, a compositor reconfigure) off the per-motionon_changepath and still move the thumb live. The gesture machine emits it from the slider branch ofon_release;on_changealone behaves exactly as before. -
Viewport::local_viewport()— resolve the child's viewport-relative (vw/vh/vmin) and fluidLengths against the viewport's own rect instead of the root layout viewport the sub-canvas inherits. For fixed-size floating mini-UIs (a phone-shaped panel pinned to a corner of a desktop-wide surface) whose content is calibrated against the panel rect; scroll-like clips should keep the default inheritance. -
ListItem::height( impl Into<Length> )/ListItem::font_size( impl Into<Length> )— override the theme row height (floored at the label's rendered height so text never clips) and the primary-label font size, mirroring theToggle/Radioheight()builders, so dense menus can trade the touch-target generosity for row density. -
Accessibility text scale —
set_text_scale/text_scaleglobal multiplier (clamped[0.5, 3.0]) applied to every resolved font size (Canvas::resolve_fontand the stock-widgetfont_pxpath); geometry is untouched. The run loop readsorg.gnome.desktop.interface text-scaling-factorat startup and follows external changes via agsettings monitorwatcher thread, repainting on change — every ltk app tracks the desktop's "large text" setting live with no app-side wiring (silently fixed at 1.0 whengsettingsis missing). Embedders drivingcore::UiSurfacecallset_text_scalethemselves. -
Per-canvas pixel density —
Canvas::set_densitypins a canvas (and the sub-canvases derived from it) to its own density factor forLength::dpresolution, overriding the processset_densityglobal;Canvas::densityreads the effective value. NewCanvas::resolve_geom/Canvas::resolve_fontresolve an explicitLengthin geometry / font space with the canvas' density — widgets now route caller-supplied lengths through them, so adpoverride follows the canvas it draws on. The hook for surfaces on outputs whose DPI differs from the process-wide one (an overlay on a second monitor, an embedder with severalUiSurfaces). -
Length::resolve_with_density—Length::resolvewith an explicit density forLengthBase::Dp, instead of the processdensity(). -
docs/backends.md— the canonical software/GLES capability matrix: what renders identically, what degrades gracefully on software (gradients, shadows, backdrop blur, bottom fade) and what is GPU-only, replacing per-method rustdoc archaeology. Linked from README, onboarding and the architecture known-gaps list. -
Clippy in CI — the
testjob lints withcargo clippy --workspace --all-targets --features test-support -- -D warnings;make clippymirrors the invocation locally. -
ListItem::trailing_icon( rgba, w, h )— right-aligned icon slot (disclosure arrow) drawn atTRAILING_ICON_SIZE(21 px) and vertically centered, alongside the existing leadingicon. Coexists withtrailingtext, which shifts to the icon's left. Symbolic icons should be pre-tinted by the caller (tint_symbolic), matching the leading-icon contract. -
ListItem::pad_h( impl Into<Length> )— per-item override of the horizontal inset between the row edge and its content; without it the theme default (16 px) applies. -
Row::align_top()/Row::fill_height()— pin children to the row's top edge instead of the default vertical centering, or stretch every non-spacer child to the row's inner height (the row itself is still sized by its tallest child), for siblings whose natural heights differ by a few font-metric pixels. -
App::window_resizable()— returnfalseto keep themin_size == max_sizepin fromwindow_size_hintfor the toplevel's lifetime, declaring a fixed-size window compositors must not resize. Defaulttrue. -
App::claims_raw_touch()— returntrueto receive the primary finger through the rawon_touch_down/move/upstream, bypassing the built-in single-slot gesture machine entirely (widget presses, taps and swipes stop working on touch), for surfaces that are one self-contained input consumer such as an embedded WebView or a game canvas. -
Responsive sizing system with two selectable modes via
WidgetScaling(Fluid/Physical;set_widget_scaling/widget_scaling, defaultFluid). NewLengthconstructors —orient( portrait, landscape )(a percentage of the width in portrait, of the height in landscape),fluid( px )(surface-proportional, calibrated againstset_fluid_referenceand bounded byFLUID_MIN/FLUID_MAX),dp( px )(constant physical size scaled byset_density/density), andwidget( px )(picks fluid or dp per the active mode).Canvas::geom_px(geometry, physical layout space) andCanvas::font_px(font, bridging the logical / physical split per mode) give widgets and apps one resolution path. -
Button::font_size/height/widthandTextEdit::heightbuilders, allimpl Into<Length>, so control boxes scale with the surface.Text::line_height( mult )opens the gap between wrapped lines.Separator::pad_v(withLength::px( 0.0 )for a flush divider). -
Performance guardrails: opt-in diagnostics via
LTK_PERF_WARN=1(stuck animation, sustained software-render animation, lowpoll_interval) and a ~30 Hz software-animation cap overridable withApp::cap_software_animation. -
grid_min_cell( width )andWrapGrid::max_columns( n )— adaptive grid: the column count is derived at layout time from the available width so every cell is at leastwidthwide (anyLength; never fewer than one column), re-derived on every resize;max_columnscaps the count so cells grow instead of multiplying on wide surfaces.grid( n )keeps the fixed-count behaviour. -
Vertical flex:
flex( child )now distributes leftover height inside aColumn, mirroring its leftover-width behaviour in aRow— weights split the spare space between flex / spacer siblings, the child draws inside the allocated share, and it contributes zero to the column's natural height like a weight-only spacer. -
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_sizeand theOrientationenum — the runtime records the main surface's physical dimensions on every configure, soview()can branch a layout on portrait vs landscape (match ltk::orientation() { … }) without trackingon_resizeby hand; the portrait/landscape rule matchesLength::orient(square counts as portrait). Embedders drivingcore::UiSurfacedirectly callset_viewport_sizethemselves.examples/clip_path.rsdemonstrates it. -
test-supportCargo feature gates thetest_supportmodule so third-party builds never see it (ltk's ownmake testenables it).
Changed
- 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_factoris anf32, the physical pixels per logical one, whichPlatform::prepare_canvas,Presentation::scaleand 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), andbuffer_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'sdevicePixelRatio.App::on_scale_changedkeeps itsu32, the scale rounded, andApp::on_resizegives the exact physical size. ltk::ChannelSenderis ltk's own type instead of a re-export of calloop'sSender, with the samesend, returningResult<(), std::sync::mpsc::SendError<T>>, andClone, so apps that only send and clone it build unchanged; code that named calloop's type has to name ltk's. The runtime arms its timers and channels throughevent_loop/sources.rsinstead of a calloopLoopHandle. calloop, khronos-egl and raw-window-handle are dependencies only offwasm32, whereegl_context,UiSurface::try_new_gles,UiSurface::from_current_gles_loaderand the native GL names ofBorrowedGlesTexture(texture_id,framebuffer_id) do not exist; the monotonic clock iscrate::time::Instant, which isweb_time::Instantonwasm32, andchronotakes itswasmbindfeature there.- Themes, fonts, keyboard layouts and images are read through
vfs.rs, which is the file system natively and onwasm32a set of files held in memory, filled throughvfs::insert. A wallpaper or anImage::from_pathis now decoded from the file's bytes, so its format is told from its contents rather than its extension. - Input reaches the runtime as platform-neutral events.
input/event.rsdefinesKeyInput,PointerInputandTouchInput, and the pointer, touch and keyboard handlers take them through a small set of entry points (pointer_input,touch_input,key_press,key_release,key_repeated,modifiers_changed,keyboard_enter,keyboard_leave,set_repeat_info) instead of smithay-client-toolkit's events. TheKeyboardHandler,PointerHandlerandTouchHandlerimplementations moved toplatform/wayland/input.rs, which only records the serials later Wayland requests need, resolves surfaces and translates. What input handling asks of the windowing system (window move, resize, minimise and fullscreen, a bare frame request, the cursor's shape, the input method) is done by named methods inplatform/wayland/, so nothing underinput/names Wayland and no entry point orset_focustakes aQueueHandle;ResizeEdgeis ltk's own. The protocol state moved out ofAppDataintoplatform::wayland::WaylandPlatform, held asAppData::platform, and the compositor, shell, seat and output handlers, the data device and the primary selection moved fromevent_loop/toplatform/wayland/. Each surface's Wayland half (SurfaceKind, the SHM pool, the EGL window surface and what the compositor was last told) moved fromSurfaceStateintoplatform::wayland::WaylandSurfaceState, held asSurfaceState::platform, and the requests above form theplatform::Platformtrait, with the surface type as its associatedSurface. Drawing a frame is split from presenting it:draw/paint.rspaints a surface's view in full or under its dirty rects, the same for the software and GPU canvases, in place of the four near-identical paths ofdraw/software.rsanddraw/gles.rs, and the backend prepares the canvas and presents it (Platform::prepare_canvas,present,precise_damage), inplatform/wayland/present.rs, which also took the opaque and input regions andpick_shm_format. The EGL context moved intoWaylandPlatform. Publishing the clipboard and the primary selection goes throughPlatform::publish_clipboardandpublish_primary_selection, while their text stays with the runtime inAppData::clipboardand the newAppData::primary_selection. The runtime is generic over the platform:AppData<A, P: Platform = NativePlatform<A>>andSurfaceState<Msg, S = NativeSurface>, resolved at compile time; the backend-neutral half of a configure isSurfaceState::set_sizeandAppData::on_main_resized, and the backend-neutral half of a loop iteration moved fromrun.rstoevent_loop/step.rs(process_messages,draw_due, and around themnext_wakeup,before_drawandafter_draw, which carry the deadlines, the clipboard inbox, the accessibility tree and its actions, and the app's blur, copy, paste and focus requests), withAppData::newandAppData::install_sources(event_loop/setup.rs) building the state and the event sources every backend has. The run loop itself, overlay and subsurface reconciliation, session management, output probing and window fitting moved fromevent_loop/toplatform/wayland/, with the main output, the subsurfaces and the drop inbox now inWaylandPlatform;AppData::secondariespairs each surface withPlatform::Output. Tests drive the runtime onplatform/mock.rs, a platform that logs what is asked of it, through input, update, painting and presentation (event_loop/tests.rs). The behaviour is unchanged, but for a frame whose presentation fails, which now leaves the next frame to repaint in full. This is the first step towards backends other than Wayland, starting with Android;docs/architecture.mddescribes the events, their units and what stays with a backend. ltk::Keysymcomes fromxkeysym, the crate smithay-client-toolkit re-exported it from, so it is the same type and no code using it changes.XF86Backis no longer an alias ofEscape. It used to run the Escape path: close transient UI, else drop the focus and reachon_key_with_modifiers. It is now a back request with its own order (seeApp::on_backunder Added). Visible differences for an existing app: with a text input focused, back only drops the focus and the app sees the next request, where it used to get the key in the same press; the key is no longer reported throughApp::on_raw_key, so hosts forwarding raw keys into an embedded view stop leaking it to the page; it no longer auto-repeats by default (App::key_repeats); and aShellMode::Windowapp that handles neitheron_backnor the key is minimised on back at its root, where nothing happened before — returnBackAtRoot::Ignorefromback_at_rootto keep the old behaviour. An app that bindsXF86_Backinon_key/on_key_with_modifierskeeps receiving it.Escapeitself is unchanged; its transient-UI step is now shared with back asAppData::dismiss_transient.- A
window_size_hintlarger than the screen is shrunk to fit. The hint is pinned asmin_size == max_sizebefore the first commit, when no output is known yet, so an app asking for a phone-shaped 720x1440 window on a 1080p desktop got a toplevel taller than the screen. On the first configure the runtime now fits the hint, keeping its aspect ratio, inside the compositor'sxdg_toplevel.configure_bounds(WindowConfigure::suggested_bounds) or, when it sends none, inside 90 % of the first output's logical size;min_size(andmax_sizefor a non-resizable window) is re-pinned to the fitted size, which also replaces the hint as the fallback when later configures carry no size. A hint that fits is untouched.event_loop/window_fit.rs, with unit tests for the fitting rule. - A press on a text field renews the text-input
enable. Moving the focus between two text fields, or pressing the one already focused, used to commit only the refreshed content type on the existingzwp_text_input_v3. It now sendsenablefirst, as GTK does when a focused entry is clicked: a compositor that folded its on-screen keyboard while the field kept the focus (the back button does) has no other way to learn that the user wants it again, since the text-input never stopped being active. ltk sends neither surrounding text nor a cursor rectangle, so the stateenableresets is only the content type set right after it. - The software backend's ARGB8888 conversion works on whole pixels.
SoftwareCanvas::write_to_wayland_bufswapped red and blue with a byte-by-byte loop over the frame; it now reads each pixel as au32, exchanges the two channels with masks and shifts, and lets the compiler vectorise it. Compositors that offerABGR8888were never affected. - Breaking:
App::app_id,App::save_stateandApp::restore_stateare new mandatory trait methods — every implementor must add them (shell components: constant id,None, no-op).xdg_toplevel.set_app_idand the AccessKit application name now come fromapp_id()(previously"ltk"/"ltk-app"); theapp_idelement of the deprecatedwindow_configtuple is ignored. Crate version bumped to 0.3.0. Button::icon_sizetakesimpl Into<Length>and resolves throughCanvas::resolve_geom, joiningfont_size/height/width. A baref32still meansLength::px— every existing call keeps its exact size — but a caller can now passLength::widget( n )to have an icon button follow the widget-scaling mode the way stock icons do. Without it an expliciticon_sizewas the one geometry setter that ignored the mode, so a 21 px back arrow sat next to alist_itemchevron that fluid sizing had grown well past 21 and looked visibly smaller.Length::dpnow applies the density at resolution time, not at construction. The value carries its design pixels in a newLengthBase::Dpvariant andresolvemultiplies by the density in effect when it runs, so aset_densitychange takes effect on the next paint without rebuilding the view's lengths — previously adpvalue was frozen to the density read when it was constructed. Behaviour is unchanged for code that sets density once at startup.- The GLES image texture cache is now bounded to 32 MiB of estimated GPU memory with least-recently-drawn eviction (the in-use entry is never evicted). Previously it grew without limit for the canvas' lifetime, so a stream of distinct buffers (photo carousel, video thumbnails) could exhaust GPU memory.
OverlaySpec::sizeis now( Length, Length )(was( u32, u32 )), resolved against the main surface when the overlay is materialized; wrap existing sizes inLength::px( … )for the old fixed behaviour.TextEdit::font_size,TextEdit::fixed_widthandSeparator::thicknessnow takeimpl Into<Length>(weref32), resolved like the button label (font space) / geometry space; thef32sentinels are gone (f32call sites still compile viaInto<Length>).- Dependency pins:
fontdue = "=0.9.3"andignore = "=0.4.23"are pinned exactly because newer releases require a rustc newer than the declared Rust 1.85 MSRV (Debian stable's toolchain). - Renamed
set_design_reference/design_reference→set_fluid_reference/fluid_reference.Length::dpchanged meaning — it used to be a surface-proportional value, and that behaviour now lives onLength::fluid;dpis the constant-physical-size unit. - Widget struct fields are now
pub( crate )(configured through builders), except the value / state types apps read or construct (Time,Date,ComboState).
Fixed
- 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 throughAppData::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::drawnow returns the view it showed, which the frame keeps, so the view moves only when the cursor leaves it, and by the least; aTextCommand::Selectno 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.
- A stack keeps its children inside it. A child that
Stackaligned 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, so a child larger than the stack gets the stack; children placed withpush_placedand the offsets ofpush_translatedare as they were. - A pull from the top unwound back to where it started still reports its release.
ReleaseEvent::OverscrollDownwas sent only while some overscroll was left, so a pull dragged back up to zero ended without one, and an app that had moved its view with the pull's progress was left waiting for it.GestureState::overscroll_engagedremembers that the gesture reported progress, and its release now always reportsOverscrollDown, uncommitted when nothing is left of the pull; a drag that never reached overscroll still reports nothing. - Pasting what another application copied. The Wayland backend bound its
wl_data_deviceand its primary-selection device inSeatHandler::new_seat, which smithay-client-toolkit calls only for seats announced after start-up, not for the seat already there when the app connects. An app therefore never had either device, the compositor never offered it another client's selection, and neither Ctrl+V nor the middle button pasted anything copied outside the app, nor did what it copied reach other applications through the compositor. Both devices are now bound on the seat's first capability, which comes for every seat. - The middle button pastes the selection just made in another application. A compositor offers the primary selection to the window it gives the keyboard, and gives it on the press, so a middle click on a window that did not have the keyboard reaches it together with the offer. ltk reads an offer on a thread of its own and pasted at once what it had read before, so after selecting in another application an ltk app pasted the selection before that. A middle-button paste, into the text field under the pointer or one an app asks for with
SelectionKind::Primary, now waits while a selection is being read (Platform::primary_selection_pending, which the Wayland backend counts) and is made once it has been, or after 500 ms with what there is. - A held button is not also tapped. A button with
on_long_pressand noon_drag_startstayed a tap after its hold fired, so releasing it sent itson_pressas well; it no longer does. - Drawing many small images no longer slows down as the image caches fill. Every hit in the GLES texture cache, the GLES surface cache and the software canvas's premultiplied cache looked its key up in a linear LRU order, so an app that draws thousands of small images, a terminal's glyphs in each of their colours, walked thousands of entries for each one on every frame. The caches now find, insert and evict an entry in O(log n), with the same budgets and eviction order.
- The cursor's shape over a browser app's overlays — the web backend set the CSS
cursoron the main surface's element only, so over an overlay, such as crustace's dock or a panel, the pointer kept the page's arrow. Each surface now keeps the element that takes its input, the cursor's shape moves to that of the surface the pointer enters, and the last shape asked for is kept for it. - A focus request reaches a widget scrolled out of view. Scroll viewports drop the widgets outside their visible area from the hit-test list, so
App::take_focus_requestfor a text input that a tap had just added below the fold (a password field under the selected network of a Wi-Fi list) never found it — and the runtime kept re-requesting a rebuild and a redraw every frame while it waited. Widgets with aWidgetIdthat a scroll clips are now recorded with their content position (scroll_clipped); a request whose target is among them moves that viewport with the same rule keyboard navigation uses (scroll::offset_into_view) and is satisfied on the next frame. The wait is bounded to a few drawn frames, after which the request is dropped instead of spinning. - Fast two-thumb typing no longer drops taps. A finger landing while another was still down went to the auxiliary slot and never reached the gesture machine, so on a touch keypad every overlapping tap was lost. A finger landing on a primary that is still a plain press (
GestureState::is_plain_press: no drag, scroll, swipe or slider in flight) now commits that press as a tap at its last position and takes the primary slot; the earlier finger's release reports throughApp::on_touch_up. Presses that have become a drag keep the old behaviour, so a second finger during a pattern stroke or a slider drag still stays out of the gesture machine. PatternGrid::on_nodefires once per node, not once per motion event. Every touch motion within a node's snap radius re-emitted the node's message and requested a repaint, so drawing a pattern rebuilt and re-rastered the surface at the touchscreen's motion rate. The gesture machine now remembers the node the finger is over (last_pattern_node) and emits only on change; aSlidermove outcome without a message no longer requests a redraw.- A tap on a button survives finger motion inside the button. Any travel past the slop turned the press into a vertical or horizontal drag and its release skipped the button, so a thumb that rolled on a large key lost the tap. While the pressed widget fires on press (
WidgetHandlers::has_press) and the finger is still inside its rect, motion is ignored; the swipe machinery takes over from the original press point once the finger leaves, and presses inside a scroll viewport are unaffected. - Gesture thresholds are logical pixels. The 8 px travel that turns a press into a scroll, swipe or drag, and the 6 px stray that cancels a touch long-press, were compared against physical coordinates, so on a scale-2 panel they were 4 and 3 logical px and an ordinary thumb tap became a vertical drag whose release skipped the button.
SwipeConfignow carries the surface's bufferscaleand the machine reads the thresholds throughSwipeConfig::slop/stray. - A 1:1 opaque image is a row copy in the software renderer. With the premultiplied copy cached, drawing it unscaled with no clip and full alpha now copies rows into the canvas instead of running tiny-skia's bilinear pipeline over every pixel, which is what a full-surface backdrop cost on every frame.
- The software renderer premultiplies an image once, not on every draw.
SoftwareCanvas::draw_image_dataconverted the whole source to premultiplied alpha in floating point on each call, so a full-surface backdrop cost a pass over every pixel per frame on top of the blit. Premultiplied copies are now kept in a 32 MiB least-recently-drawn cache keyed by size, content fingerprint and alpha, the same key the GLES texture cache uses (fingerprint_rgbamoves torender::helpersto be shared). - Rows no longer spill their children out of the layout.
Rowdistributed leftover width but had no notion of a shortfall:leftoverfloored at zero, so a cluster wider than its rect kept every preferred width and overflowed — symmetrically, since the no-spacer branch centres it, which is why both end labels of a segmented control were clipped at once. The shortfall now comes off the widest children first (water filling), so a long title gives up its width while the icon beside it keeps its size, and equal siblings — a segmented control — share it evenly; pinned spacers keep their width, andRow::no_shrink()opts out for strips deliberately wider than their viewport.Buttonelides its label into the rect it is granted, so shrinking degrades to a truncated label rather than clipped glyphs; the truncation rule moves to one crate-internal helper shared withListItem. ListItemlabels no longer run under the trailing slot. The label and subtitle were painted with no width limit, so a title longer than the row overlapped the trailing text or the disclosure icon instead of truncating. The trailing slots are now laid out first and both lines are elided with an ellipsis against the space they leave, matching whatTextalready did.- Text inputs no longer insert mid-string when the value grows after focus. Focusing a text input pinned the cursor to a snapshot of
value.len(); if the value kept growing without the widget seeing keystrokes (a field fed over IPC), the first normally-delivered key inserted at the stale position. The focus-time cursor is now an end-of-value sentinel that every consumer clamps to the current value length, collapsing to a concrete position on the first real keystroke or click. - Single-line caret height now follows the text line. The caret spanned
rect.height - 16, so a field taller than its text line grew an oversized caret; it now measuresfont_size + 4and is vertically centered like the text, matching the multiline caret. - Dialog action buttons no longer overflow the card on large surfaces. The card's width cap was a fixed 480 px while the stock buttons inside grow fluidly with the surface, so on windows past the design size the right-aligned action row ran off the card's right edge.
Dialog::max_widthnow takesimpl Into<Length>(f32 call sites keep compiling as fixed px) and the default isLength::fluid( 480.0 ), matching the buttons' scaling curve. App::on_pointer_axisnow triggers a view rebuild and repaint. The raw axis hook (wheel/touchpad outside any ltk scroll viewport) fired the app callback but never requested a redraw, so state mutated there (a wheel-stepped carousel, an embedder-scrolled canvas) did not repaint until the next unrelated event.- Dialog cards no longer double-pad their interior. The inner card column carried the
column()16 px default padding on top of the container'sCARD_PADDING(24 px), giving the content 40 px of interior inset per side and squeezing the action row until its buttons clipped on narrow windows. The inner column is now flush and the interior inset isCARD_PADDINGalone; the centering column's margin to the surface edges is explicit. - Time-picker digits no longer overflow their boxes. The editable digit fields paired a fluid font (
font_size_fluid) with a fixed 72 px box, so on any surface where the fluid font resolved larger than its design size the glyphs outgrew the field. The box is nowLength::fluid( 72.0 )— the same curve and clamp factors as the font, so both scale in lockstep. - Content inside a
scrollno longer renders smaller than the rest of the surface. Scroll viewports draw their child into a sub-canvas sized to the viewport rect, and fluidLengthresolution (Canvas::geom_px/font_px— icon sizes, row heights, paddings, font sizes) resolved against that smaller canvas instead of the surface, shrinking everything inside any scroll by the width ratio (~13 % in a 360 px window with 24 px margins). Sub-canvases now inherit the root canvas's layout viewport (viewport_layout/viewport_logical), propagated through nested sub-canvases, so geometry resolves identically inside and outside offscreen content.
0.2.0 - 2026-06-25
This release adds the primitives an embedder needs to drive ltk as the render backend for a retained, externally-owned widget tree (for example projecting an Android view hierarchy onto an ltk surface). Each is kept general rather than tied to one consumer.
Added
Canvas::set_clip_path— anti-aliased clipping to an arbitrary vector path (&[PathCmd]) on both backends. The software backend installs a tiny-skia coverage mask; the GLES backend captures the clipped draws into an offscreen layer and composites them back through an anti-aliased coverage mask. Complements the existing rect clip (set_clip_rects) for shaped clips such as a circular avatar, a rounded card or aVectorDrawablemask. Seeexamples/clip_path.rs.Canvas::fill_path/Canvas::stroke_pathover a newPathCmdcommand list (MoveTo/LineTo/QuadTo/CubicTo/Close, in surface coordinates) — renders an arbitrary vector path, aPath, aVectorDrawableor a Lottie frame. The software backend rasterises directly with tiny-skia; the GLES backend rasterises into a tiny-skia pixmap and uploads it, so both backends share the same path rasteriser and stay at visual parity.Canvas::read_rgba_pixels— reads any canvas into tightly packed straight-alpha RGBA8 (top-left row first), on both the GLES and software backends (the software path un-premultiplies its pixmap).Canvas::is_softwarelets a caller branch on the backend (e.g. honour a real path clip on software but only a bounding rect on GLES).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 liveCanvas, for an embedder's measure pass that must match the renderer's metrics. Backed by a process-wide cached primary-font handle.Stack::push_placed— appends a child at an exact rect, bypassing alignment and intrinsic sizing, so a view tree whose geometry is computed elsewhere can be projected onto a Stack in paint order.Stack::push_placed_clippedadditionally clips the child's subtree to a rect (Android'sclipChildren): overflowing content is not painted.ExternalSource::Cpuand theExternal::cpuconstructor — an immediate-mode CPU drawing closure invoked once per frame with the canvas and the widget's laid-out rect, working on both backends. Hosts a customView.onDrawstraight onto the ltk canvas without a GL texture round-trip, unlike the existingTexturesource which only renders on GLES.RichTextwidget — wrapped paragraph text carrying aMsgper clickable link range; the layout pass emits one hit rect per link line so taps land on the link rather than the whole paragraph. The ltk side of an AndroidSpannedcarryingURLSpan/ClickableSpan.
Fixed
- GLES clip-layer composite no longer renders path-clipped content vertically flipped: the offscreen layer was sampled at the inverted screen Y. The software backend was unaffected.
- Gesture: the horizontal pager is driven only once the swipe axis locks horizontal. Previously the pre-lock lateral drift of a vertical gesture could arm the consumer's pager without a matching release event, which could freeze the surface until an unrelated gesture reset it.
- Software/GLES parity: the software backend now snaps glyph pen positions and image destinations to integer pixels (rounding to nearest), matching the GLES backend. Previously it truncated, so text and 1:1 images could land up to half a pixel off between backends and sample ~1 px softer.
Changed
- Default theme launcher SVGs replaced with renderer-compatible versions.
0.1.0 - 2026-03-10
Added
- Initial release.