Files
ltk/tests/text_edit_dispatch.rs
Pedro M. de Echanove Pasquin ce893ac776
Some checks are pending
CI / build + test (push) Waiting to run
CI / cargo audit (push) Waiting to run
responsive fluid/physical scaling, widget-API stabilization, and perf guardrails
Responsive scaling. ltk now offers two first-class ways to size a UI so it adapts across screens, chosen per process via `WidgetScaling { Fluid, Physical }` (`set_widget_scaling` / `widget_scaling`, default `Fluid`). Fluid sizing (`Length::fluid( px )`) makes a design pixel a proportion of the surface's smaller side, calibrated against a reference width (`set_fluid_reference` / `fluid_reference`, 412 px default) and bounded by `FLUID_MIN` / `FLUID_MAX`; physical sizing (`Length::dp( px )`) is a constant-physical-size pixel scaled by display density (`set_density` / `density`). `Length` gains `orient( portrait, landscape )` — resolve one value in portrait, another in landscape — plus `widget( px )`, which picks fluid or dp per the active mode. Canvas exposes `geom_px` (geometry, resolved in physical layout space) and `font_px` (font size, bridging logical / physical per mode) so widgets and apps share one resolution path. Note the rename: `set_design_reference` / `design_reference` became `set_fluid_reference` / `fluid_reference`, and `Length::dp` changed meaning — the old surface-proportional behaviour now lives on `Length::fluid`.
Widgets. Every stock widget resolves its default geometry and font through the widget-scaling mode instead of frozen pixels, so a whole UI scales coherently without per-call units. New size builders where they were missing: `button` gains `font_size` / `height`, `text_edit` gains `height` / `font_size_fluid`, `separator` gains `pad_v`, and assorted widgets accept a `Length` where they previously took only `f32`.
Overlays. `OverlaySpec::size` is now `( Length, Length )` instead of `( u32, u32 )`, resolved against the main surface when the overlay is materialized, so overlays can scale with the display; `Length::px( … )` reproduces the old fixed sizing.
API stabilization (toward 1.0). Widget struct fields are now `pub( crate )` — they are configured through builders, not field access — except the value / state types apps genuinely read or construct (`Time`, `Date`, `ComboState`), which stay public. The internal `test_support` helpers move behind a `test-support` Cargo feature (off by default, so third-party builds never see them; ltk's own `make test` enables it). `Separator` drops its `0.0`-means-mode sentinel for `Option<Length>`, so an explicit `pad_v( 0.0 )` is a real flush divider distinct from the mode-following default.
Performance guardrails. Opt-in diagnostics via `LTK_PERF_WARN=1` warn about stuck animations, sustained software-render animation, and low `poll_interval`; software-rendered animation is capped near 30 Hz to spare CPU on machines that fall back off EGL. Apps can override the cap with `App::cap_software_animation`.
Docs and build. The two scaling modes are documented in README, onboarding and architecture, with the earlier gradient / backdrop doc drift cleaned up. The Makefile now ships the `locales/` directory into the packaged crate (fixing i18n keys rendering raw for downstreams), builds the new `responsive` example, and runs tests with `--features test-support`.
2026-07-07 17:40:33 +02:00

210 lines
6.2 KiB
Rust

#![ cfg( feature = "test-support" ) ]
// End-to-end coverage for the `text_edit` ↔ event-loop contract: the layout
// pass snapshots the widget's `on_change` / `on_submit` / `value` into a
// `WidgetHandlers::TextEdit` entry, and the runtime drives keystrokes through
// `text_change_msg` / `submit_msg` / `current_value`. These tests skip the
// Wayland loop and exercise that contract directly through `UiSurface`.
use ltk::core::{ RenderOptions, UiSurface };
use ltk::test_support::WidgetHandlers;
use ltk::{ column, text_edit, Color, Element };
#[ derive( Clone, Debug, PartialEq, Eq ) ]
enum Msg
{
Username( String ),
Password( String ),
UsernameSubmit,
PasswordSubmit,
}
fn login_view() -> Element<Msg>
{
column::<Msg>()
.padding( 16.0 )
.spacing( 8.0 )
.push(
text_edit( "Username", "alice" )
.on_change( |s| Msg::Username( s ) )
.on_submit( Msg::UsernameSubmit ),
)
.push(
text_edit::<Msg>( "Password", "" )
.secure( true )
.on_change( |s| Msg::Password( s ) )
.on_submit( Msg::PasswordSubmit ),
)
.into()
}
fn render_login() -> UiSurface<Msg>
{
let mut surface = UiSurface::<Msg>::new( 320, 240 );
let _ = surface.render(
&login_view(),
RenderOptions::full_canvas( 320, 240 ).background( Color::rgb( 0.1, 0.1, 0.1 ) ),
);
surface
}
#[ test ]
fn layout_records_two_text_edit_widgets()
{
let surface = render_login();
assert_eq!( surface.widget_rects().len(), 2 );
for w in surface.widget_rects()
{
assert!( w.handlers.is_text_input() );
}
}
#[ test ]
fn text_change_msg_carries_new_value_through_user_callback()
{
let surface = render_login();
let username_idx = surface.widget_rects()[ 0 ].flat_idx;
let h = surface.handlers( username_idx ).expect( "text edit handler should exist" );
let msg = h.text_change_msg( "alic" ); // user pressed backspace once
assert_eq!( msg, Some( Msg::Username( "alic".into() ) ) );
}
#[ test ]
fn text_change_msg_passes_unicode_unchanged()
{
let surface = render_login();
let username_idx = surface.widget_rects()[ 0 ].flat_idx;
let h = surface.handlers( username_idx ).unwrap();
let msg = h.text_change_msg( "café 🦀" );
assert_eq!( msg, Some( Msg::Username( "café 🦀".into() ) ) );
}
#[ test ]
fn submit_msg_returns_configured_message()
{
let surface = render_login();
let password_idx = surface.widget_rects()[ 1 ].flat_idx;
let h = surface.handlers( password_idx ).unwrap();
assert_eq!( h.submit_msg(), Some( Msg::PasswordSubmit ) );
}
#[ test ]
fn each_text_edit_dispatches_to_its_own_callback()
{
// The two fields share the same `Msg` enum but differ in which variant
// they wrap the new value in — confirms the handler snapshot captured
// the right closure for the right widget.
let surface = render_login();
let username_h = surface.handlers( surface.widget_rects()[ 0 ].flat_idx ).unwrap();
let password_h = surface.handlers( surface.widget_rects()[ 1 ].flat_idx ).unwrap();
assert_eq!( username_h.text_change_msg( "x" ), Some( Msg::Username( "x".into() ) ) );
assert_eq!( password_h.text_change_msg( "x" ), Some( Msg::Password( "x".into() ) ) );
}
#[ test ]
fn current_value_reflects_initial_state()
{
let surface = render_login();
let username_h = surface.handlers( surface.widget_rects()[ 0 ].flat_idx ).unwrap();
let password_h = surface.handlers( surface.widget_rects()[ 1 ].flat_idx ).unwrap();
assert_eq!( username_h.current_value(), Some( "alice" ) );
assert_eq!( password_h.current_value(), Some( "" ) );
}
#[ test ]
fn current_value_reflects_state_after_app_update()
{
// User types into the username field. The app updates state and the next
// render must snapshot the new value into the handler.
let mut surface = UiSurface::<Msg>::new( 320, 240 );
let mut username = String::from( "alice" );
let view = |val: &str| -> Element<Msg>
{
column::<Msg>()
.padding( 16.0 )
.push(
text_edit( "Username", val.to_string() )
.on_change( |s| Msg::Username( s ) ),
)
.into()
};
let opts = RenderOptions::full_canvas( 320, 240 );
let _ = surface.render( &view( &username ), opts );
assert_eq!(
surface.handlers( surface.widget_rects()[ 0 ].flat_idx ).unwrap().current_value(),
Some( "alice" ),
);
username.push_str( "_2" );
let _ = surface.render( &view( &username ), opts );
assert_eq!(
surface.handlers( surface.widget_rects()[ 0 ].flat_idx ).unwrap().current_value(),
Some( "alice_2" ),
);
}
#[ test ]
fn is_text_input_distinguishes_text_edit_from_other_handlers()
{
use ltk::button;
let mut surface = UiSurface::<Msg>::new( 240, 200 );
let view: Element<Msg> = column()
.push( text_edit::<Msg>( "p", "" ) )
.push( button( "go" ).on_press( Msg::UsernameSubmit ) )
.into();
let _ = surface.render( &view, RenderOptions::full_canvas( 240, 200 ) );
let h0 = surface.handlers( surface.widget_rects()[ 0 ].flat_idx ).unwrap();
let h1 = surface.handlers( surface.widget_rects()[ 1 ].flat_idx ).unwrap();
assert!( h0.is_text_input() );
assert!( !h1.is_text_input() );
}
#[ test ]
fn submit_msg_returns_none_for_non_text_edit_handlers()
{
use ltk::button;
let mut surface = UiSurface::<Msg>::new( 240, 100 );
let view: Element<Msg> = column()
.push( button( "go" ).on_press( Msg::UsernameSubmit ) )
.into();
let _ = surface.render( &view, RenderOptions::full_canvas( 240, 100 ) );
let h = surface.handlers( surface.widget_rects()[ 0 ].flat_idx ).unwrap();
assert!( h.submit_msg().is_none() );
}
#[ test ]
fn text_change_msg_returns_none_when_no_callback_configured()
{
let mut surface = UiSurface::<Msg>::new( 240, 100 );
let view: Element<Msg> = column()
.push( text_edit::<Msg>( "no callback", "x" ) )
.into();
let _ = surface.render( &view, RenderOptions::full_canvas( 240, 100 ) );
let h = surface.handlers( surface.widget_rects()[ 0 ].flat_idx ).unwrap();
assert!( h.text_change_msg( "y" ).is_none() );
}
#[ test ]
fn handler_variant_for_text_edit_carries_value_field()
{
// Confirm the layout pass populates the `value` field of the
// `WidgetHandlers::TextEdit` snapshot — input dispatch reads it for cursor
// placement / backspace rebuild.
let surface = render_login();
let h = surface.handlers( surface.widget_rects()[ 0 ].flat_idx ).unwrap();
match h
{
WidgetHandlers::TextEdit { value, .. } => assert_eq!( value, "alice" ),
_ => panic!( "expected TextEdit handler variant" ),
}
}