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`.
162 lines
5.0 KiB
Rust
162 lines
5.0 KiB
Rust
//! `cargo run --example responsive`
|
||
//!
|
||
//! Demonstrates ltk's two responsive modes side by side on stock widgets.
|
||
//! None of the widgets below set an explicit size — they all follow the
|
||
//! process-wide [`ltk::WidgetScaling`] mode:
|
||
//!
|
||
//! - **Fluid** (the default): sizes are a fraction of the surface, so the
|
||
//! whole set grows and shrinks as you resize the window.
|
||
//! - **Physical**: sizes are a constant real-world size scaled by
|
||
//! [`ltk::density`], independent of the surface.
|
||
//!
|
||
//! Tap **Switch mode** to flip between them, and **−/+ density** to change
|
||
//! the physical density. Watch the button, field, checkbox, switch, slider
|
||
//! and progress bar resize (or not) accordingly. Esc exits.
|
||
//!
|
||
//! NOTE: ltk is a Wayland layer-shell toolkit. This example requires a running
|
||
//! Wayland compositor (e.g. sway, labwc, or a full desktop session).
|
||
|
||
use ltk::
|
||
{
|
||
App, Element, Keysym, ButtonVariant, WidgetScaling,
|
||
button, checkbox, column, grid, progress_bar, separator, slider, spacer, text, text_edit, toggle,
|
||
set_widget_scaling, set_density, density,
|
||
};
|
||
|
||
#[ derive( Clone ) ]
|
||
enum Message
|
||
{
|
||
SwitchMode,
|
||
DensityUp,
|
||
DensityDown,
|
||
NameChanged( String ),
|
||
ToggleCheck,
|
||
ToggleSwitch,
|
||
SliderChanged( f32 ),
|
||
}
|
||
|
||
struct ResponsiveApp
|
||
{
|
||
physical: bool,
|
||
name: String,
|
||
checked: bool,
|
||
switched: bool,
|
||
volume: f32,
|
||
}
|
||
|
||
impl ResponsiveApp
|
||
{
|
||
fn new() -> Self
|
||
{
|
||
// Start in the default fluid mode with a neutral density.
|
||
set_widget_scaling( WidgetScaling::Fluid );
|
||
set_density( 1.5 );
|
||
Self
|
||
{
|
||
physical: false,
|
||
name: String::new(),
|
||
checked: true,
|
||
switched: false,
|
||
volume: 0.4,
|
||
}
|
||
}
|
||
}
|
||
|
||
impl App for ResponsiveApp
|
||
{
|
||
type Message = Message;
|
||
|
||
fn app_id( &self ) -> &str { "net.liberux.ltk.example.responsive" }
|
||
fn save_state( &self ) -> Option<Vec<u8>> { None }
|
||
fn restore_state( &mut self, _state: Vec<u8> ) {}
|
||
|
||
fn view( &self ) -> Element<Message>
|
||
{
|
||
let palette = ltk::theme_palette();
|
||
let primary = palette.text_primary;
|
||
let secondary = palette.text_secondary;
|
||
|
||
let mode_label = if self.physical
|
||
{
|
||
format!( "Mode: Physical (density {:.1})", density() )
|
||
} else {
|
||
"Mode: Fluid (resize the window to see it scale)".to_string()
|
||
};
|
||
|
||
// Mode + density controls. Explicit sizes here so the controls stay
|
||
// stable while the demo widgets below react to the mode.
|
||
// Mode + density controls. Stock buttons like the demo widgets
|
||
// below, so they follow the active mode too. The switch gets the
|
||
// full width and the density pair half a cell each: every button
|
||
// clamps to its slot instead of overflowing a shared row, and the
|
||
// slots leave room for Physical-mode text at raised densities.
|
||
let controls = column::<Message>()
|
||
.padding( 0.0 )
|
||
.spacing( 8.0 )
|
||
.push( button::<Message>( "Switch mode".to_string() )
|
||
.variant( ButtonVariant::Primary )
|
||
.on_press( Message::SwitchMode ) )
|
||
.push( grid::<Message>( 2 )
|
||
.push( button::<Message>( "− density".to_string() )
|
||
.on_press( Message::DensityDown ) )
|
||
.push( button::<Message>( "+ density".to_string() )
|
||
.on_press( Message::DensityUp ) ) );
|
||
|
||
// The demo widgets — NO explicit sizes, so they follow the mode.
|
||
let demo = column::<Message>()
|
||
.spacing( 16.0 )
|
||
.max_width( 520.0 )
|
||
.push( button::<Message>( "A stock button".to_string() ).variant( ButtonVariant::Secondary ).on_press( Message::SwitchMode ) )
|
||
.push( text_edit( "A stock text field".to_string(), self.name.clone() ).on_change( Message::NameChanged ) )
|
||
.push( checkbox( self.checked ).label( "A stock checkbox".to_string() ).on_toggle( Message::ToggleCheck ) )
|
||
.push( toggle( self.switched ).label( "A stock switch".to_string() ).on_toggle( Message::ToggleSwitch ) )
|
||
.push( slider( self.volume ).on_change( Message::SliderChanged ) )
|
||
.push( progress_bar( self.volume ) );
|
||
|
||
column::<Message>()
|
||
.padding( 32.0 )
|
||
.spacing( 20.0 )
|
||
.center_y( true )
|
||
.push( text( "ltk responsive modes".to_string() ).size( 26.0 ).color( primary ).align_center() )
|
||
.push( text( mode_label ).size( 15.0 ).color( secondary ).align_center() )
|
||
.push( controls )
|
||
.push( separator() )
|
||
.push( demo )
|
||
.push( spacer().weight( 1 ) )
|
||
.push( text( "Esc to exit".to_string() ).size( 12.0 ).color( secondary ).align_center() )
|
||
.into()
|
||
}
|
||
|
||
fn update( &mut self, msg: Message )
|
||
{
|
||
match msg
|
||
{
|
||
Message::SwitchMode =>
|
||
{
|
||
self.physical = !self.physical;
|
||
set_widget_scaling( if self.physical { WidgetScaling::Physical } else { WidgetScaling::Fluid } );
|
||
}
|
||
Message::DensityUp => set_density( ( density() + 0.25 ).min( 4.0 ) ),
|
||
Message::DensityDown => set_density( ( density() - 0.25 ).max( 0.5 ) ),
|
||
Message::NameChanged( v ) => self.name = v,
|
||
Message::ToggleCheck => self.checked = !self.checked,
|
||
Message::ToggleSwitch => self.switched = !self.switched,
|
||
Message::SliderChanged( v ) => self.volume = v,
|
||
}
|
||
}
|
||
|
||
fn on_key( &mut self, keysym: Keysym ) -> Option<Message>
|
||
{
|
||
if keysym == Keysym::Escape
|
||
{
|
||
std::process::exit( 0 );
|
||
}
|
||
None
|
||
}
|
||
}
|
||
|
||
fn main()
|
||
{
|
||
ltk::run( ResponsiveApp::new() );
|
||
}
|