Applications built on ltk had no way to come back where the user left them: the toolkit hardcoded `app_id = "ltk"` on every toplevel, never wrote anything to disk, and died on SIGTERM without a chance to save. This release gives the runtime the whole plumbing and asks each application only for the bytes worth keeping, in the spirit of Android's saved-instance state. The `App` trait gains three mandatory methods, deliberately without default bodies so every application states its position: `app_id()` (reverse-DNS, used for `xdg_toplevel.set_app_id`, the AccessKit application name and the state directory — the `app_id` element of the deprecated `window_config` tuple is now ignored and a one-time warning reports a mismatch), `save_state() -> Option<Vec<u8>>` and `restore_state(Vec<u8>)`. The bytes are opaque; the trait carries no serde bound. Their rustdoc is the contract: when the runtime saves, where the files live, when the bytes come back and when they do not, what must never go in them, and a worked serde_json example. The runtime persists under `$XDG_STATE_HOME/<app_id>/` (falling back to `~/.local/state`): `session.json` holds the compositor session id, a clean-exit marker and the writer's pid; `state.bin` holds the application bytes. Writes are atomic (temp file + rename, mode 0600, directory 0700) and best-effort. State is saved every 30 s when the bytes changed, once after the event loop exits (which covers `on_close_requested`, `requested_exit` and lost connections), and on SIGTERM/SIGINT — a calloop signal source, installed before any thread exists, now turns those into a clean exit of the loop instead of process death. `restore_state` runs synchronously in `try_run` before the window is created and before the first `view()`, and only when the process is relaunched as part of a session restore (`LTK_SESSION_RESTORE=1`, removed from the environment before the app can spawn children) or when the previous run left `clean_exit: false`; a plain launch starts fresh. A second concurrent instance detects the live pid and runs with persistence disabled rather than clobbering the first. The compositor side of geometry restore goes through `xdg-session-management-v1`. Neither wayland-protocols nor sctk ship generated code for it yet, so the XML is vendored under `protocols/` and `wayland-scanner` generates the client module in-tree (`src/protocol/`), resolving the crate names through sctk's reexports so the bindings stay on the crate instances sctk links. Before the first commit of a `ShellMode::Window` toplevel the runtime binds `xdg_session_manager_v1`, calls `get_session(reason, stored_id)` and `restore_toplevel(toplevel, "main")`; the three window-creation paths in `run.rs` are folded into one `make_window` helper so the attach always sits immediately before `commit()`. `created` persists the id, `replaced` destroys the objects and stops persisting. Compositors without the global lose only the geometry half. Layer-shell and session-lock surfaces skip the whole machinery. Every `App` implementor in the tree is updated: the twelve examples (`showcase`, `scroll` and `mini_shell` persist real state; the rest return `None`), both integration tests (`event_loop_flow` gains `save_restore_round_trip`), the in-source and markdown doctests, README, onboarding, cookbook (new recipe "Surviving relaunch: session state") and architecture docs, and the changelog. `src/session_state.rs` carries unit tests over a temporary state directory. `Makefile install` now copies `protocols/` into the cargo registry — without it downstream builds would fail inside the proc-macro — and `debian/copyright` covers the vendored XML. The trait change is breaking, hence 0.3.0. Also fixes the pre-existing `viewport_tests` module in `render/mod.rs`, which used `Length` without importing it and broke `cargo test`.
166 lines
5.1 KiB
Rust
166 lines
5.1 KiB
Rust
//! `cargo run --example clip_path`
|
|
//!
|
|
//! Demonstrates [`Canvas::set_clip_path`] — real per-path clipping. Each cell
|
|
//! fills its whole rect with a background colour, then installs a path clip and
|
|
//! fills again with a foreground colour: only the pixels inside the path are
|
|
//! repainted, so the shape's silhouette appears (not its bounding box).
|
|
//!
|
|
//! Both backends clip with smooth, anti-aliased edges: the software backend
|
|
//! with a tiny-skia coverage mask, the GLES backend by compositing an offscreen
|
|
//! layer through an equivalent coverage mask. The circle and rounded corners
|
|
//! look the same on both. Esc exits.
|
|
|
|
use ltk::{ column, row, text, App, Color, Element, External, Keysym, PathCmd, Rect };
|
|
|
|
#[ derive( Clone, Debug ) ]
|
|
enum Msg {}
|
|
|
|
struct Demo;
|
|
|
|
/// Floor for the square cells — the orientation-derived size never
|
|
/// shrinks below this, so the shapes stay readable on tiny windows.
|
|
const CELL_MIN: f32 = 120.0;
|
|
const MARGIN: f32 = 24.0;
|
|
|
|
fn bg() -> Color { Color::rgba( 0.16, 0.18, 0.22, 1.0 ) }
|
|
fn fg() -> Color { Color::rgba( 0.30, 0.68, 0.90, 1.0 ) }
|
|
|
|
/// Rounded rectangle inset by `MARGIN`, corners of `radius`, built from lines
|
|
/// and quadratic corners.
|
|
fn rounded_rect_path( r: Rect, radius: f32 ) -> Vec<PathCmd>
|
|
{
|
|
let l = r.x + MARGIN;
|
|
let t = r.y + MARGIN;
|
|
let right = r.x + r.width - MARGIN;
|
|
let b = r.y + r.height - MARGIN;
|
|
vec![
|
|
PathCmd::MoveTo( l + radius, t ),
|
|
PathCmd::LineTo( right - radius, t ),
|
|
PathCmd::QuadTo( right, t, right, t + radius ),
|
|
PathCmd::LineTo( right, b - radius ),
|
|
PathCmd::QuadTo( right, b, right - radius, b ),
|
|
PathCmd::LineTo( l + radius, b ),
|
|
PathCmd::QuadTo( l, b, l, b - radius ),
|
|
PathCmd::LineTo( l, t + radius ),
|
|
PathCmd::QuadTo( l, t, l + radius, t ),
|
|
PathCmd::Close,
|
|
]
|
|
}
|
|
|
|
/// Circle centred in the cell, drawn as four cubic-Bézier quadrants.
|
|
fn circle_path( r: Rect ) -> Vec<PathCmd>
|
|
{
|
|
let cx = r.x + r.width / 2.0;
|
|
let cy = r.y + r.height / 2.0;
|
|
let rad = ( r.width.min( r.height ) ) / 2.0 - MARGIN;
|
|
let k = rad * 0.552_284_75; // 4/3 * tan(pi/8): cubic circle approximation
|
|
vec![
|
|
PathCmd::MoveTo( cx + rad, cy ),
|
|
PathCmd::CubicTo( cx + rad, cy + k, cx + k, cy + rad, cx, cy + rad ),
|
|
PathCmd::CubicTo( cx - k, cy + rad, cx - rad, cy + k, cx - rad, cy ),
|
|
PathCmd::CubicTo( cx - rad, cy - k, cx - k, cy - rad, cx, cy - rad ),
|
|
PathCmd::CubicTo( cx + k, cy - rad, cx + rad, cy - k, cx + rad, cy ),
|
|
PathCmd::Close,
|
|
]
|
|
}
|
|
|
|
/// Downward triangle inset by `MARGIN` — a polygon clip.
|
|
fn triangle_path( r: Rect ) -> Vec<PathCmd>
|
|
{
|
|
vec![
|
|
PathCmd::MoveTo( r.x + r.width / 2.0, r.y + MARGIN ),
|
|
PathCmd::LineTo( r.x + r.width - MARGIN, r.y + r.height - MARGIN ),
|
|
PathCmd::LineTo( r.x + MARGIN, r.y + r.height - MARGIN ),
|
|
PathCmd::Close,
|
|
]
|
|
}
|
|
|
|
/// A cell that paints `bg()` everywhere, then `fg()` clipped to `shape`. The
|
|
/// previous clip is snapshotted and restored so the clip does not leak to the
|
|
/// rest of the frame.
|
|
fn clipped_cell( size: f32, shape: fn( Rect ) -> Vec<PathCmd> ) -> Element<Msg>
|
|
{
|
|
External::cpu( size, size, move |canvas, rect|
|
|
{
|
|
canvas.fill_rect( rect, bg(), 0.0 );
|
|
let saved = canvas.clip_bounds();
|
|
canvas.set_clip_path( &shape( rect ) );
|
|
canvas.fill_rect( rect, fg(), 0.0 );
|
|
canvas.set_clip_rects( &saved );
|
|
} ).into()
|
|
}
|
|
|
|
impl App for Demo
|
|
{
|
|
type Message = Msg;
|
|
|
|
fn app_id( &self ) -> &str { "net.liberux.ltk.example.clip_path" }
|
|
fn save_state( &self ) -> Option<Vec<u8>> { None }
|
|
fn restore_state( &mut self, _state: Vec<u8> ) {}
|
|
|
|
fn view( &self ) -> Element<Msg>
|
|
{
|
|
// Cells side by side in landscape, stacked in portrait; the
|
|
// runtime rebuilds the view on every configure, so both the
|
|
// arrangement and the cell size follow the window live. The
|
|
// square side comes from the axis the cells do NOT flow along
|
|
// (the height in landscape, the width in portrait), capped so
|
|
// the three of them also fit along the flow axis with the
|
|
// paddings and gaps accounted for.
|
|
let ( vw, vh ) = ltk::viewport_size();
|
|
let ( vw, vh ) = ( vw as f32, vh as f32 );
|
|
let cells: Element<Msg> = match ltk::orientation()
|
|
{
|
|
ltk::Orientation::Landscape =>
|
|
{
|
|
let cell = ( vh * 0.5 ).min( ( vw - 80.0 ) / 3.0 ).max( CELL_MIN );
|
|
row()
|
|
.spacing( 16.0 )
|
|
.push( clipped_cell( cell, |r| rounded_rect_path( r, 32.0 ) ) )
|
|
.push( clipped_cell( cell, circle_path ) )
|
|
.push( clipped_cell( cell, triangle_path ) )
|
|
.into()
|
|
}
|
|
ltk::Orientation::Portrait =>
|
|
{
|
|
let cell = ( vw * 0.55 ).min( ( vh - 130.0 ) / 3.0 ).max( CELL_MIN );
|
|
column()
|
|
.padding( 0.0 )
|
|
.spacing( 16.0 )
|
|
.push( clipped_cell( cell, |r| rounded_rect_path( r, 32.0 ) ) )
|
|
.push( clipped_cell( cell, circle_path ) )
|
|
.push( clipped_cell( cell, triangle_path ) )
|
|
.into()
|
|
}
|
|
};
|
|
|
|
column()
|
|
.spacing( 16.0 )
|
|
.padding( 24.0 )
|
|
.push( text( "Canvas::set_clip_path — rounded rect, circle, triangle" ) )
|
|
.push( cells )
|
|
.into()
|
|
}
|
|
|
|
fn update( &mut self, _msg: Msg ) {}
|
|
|
|
fn on_key( &mut self, keysym: Keysym ) -> Option<Msg>
|
|
{
|
|
if keysym == Keysym::Escape
|
|
{
|
|
std::process::exit( 0 );
|
|
}
|
|
None
|
|
}
|
|
|
|
fn background_color( &self ) -> Color
|
|
{
|
|
ltk::theme_palette().bg
|
|
}
|
|
}
|
|
|
|
fn main()
|
|
{
|
|
ltk::run( Demo );
|
|
}
|