Example 02 realized

This commit is contained in:
2026-03-21 23:25:28 -04:00
parent c9966b79ef
commit bc287f615d
6 changed files with 1016 additions and 34 deletions

View File

@@ -21,3 +21,7 @@ path = "example/00_bootstrap_and_counter.rs"
[[example]]
name = "01_async_data_and_effects"
path = "example/01_async_data_and_effects.rs"
[[example]]
name = "02_widget_refs_and_commands"
path = "example/02_widget_refs_and_commands.rs"

View File

@@ -0,0 +1,496 @@
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);
});
}

View File

@@ -9,6 +9,7 @@ use std::collections::HashMap;
use std::error::Error;
use std::future::Future;
use std::iter;
use std::marker::PhantomData;
use std::rc::Rc;
use ruin_reactivity::effect;
@@ -148,6 +149,7 @@ impl<M: Mountable> MountedApp<M> {
let text_system = Rc::new(RefCell::new(TextSystem::new()));
let interaction_tree = Rc::new(RefCell::new(None::<InteractionTree>));
let bindings = Rc::new(RefCell::new(EventBindings::default()));
let shortcuts = Rc::new(RefCell::new(Vec::<ShortcutBinding>::new()));
let current_title = Rc::new(RefCell::new(None::<String>));
let mut input_state = InputState::new();
let mut pointer_router = PointerRouter::new();
@@ -158,6 +160,7 @@ impl<M: Mountable> MountedApp<M> {
let text_system = Rc::clone(&text_system);
let interaction_tree = Rc::clone(&interaction_tree);
let bindings = Rc::clone(&bindings);
let shortcuts = Rc::clone(&shortcuts);
let current_title = Rc::clone(&current_title);
let root = Rc::clone(&root);
let render_state = Rc::clone(&render_state);
@@ -193,6 +196,7 @@ impl<M: Mountable> MountedApp<M> {
*interaction_tree.borrow_mut() = Some(next_interaction_tree);
*bindings.borrow_mut() = render_output.view.bindings;
*shortcuts.borrow_mut() = render_output.side_effects.shortcuts.clone();
window
.replace_scene(scene)
.expect("window should remain alive while the app is running");
@@ -226,6 +230,7 @@ impl<M: Mountable> MountedApp<M> {
Self::handle_keyboard_event(
&interaction_tree,
&bindings,
&shortcuts,
&input_state,
event,
)?;
@@ -309,6 +314,7 @@ impl<M: Mountable> MountedApp<M> {
fn handle_keyboard_event(
interaction_tree: &RefCell<Option<InteractionTree>>,
bindings: &RefCell<EventBindings>,
shortcuts: &RefCell<Vec<ShortcutBinding>>,
input_state: &InputState,
event: KeyboardEvent,
) -> Result<()> {
@@ -316,6 +322,17 @@ impl<M: Mountable> MountedApp<M> {
let Some(interaction_tree) = interaction_tree.as_ref() else {
return Ok(());
};
let shortcut_bindings = shortcuts.borrow().clone();
let mut consumed = false;
for shortcut in shortcut_bindings {
if shortcut.matches(&event, input_state.focused_element, interaction_tree) {
shortcut.trigger(interaction_tree);
consumed = true;
}
}
if consumed {
return Ok(());
}
bindings
.borrow()
.dispatch_key(input_state.focused_element, &event, interaction_tree);
@@ -676,18 +693,21 @@ pub mod surfaces {
pub fn column() -> ContainerBuilder {
ContainerBuilder {
element: Element::column().background(surfaces::canvas()),
widget_ref: None,
}
}
pub fn row() -> ContainerBuilder {
ContainerBuilder {
element: Element::row(),
widget_ref: None,
}
}
pub fn block() -> ContainerBuilder {
ContainerBuilder {
element: Element::column(),
widget_ref: None,
}
}
@@ -704,11 +724,13 @@ pub fn scroll_box() -> ScrollBoxBuilder {
element: Element::scroll_box(0.0),
offset_y: None,
drag: with_hook_slot(|| Signal::new(None::<ScrollbarDrag>), |drag| drag.clone()),
widget_ref: None,
}
}
pub struct ContainerBuilder {
element: Element,
widget_ref: Option<Signal<Option<ElementId>>>,
}
impl ContainerBuilder {
@@ -753,7 +775,17 @@ impl ContainerBuilder {
self
}
pub fn children(self, children: impl Children) -> View {
pub fn widget_ref<T>(mut self, widget_ref: WidgetRef<T>) -> Self {
self.widget_ref = Some(widget_ref.element_id.clone());
self
}
pub fn children(mut self, children: impl Children) -> View {
if let Some(widget_ref) = &self.widget_ref {
let element_id = allocate_element_id();
self.element = self.element.id(element_id);
let _ = widget_ref.set(Some(element_id));
}
View::from_container(self.element, children.into_views())
}
}
@@ -762,6 +794,7 @@ pub struct ScrollBoxBuilder {
element: Element,
offset_y: Option<Signal<f32>>,
drag: Signal<Option<ScrollbarDrag>>,
widget_ref: Option<Signal<Option<ElementId>>>,
}
impl ScrollBoxBuilder {
@@ -812,9 +845,17 @@ impl ScrollBoxBuilder {
self
}
pub fn widget_ref<T>(mut self, widget_ref: WidgetRef<T>) -> Self {
self.widget_ref = Some(widget_ref.element_id.clone());
self
}
pub fn children(mut self, children: impl Children) -> View {
let element_id = allocate_element_id();
self.element = self.element.id(element_id);
if let Some(widget_ref) = &self.widget_ref {
let _ = widget_ref.set(Some(element_id));
}
let mut view = View::from_container(self.element, children.into_views());
if let Some(offset_y) = self.offset_y {
@@ -1129,6 +1170,169 @@ pub fn use_window_title(compute: impl FnOnce() -> String) {
});
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum Key {
Character(char),
Enter,
ArrowUp,
ArrowDown,
Home,
End,
}
impl Key {
fn matches(&self, event: &KeyboardEvent) -> bool {
match (self, &event.key) {
(Self::Character(expected), KeyboardKey::Character(actual)) => actual
.chars()
.next()
.is_some_and(|actual| actual.eq_ignore_ascii_case(expected)),
(Self::Enter, KeyboardKey::Enter) => true,
(Self::ArrowUp, KeyboardKey::ArrowUp) => true,
(Self::ArrowDown, KeyboardKey::ArrowDown) => true,
(Self::Home, KeyboardKey::Home) => true,
(Self::End, KeyboardKey::End) => true,
_ => false,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Shortcut {
key: Key,
control: bool,
shift: bool,
alt: bool,
super_key: bool,
}
impl Shortcut {
pub fn new(key: Key) -> Self {
Self {
key,
control: false,
shift: false,
alt: false,
super_key: false,
}
}
pub fn with_ctrl(mut self) -> Self {
self.control = true;
self
}
pub fn with_shift(mut self) -> Self {
self.shift = true;
self
}
fn matches(&self, event: &KeyboardEvent) -> bool {
event.kind == KeyboardEventKind::Pressed
&& self.key.matches(event)
&& event.modifiers.control == self.control
&& event.modifiers.shift == self.shift
&& event.modifiers.alt == self.alt
&& event.modifiers.super_key == self.super_key
}
}
#[derive(Clone)]
pub struct FocusScope {
element_id: Signal<Option<ElementId>>,
}
#[derive(Clone)]
pub enum ShortcutScope {
Application,
FocusedWithin(FocusScope),
}
impl ShortcutScope {
fn matches(
&self,
focused_element: Option<ElementId>,
interaction_tree: &InteractionTree,
) -> bool {
match self {
Self::Application => true,
Self::FocusedWithin(scope) => {
let Some(scope_element) = scope.element_id.get() else {
return false;
};
let Some(focused_element) = focused_element else {
return false;
};
element_contains_element(interaction_tree, scope_element, focused_element)
}
}
}
}
pub struct WidgetRef<T> {
element_id: Signal<Option<ElementId>>,
_marker: PhantomData<fn() -> T>,
}
impl<T> Clone for WidgetRef<T> {
fn clone(&self) -> Self {
Self {
element_id: self.element_id.clone(),
_marker: PhantomData,
}
}
}
impl<T> WidgetRef<T> {
fn new() -> Self {
Self {
element_id: Signal::new(None),
_marker: PhantomData,
}
}
pub fn element_id(&self) -> Option<ElementId> {
self.element_id.get()
}
pub fn focus_scope(&self) -> FocusScope {
FocusScope {
element_id: self.element_id.clone(),
}
}
}
pub struct ScrollBoxWidget;
pub struct BlockWidget;
pub fn use_widget_ref<T: 'static>() -> WidgetRef<T> {
with_hook_slot(WidgetRef::new, |widget_ref: &mut WidgetRef<T>| {
widget_ref.clone()
})
}
pub fn use_shortcut(shortcut: Shortcut, scope: ShortcutScope, action: impl Fn() + 'static) {
use_shortcut_with_context(shortcut, scope, move |_| action());
}
pub fn use_shortcut_with_context(
shortcut: Shortcut,
scope: ShortcutScope,
action: impl Fn(&InteractionTree) + 'static,
) {
with_render_context_state(|context| {
context
.side_effects
.borrow_mut()
.shortcuts
.push(ShortcutBinding {
shortcut,
scope,
action: Rc::new(action),
});
});
}
#[derive(Clone)]
pub struct Resource<T, E> {
state: Signal<ResourceState<T, E>>,
@@ -1190,9 +1394,32 @@ struct RenderState {
next_element_id: StdCell<u64>,
}
#[derive(Clone, Default, PartialEq)]
#[derive(Clone)]
struct ShortcutBinding {
shortcut: Shortcut,
scope: ShortcutScope,
action: Rc<dyn Fn(&InteractionTree)>,
}
impl ShortcutBinding {
fn matches(
&self,
event: &KeyboardEvent,
focused_element: Option<ElementId>,
interaction_tree: &InteractionTree,
) -> bool {
self.shortcut.matches(event) && self.scope.matches(focused_element, interaction_tree)
}
fn trigger(&self, interaction_tree: &InteractionTree) {
(self.action)(interaction_tree);
}
}
#[derive(Clone, Default)]
struct RenderSideEffects {
window_title: Option<String>,
shortcuts: Vec<ShortcutBinding>,
}
#[derive(Clone)]
@@ -1384,10 +1611,8 @@ impl EventBindings {
event: &KeyboardEvent,
interaction_tree: &InteractionTree,
) {
let Some(element_id) = focused_element else {
return;
};
let Some(handler) = self.on_key.get(&element_id) else {
let Some(handler) = key_handler_for_focus(&self.on_key, focused_element, interaction_tree)
else {
return;
};
let _ = handler(event, interaction_tree);
@@ -1526,18 +1751,98 @@ fn focused_element_for_pointer(
.find_map(|target| target.focusable.then_some(target.element_id).flatten())
}
fn element_contains_element(
interaction_tree: &InteractionTree,
ancestor: ElementId,
descendant: ElementId,
) -> bool {
fn contains_descendant(node: &ruin_ui::LayoutNode, descendant: ElementId) -> bool {
if node.element_id == Some(descendant) {
return true;
}
node.children
.iter()
.any(|child| contains_descendant(child, descendant))
}
fn ancestor_contains(
node: &ruin_ui::LayoutNode,
ancestor: ElementId,
descendant: ElementId,
) -> bool {
if node.element_id == Some(ancestor) {
return contains_descendant(node, descendant);
}
node.children
.iter()
.any(|child| ancestor_contains(child, ancestor, descendant))
}
ancestor_contains(&interaction_tree.root, ancestor, descendant)
}
fn key_handler_for_focus<'a>(
handlers: &'a HashMap<ElementId, KeyHandler>,
focused_element: Option<ElementId>,
interaction_tree: &InteractionTree,
) -> Option<&'a KeyHandler> {
let focused_element = focused_element?;
focused_ancestor_chain(&interaction_tree.root, focused_element)?
.into_iter()
.rev()
.find_map(|element_id| handlers.get(&element_id))
}
fn focused_ancestor_chain(
node: &ruin_ui::LayoutNode,
focused_element: ElementId,
) -> Option<Vec<ElementId>> {
let mut chain = Vec::new();
if build_focused_ancestor_chain(node, focused_element, &mut chain) {
Some(chain)
} else {
None
}
}
fn build_focused_ancestor_chain(
node: &ruin_ui::LayoutNode,
focused_element: ElementId,
chain: &mut Vec<ElementId>,
) -> bool {
if node.element_id == Some(focused_element) {
if let Some(element_id) = node.element_id {
chain.push(element_id);
}
return true;
}
for child in &node.children {
if build_focused_ancestor_chain(child, focused_element, chain) {
if let Some(element_id) = node.element_id {
chain.push(element_id);
}
return true;
}
}
false
}
pub mod prelude {
pub use crate::{
App, ButtonBuilder, Children, Component, ContainerBuilder, FontWeight, IntoBorder,
IntoEdges, IntoView, Memo, Mountable, Pending, Ready, Resource, ResourceState, Result,
ScrollBoxBuilder, Signal, TextBuilder, TextChildren, TextRole, View, Window, block, button,
colors, column, component, row, scroll_box, surfaces, text, use_effect, use_memo,
use_resource, use_signal, use_window_title, view,
App, BlockWidget, ButtonBuilder, Children, Component, ContainerBuilder, FocusScope,
FontWeight, IntoBorder, IntoEdges, IntoView, Key, Memo, Mountable, Pending, Ready,
Resource, ResourceState, Result, ScrollBoxBuilder, ScrollBoxWidget, Shortcut,
ShortcutScope, Signal, TextBuilder, TextChildren, TextRole, View, WidgetRef, Window, block,
button, colors, column, component, row, scroll_box, surfaces, text, use_effect, use_memo,
use_resource, use_shortcut, use_shortcut_with_context, use_signal, use_widget_ref,
use_window_title, view,
};
pub use ruin_ui::{
Border, Color, CursorIcon, Edges, Element, ElementId, PointerButton, PointerEventKind,
RoutedPointerEvent, RoutedPointerEventKind, ScrollbarStyle, TextFontFamily, TextStyle,
TextWrap, UiSize,
Border, Color, CursorIcon, Edges, Element, ElementId, InteractionTree, PointerButton,
PointerEventKind, RoutedPointerEvent, RoutedPointerEventKind, ScrollbarStyle,
TextFontFamily, TextStyle, TextWrap, UiSize,
};
}
struct SignalInner<T> {