use ruin_app::prelude::*; #[ruin_runtime::async_main] async fn main() -> ruin_app::Result<()> { App::new() .window( Window::new() .title("RUIN Widget Refs and Commands") .app_id("dev.ruin.widget-refs") .size(1100.0, 760.0), ) .mount(view! { WidgetRefsAndCommandsDemo() {} }) .run() .await } #[component] fn WidgetRefsAndCommandsDemo() -> impl IntoView { let selected_index = use_signal(|| 0_usize); let last_command = use_signal(|| "No command has been run yet.".to_string()); let results_scroll = use_signal(|| 0.0_f32); let results_list = use_widget_ref::(); use_window_title({ let selected_index = selected_index.clone(); move || format!("RUIN Commands ({})", COMMANDS[selected_index.get()].title) }); use_shortcut( Shortcut::new(Key::Character('k')).with_ctrl(), ShortcutScope::Application, { let last_command = last_command.clone(); move || { let _ = last_command.set( "Application shortcut fired. Click inside the command list, then use \ ArrowUp / ArrowDown / Enter." .to_string(), ); } }, ); use_shortcut( Shortcut::new(Key::Enter), ShortcutScope::FocusedWithin(results_list.focus_scope()), { let selected_index = selected_index.clone(); let last_command = last_command.clone(); move || { let command = &COMMANDS[selected_index.get()]; let _ = last_command.set(format!("Executed: {}", command.title)); } }, ); let selected = &COMMANDS[selected_index.get()]; view! { column(gap = 16.0, padding = 24.0) { text(role = TextRole::Heading(1), size = 32.0, weight = FontWeight::Semibold) { "Widget refs and commands" } text(color = colors::muted(), wrap = TextWrap::Word) { "This is a smaller truthful slice of example 02: the results list installs a widget \ ref-backed focus scope, typed shortcuts are explicit about application vs focused-within \ behavior, and the scroll box keeps its own default arrow-key scrolling." } block( padding = 16.0, gap = 10.0, background = surfaces::raised(), border_radius = 12.0, ) { text(size = 18.0, weight = FontWeight::Semibold) { "Shortcuts" } text(color = colors::muted(), wrap = TextWrap::Word) { "Ctrl+K writes a global status message. Click any command row below to focus the \ list, then use ArrowUp / ArrowDown / Enter. The explicit shortcuts now override \ the scroll box's default arrow-key scrolling in this focused scope." } } row(gap = 16.0) { block( flex = 1.0, padding = 16.0, gap = 10.0, background = surfaces::raised(), border_radius = 12.0, ) { text(size = 18.0, weight = FontWeight::Semibold) { "Commands" } button( on_press = { let selected_index = selected_index.clone(); let last_command = last_command.clone(); move |_| { let command = &COMMANDS[selected_index.get()]; let _ = last_command.set(format!("Executed: {}", command.title)); } }, ) { "Run selected command" } scroll_box( widget_ref = results_list.clone(), offset_y = results_scroll.clone(), height = 420.0, padding = 12.0, background = surfaces::canvas(), border_radius = 10.0, border = (2.0, colors::muted()), ) { column(gap = 8.0) { CommandRows( selected_index = selected_index.clone(), results_list = results_list.clone(), results_scroll = results_scroll.clone(), ) {} } } } block( width = 320.0, padding = 16.0, gap = 10.0, background = surfaces::raised(), border_radius = 12.0, ) { text(size = 18.0, weight = FontWeight::Semibold) { "Selection" } text() { "title = "; selected.title } text(color = colors::muted(), wrap = TextWrap::Word) { selected.subtitle } block( padding = 12.0, gap = 8.0, background = surfaces::canvas(), border_radius = 10.0, ) { text(size = 16.0, weight = FontWeight::Semibold) { "Last command" } text(color = colors::muted(), wrap = TextWrap::Word) { last_command.clone() } } } } } } } #[component] fn CommandRows( selected_index: Signal, results_list: WidgetRef, results_scroll: Signal, ) -> impl IntoView { let row0 = use_widget_ref::(); let row1 = use_widget_ref::(); let row2 = use_widget_ref::(); let row3 = use_widget_ref::(); let row4 = use_widget_ref::(); let row5 = use_widget_ref::(); let row6 = use_widget_ref::(); let row7 = use_widget_ref::(); let row8 = use_widget_ref::(); let row9 = use_widget_ref::(); use_shortcut_with_context( Shortcut::new(Key::ArrowDown), ShortcutScope::FocusedWithin(results_list.focus_scope()), { let selected_index = selected_index.clone(); let results_list = results_list.clone(); let results_scroll = results_scroll.clone(); let row0 = row0.clone(); let row1 = row1.clone(); let row2 = row2.clone(); let row3 = row3.clone(); let row4 = row4.clone(); let row5 = row5.clone(); let row6 = row6.clone(); let row7 = row7.clone(); let row8 = row8.clone(); let row9 = row9.clone(); move |interaction_tree| { let next_index = (selected_index.get() + 1).min(COMMANDS.len().saturating_sub(1)); let _ = selected_index.set(next_index); match next_index { 0 => scroll_widget_into_view( interaction_tree, &results_list, &row0, &results_scroll, ), 1 => scroll_widget_into_view( interaction_tree, &results_list, &row1, &results_scroll, ), 2 => scroll_widget_into_view( interaction_tree, &results_list, &row2, &results_scroll, ), 3 => scroll_widget_into_view( interaction_tree, &results_list, &row3, &results_scroll, ), 4 => scroll_widget_into_view( interaction_tree, &results_list, &row4, &results_scroll, ), 5 => scroll_widget_into_view( interaction_tree, &results_list, &row5, &results_scroll, ), 6 => scroll_widget_into_view( interaction_tree, &results_list, &row6, &results_scroll, ), 7 => scroll_widget_into_view( interaction_tree, &results_list, &row7, &results_scroll, ), 8 => scroll_widget_into_view( interaction_tree, &results_list, &row8, &results_scroll, ), 9 => scroll_widget_into_view( interaction_tree, &results_list, &row9, &results_scroll, ), _ => {} } } }, ); use_shortcut_with_context( Shortcut::new(Key::ArrowUp), ShortcutScope::FocusedWithin(results_list.focus_scope()), { let selected_index = selected_index.clone(); let results_list = results_list.clone(); let results_scroll = results_scroll.clone(); let row0 = row0.clone(); let row1 = row1.clone(); let row2 = row2.clone(); let row3 = row3.clone(); let row4 = row4.clone(); let row5 = row5.clone(); let row6 = row6.clone(); let row7 = row7.clone(); let row8 = row8.clone(); let row9 = row9.clone(); move |interaction_tree| { let next_index = selected_index.get().saturating_sub(1); let _ = selected_index.set(next_index); match next_index { 0 => scroll_widget_into_view( interaction_tree, &results_list, &row0, &results_scroll, ), 1 => scroll_widget_into_view( interaction_tree, &results_list, &row1, &results_scroll, ), 2 => scroll_widget_into_view( interaction_tree, &results_list, &row2, &results_scroll, ), 3 => scroll_widget_into_view( interaction_tree, &results_list, &row3, &results_scroll, ), 4 => scroll_widget_into_view( interaction_tree, &results_list, &row4, &results_scroll, ), 5 => scroll_widget_into_view( interaction_tree, &results_list, &row5, &results_scroll, ), 6 => scroll_widget_into_view( interaction_tree, &results_list, &row6, &results_scroll, ), 7 => scroll_widget_into_view( interaction_tree, &results_list, &row7, &results_scroll, ), 8 => scroll_widget_into_view( interaction_tree, &results_list, &row8, &results_scroll, ), 9 => scroll_widget_into_view( interaction_tree, &results_list, &row9, &results_scroll, ), _ => {} } } }, ); view! { column(gap = 8.0) { column(gap = 8.0) { CommandRow0(selected_index = selected_index.clone(), row_ref = row0.clone()) {} CommandRow1(selected_index = selected_index.clone(), row_ref = row1.clone()) {} CommandRow2(selected_index = selected_index.clone(), row_ref = row2.clone()) {} CommandRow3(selected_index = selected_index.clone(), row_ref = row3.clone()) {} CommandRow4(selected_index = selected_index.clone(), row_ref = row4.clone()) {} } column(gap = 8.0) { CommandRow5(selected_index = selected_index.clone(), row_ref = row5.clone()) {} CommandRow6(selected_index = selected_index.clone(), row_ref = row6.clone()) {} CommandRow7(selected_index = selected_index.clone(), row_ref = row7.clone()) {} CommandRow8(selected_index = selected_index.clone(), row_ref = row8.clone()) {} CommandRow9(selected_index = selected_index.clone(), row_ref = row9.clone()) {} } } } } macro_rules! command_row_component { ($name:ident, $index:expr) => { #[component] fn $name(selected_index: Signal, row_ref: WidgetRef) -> impl IntoView { let command = &COMMANDS[$index]; let prefix = if selected_index.get() == $index { "> " } else { " " }; let background = if selected_index.get() == $index { surfaces::interactive() } else { surfaces::interactive_muted() }; view! { block( widget_ref = row_ref, padding = 12.0, gap = 6.0, background = background, border_radius = 10.0, ) { text(weight = FontWeight::Semibold) { prefix; command.title } text(color = colors::muted(), wrap = TextWrap::Word) { command.subtitle } } } } }; } command_row_component!(CommandRow0, 0); command_row_component!(CommandRow1, 1); command_row_component!(CommandRow2, 2); command_row_component!(CommandRow3, 3); command_row_component!(CommandRow4, 4); command_row_component!(CommandRow5, 5); command_row_component!(CommandRow6, 6); command_row_component!(CommandRow7, 7); command_row_component!(CommandRow8, 8); command_row_component!(CommandRow9, 9); struct Command { title: &'static str, subtitle: &'static str, } static COMMANDS: &[Command] = &[ Command { title: "Open workspace search", subtitle: "Jump into a project-wide search panel with the current repository root prefilled.", }, Command { title: "Show diagnostics", subtitle: "Collect the latest compile, lint, and runtime diagnostics into a single review list.", }, Command { title: "Reload shaders", subtitle: "Force a renderer-side asset refresh without restarting the process.", }, Command { title: "Toggle layout bounds", subtitle: "Render debug outlines for container and text layout boxes.", }, Command { title: "Profile text layout", subtitle: "Capture a narrow text-layout performance sample for the active window.", }, Command { title: "Dump interaction tree", subtitle: "Serialize the current focus and hit-test tree for debugging pointer routing.", }, Command { title: "Clear transient overlays", subtitle: "Dismiss temporary banners, confirmations, and other transient UI state.", }, Command { title: "Restart background tasks", subtitle: "Cancel and requeue currently tracked background work items.", }, Command { title: "Copy scene summary", subtitle: "Place a concise scene-graph summary onto the clipboard for issue reports.", }, Command { title: "Open command help", subtitle: "Explain the currently wired shortcut scopes and default list interactions.", }, ]; fn scroll_widget_into_view( interaction_tree: &InteractionTree, scroll_box: &WidgetRef, item: &WidgetRef, scroll_offset: &Signal, ) { let Some(scroll_box_id) = scroll_box.element_id() else { return; }; let Some(item_id) = item.element_id() else { return; }; let Some(metrics) = interaction_tree.scroll_metrics_for_element(scroll_box_id) else { return; }; let Some(item_rect) = interaction_tree.rect_for_element(item_id) else { return; }; let viewport_top = metrics.viewport_rect.origin.y; let viewport_bottom = viewport_top + metrics.viewport_rect.size.height; let item_top = item_rect.origin.y; let item_bottom = item_rect.origin.y + item_rect.size.height; scroll_offset.update(|offset| { let mut next_offset = *offset; if item_top < viewport_top { next_offset -= viewport_top - item_top; } else if item_bottom > viewport_bottom { next_offset += item_bottom - viewport_bottom; } *offset = next_offset.clamp(0.0, metrics.max_offset_y); }); }