// Handler-level dispatch coverage for the interactive widgets that aren't // buttons or text edits: toggle, checkbox, radio and slider. Each test renders // the widget through `UiSurface`, locates its `WidgetHandlers` snapshot and // confirms the snapshot routes activations to the configured Msg variant. use ltk::core::{ RenderOptions, UiSurface }; use ltk::test_support::WidgetHandlers; use ltk::{ checkbox, column, radio, slider, toggle, Color, Element, Point, Rect, SliderAxis, }; #[ derive( Clone, Debug, PartialEq ) ] enum Msg { WifiToggled, TermsAccepted, PriorityHigh, Brightness( f32 ), } fn render_view( view: Element ) -> UiSurface { let mut surface = UiSurface::::new( 320, 240 ); let _ = surface.render( &view, RenderOptions::full_canvas( 320, 240 ).background( Color::rgb( 0.1, 0.1, 0.1 ) ), ); surface } // ── Toggle ──────────────────────────────────────────────────────────────────── #[ test ] fn toggle_handler_carries_on_toggle_message() { let view: Element = column() .push( toggle( false ).on_toggle( Msg::WifiToggled ) ) .into(); let surface = render_view( view ); let idx = surface.widget_rects()[ 0 ].flat_idx; let h = surface.handlers( idx ).expect( "toggle should expose a handler" ); assert!( !h.is_text_input() ); assert!( !h.is_slider() ); assert_eq!( h.press_msg(), Some( Msg::WifiToggled ) ); } #[ test ] fn toggle_handler_variant_is_toggle() { let view: Element = column() .push( toggle( true ).on_toggle( Msg::WifiToggled ) ) .into(); let surface = render_view( view ); let h = surface.handlers( surface.widget_rects()[ 0 ].flat_idx ).unwrap(); assert!( matches!( h, WidgetHandlers::Toggle { on_toggle: Some( _ ), .. } ) ); } #[ test ] fn toggle_without_on_toggle_yields_none_press_msg() { let view: Element = column() .push( toggle::( false ) ) .into(); let surface = render_view( view ); let h = surface.handlers( surface.widget_rects()[ 0 ].flat_idx ).unwrap(); assert!( h.press_msg().is_none() ); } // ── Checkbox ────────────────────────────────────────────────────────────────── #[ test ] fn checkbox_handler_carries_on_toggle_message() { let view: Element = column() .push( checkbox( false ).on_toggle( Msg::TermsAccepted ).label( "Accept" ) ) .into(); let surface = render_view( view ); let idx = surface.widget_rects()[ 0 ].flat_idx; let h = surface.handlers( idx ).unwrap(); assert!( matches!( h, WidgetHandlers::Checkbox { on_toggle: Some( _ ), .. } ) ); assert_eq!( h.press_msg(), Some( Msg::TermsAccepted ) ); } #[ test ] fn checkbox_state_is_independent_of_handler() { // The widget renders the current `checked` value; toggling that value is // the parent app's job — handler dispatch must work regardless of the // initial state. let unchecked: Element = column().push( checkbox( false ).on_toggle( Msg::TermsAccepted ) ).into(); let checked: Element = column().push( checkbox( true ).on_toggle( Msg::TermsAccepted ) ).into(); for view in [ unchecked, checked ] { let surface = render_view( view ); let h = surface.handlers( surface.widget_rects()[ 0 ].flat_idx ).unwrap(); assert_eq!( h.press_msg(), Some( Msg::TermsAccepted ) ); } } // ── Radio ───────────────────────────────────────────────────────────────────── #[ test ] fn radio_handler_carries_on_select_message() { let view: Element = column() .push( radio( false ).on_select( Msg::PriorityHigh ).label( "High" ) ) .into(); let surface = render_view( view ); let idx = surface.widget_rects()[ 0 ].flat_idx; let h = surface.handlers( idx ).unwrap(); assert!( matches!( h, WidgetHandlers::Radio { on_select: Some( _ ), .. } ) ); assert_eq!( h.press_msg(), Some( Msg::PriorityHigh ) ); } #[ test ] fn three_radios_each_route_to_their_own_message() { #[ derive( Clone, Debug, PartialEq ) ] enum Choice { Low, Medium, High } let view: Element = column() .push( radio( false ).on_select( Choice::Low ) ) .push( radio( false ).on_select( Choice::Medium ) ) .push( radio( true ).on_select( Choice::High ) ) .into(); let mut surface = UiSurface::::new( 240, 240 ); let _ = surface.render( &view, RenderOptions::full_canvas( 240, 240 ) ); let messages: Vec<_> = surface.widget_rects().iter() .map( |w| surface.handlers( w.flat_idx ).and_then( |h| h.press_msg() ) ) .collect(); assert_eq!( messages, vec![ Some( Choice::Low ), Some( Choice::Medium ), Some( Choice::High ) ], ); } // ── Slider ──────────────────────────────────────────────────────────────────── #[ test ] fn slider_handler_variant_is_slider() { let view: Element = column() .push( slider( 0.5 ).on_change( |v| Msg::Brightness( v ) ) ) .into(); let surface = render_view( view ); let h = surface.handlers( surface.widget_rects()[ 0 ].flat_idx ).unwrap(); assert!( h.is_slider() ); assert!( !h.is_text_input() ); } #[ test ] fn slider_change_msg_wraps_value_through_callback() { let view: Element = column() .push( slider( 0.0 ).on_change( |v| Msg::Brightness( v ) ) ) .into(); let surface = render_view( view ); let h = surface.handlers( surface.widget_rects()[ 0 ].flat_idx ).unwrap(); assert_eq!( h.slider_change_msg( 0.0 ), Some( Msg::Brightness( 0.0 ) ) ); assert_eq!( h.slider_change_msg( 0.5 ), Some( Msg::Brightness( 0.5 ) ) ); assert_eq!( h.slider_change_msg( 1.0 ), Some( Msg::Brightness( 1.0 ) ) ); } #[ test ] fn slider_value_from_pos_at_left_edge_is_zero() { let view: Element = column() .push( slider( 0.5 ).on_change( |v| Msg::Brightness( v ) ) ) .into(); let surface = render_view( view ); let w = &surface.widget_rects()[ 0 ]; let left = Point { x: w.rect.x, y: w.rect.y + w.rect.height * 0.5 }; let v = w.handlers.slider_value_from_pos( w.rect, left ); assert!( ( v - 0.0 ).abs() < 1e-3 ); } #[ test ] fn slider_value_from_pos_at_right_edge_is_one() { let view: Element = column() .push( slider( 0.5 ).on_change( |v| Msg::Brightness( v ) ) ) .into(); let surface = render_view( view ); let w = &surface.widget_rects()[ 0 ]; let right = Point { x: w.rect.x + w.rect.width, y: w.rect.y + w.rect.height * 0.5 }; let v = w.handlers.slider_value_from_pos( w.rect, right ); assert!( ( v - 1.0 ).abs() < 1e-3 ); } #[ test ] fn slider_value_from_pos_at_midpoint_is_one_half() { let view: Element = column() .push( slider( 0.0 ).on_change( |v| Msg::Brightness( v ) ) ) .into(); let surface = render_view( view ); let w = &surface.widget_rects()[ 0 ]; let mid = Point { x: w.rect.x + w.rect.width * 0.5, y: w.rect.y + w.rect.height * 0.5, }; let v = w.handlers.slider_value_from_pos( w.rect, mid ); assert!( ( v - 0.5 ).abs() < 1e-3 ); } #[ test ] fn slider_value_from_pos_clamps_outside_rect() { // Pointer way outside the rect on either side must clamp to 0 or 1. let view: Element = column() .push( slider( 0.5 ).on_change( |v| Msg::Brightness( v ) ) ) .into(); let surface = render_view( view ); let w = &surface.widget_rects()[ 0 ]; let far_left = Point { x: w.rect.x - 200.0, y: w.rect.y + 10.0 }; let far_right = Point { x: w.rect.x + w.rect.width + 200.0, y: w.rect.y + 10.0 }; assert_eq!( w.handlers.slider_value_from_pos( w.rect, far_left ), 0.0 ); assert_eq!( w.handlers.slider_value_from_pos( w.rect, far_right ), 1.0 ); } // Bonus: confirm slider_value_from_pos returns 0.0 for non-slider variants — // gives the gesture state machine a safe fallback when a slider handler gets // swapped out mid-gesture. #[ test ] fn slider_value_from_pos_returns_zero_for_non_slider_handler() { use ltk::button; let view: Element = column() .push( button( "go" ).on_press( Msg::WifiToggled ) ) .into(); let surface = render_view( view ); let w = &surface.widget_rects()[ 0 ]; let mid = Point { x: w.rect.x + w.rect.width * 0.5, y: w.rect.y + w.rect.height * 0.5, }; assert_eq!( w.handlers.slider_value_from_pos( w.rect, mid ), 0.0 ); } // ── SliderAxis sanity ───────────────────────────────────────────────────────── #[ test ] fn slider_axis_enum_is_publicly_exposed() { // Compile-time guard: SliderAxis must remain a publicly-named enum so // downstream apps can match on it (e.g. when wrapping value_from_pos // for a custom hit area). let _ = SliderAxis::Horizontal; let _ = SliderAxis::Vertical; } // Bonus: ensure the rect from layout has non-zero size so the value tests // above are meaningful (if rect.width were 0 every position lookup would // degenerate to 0 / 0). #[ test ] fn slider_layout_rect_has_non_zero_width() { let view: Element = column() .push( slider( 0.5 ).on_change( |v| Msg::Brightness( v ) ) ) .into(); let surface = render_view( view ); let w: Rect = surface.widget_rects()[ 0 ].rect; assert!( w.width > 0.0 ); assert!( w.height > 0.0 ); }