497 lines
18 KiB
Rust
497 lines
18 KiB
Rust
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::<ScrollBoxWidget>();
|
|
|
|
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<usize>,
|
|
results_list: WidgetRef<ScrollBoxWidget>,
|
|
results_scroll: Signal<f32>,
|
|
) -> impl IntoView {
|
|
let row0 = use_widget_ref::<BlockWidget>();
|
|
let row1 = use_widget_ref::<BlockWidget>();
|
|
let row2 = use_widget_ref::<BlockWidget>();
|
|
let row3 = use_widget_ref::<BlockWidget>();
|
|
let row4 = use_widget_ref::<BlockWidget>();
|
|
let row5 = use_widget_ref::<BlockWidget>();
|
|
let row6 = use_widget_ref::<BlockWidget>();
|
|
let row7 = use_widget_ref::<BlockWidget>();
|
|
let row8 = use_widget_ref::<BlockWidget>();
|
|
let row9 = use_widget_ref::<BlockWidget>();
|
|
|
|
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<usize>, row_ref: WidgetRef<BlockWidget>) -> 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<T>(
|
|
interaction_tree: &InteractionTree,
|
|
scroll_box: &WidgetRef<ScrollBoxWidget>,
|
|
item: &WidgetRef<T>,
|
|
scroll_offset: &Signal<f32>,
|
|
) {
|
|
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);
|
|
});
|
|
}
|