Example 02 realized
This commit is contained in:
@@ -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(¤t_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> {
|
||||
|
||||
Reference in New Issue
Block a user