Merge origin/main into macos

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Will Temple
2026-05-16 16:38:10 -04:00
66 changed files with 9558 additions and 2326 deletions

357
lib/ruin_app/src/input.rs Normal file
View File

@@ -0,0 +1,357 @@
use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
use ruin_ui::{
CursorIcon, DisplayItem, ElementId, HitTarget, InteractionTree, KeyboardEvent, PointerButton,
PointerEvent, Quad, RoutedPointerEvent, RoutedPointerEventKind, WindowController,
};
use crate::hooks::{Shortcut, ShortcutScope, Signal};
use crate::view::View;
pub(crate) type PressHandler = Rc<dyn Fn(&RoutedPointerEvent) + 'static>;
pub(crate) type ScrollHandler = Rc<dyn Fn(&RoutedPointerEvent, &InteractionTree) + 'static>;
pub(crate) type KeyHandler = Rc<dyn Fn(&KeyboardEvent, &InteractionTree) -> bool + 'static>;
#[derive(Clone, Default)]
pub(crate) struct EventBindings {
pub(crate) on_press: HashMap<ElementId, PressHandler>,
pub(crate) on_scroll: HashMap<ElementId, ScrollHandler>,
pub(crate) on_key: HashMap<ElementId, KeyHandler>,
}
impl EventBindings {
pub(crate) fn extend(&mut self, other: EventBindings) {
self.on_press.extend(other.on_press);
self.on_scroll.extend(other.on_scroll);
self.on_key.extend(other.on_key);
}
pub(crate) fn dispatch(
&self,
event: &RoutedPointerEvent,
interaction_tree: &InteractionTree,
hovered_targets: &[HitTarget],
) {
match event.kind {
RoutedPointerEventKind::Up {
button: PointerButton::Primary,
} => {
if let Some(element_id) = event.target.element_id
&& let Some(handler) = self.on_press.get(&element_id)
{
handler(event);
}
if let Some(handler) = scroll_handler_for_event(
&self.on_scroll,
event.target.element_id,
hovered_targets,
) {
handler(event, interaction_tree);
}
}
RoutedPointerEventKind::Down {
button: PointerButton::Primary,
}
| RoutedPointerEventKind::Move
| RoutedPointerEventKind::Scroll { .. }
| RoutedPointerEventKind::Leave => {
if let Some(handler) = scroll_handler_for_event(
&self.on_scroll,
event.target.element_id,
hovered_targets,
) {
handler(event, interaction_tree);
}
}
_ => {}
}
}
pub(crate) fn dispatch_key(
&self,
focused_element: Option<ElementId>,
event: &KeyboardEvent,
interaction_tree: &InteractionTree,
) {
let Some(handler) = key_handler_for_focus(&self.on_key, focused_element, interaction_tree)
else {
return;
};
let _ = handler(event, interaction_tree);
}
}
#[derive(Clone, Copy, PartialEq)]
pub(crate) struct ScrollbarDrag {
pub(crate) start_pointer_y: f32,
pub(crate) start_offset_y: f32,
}
#[derive(Clone, Copy, PartialEq)]
pub(crate) struct TextSelection {
pub(crate) element_id: ElementId,
pub(crate) anchor: usize,
pub(crate) focus: usize,
}
#[derive(Clone, Copy, PartialEq)]
pub(crate) struct TextSelectionDrag {
pub(crate) element_id: ElementId,
pub(crate) anchor: usize,
}
pub(crate) struct TextSelectionState {
pub(crate) selection: RefCell<Option<TextSelection>>,
pub(crate) drag: RefCell<Option<TextSelectionDrag>>,
pub(crate) version: Signal<u64>,
}
impl TextSelectionState {
pub(crate) fn new() -> Self {
Self {
selection: RefCell::new(None),
drag: RefCell::new(None),
version: Signal::new(0),
}
}
}
pub(crate) struct InputState {
pub(crate) current_cursor: CursorIcon,
pub(crate) focused_element: Option<ElementId>,
pub(crate) text_selection: Rc<TextSelectionState>,
pub(crate) hovered_elements: Signal<Vec<ElementId>>,
pub(crate) pressed_elements: Signal<Vec<ElementId>>,
pub(crate) interaction_version: Signal<u64>,
}
impl InputState {
pub(crate) fn new() -> Self {
Self {
current_cursor: CursorIcon::Default,
focused_element: None,
text_selection: Rc::new(TextSelectionState::new()),
hovered_elements: Signal::new(Vec::new()),
pressed_elements: Signal::new(Vec::new()),
interaction_version: Signal::new(0),
}
}
}
#[derive(Clone)]
pub(crate) struct ShortcutBinding {
pub(crate) shortcut: Shortcut,
pub(crate) scope: ShortcutScope,
pub(crate) action: Rc<dyn Fn(&InteractionTree)>,
}
impl ShortcutBinding {
pub(crate) fn matches(
&self,
event: &KeyboardEvent,
focused_element: Option<ElementId>,
interaction_tree: &InteractionTree,
) -> bool {
self.shortcut.matches(event) && self.scope.matches(focused_element, interaction_tree)
}
pub(crate) fn trigger(&self, interaction_tree: &InteractionTree) {
(self.action)(interaction_tree);
}
}
pub(crate) fn clamp_scroll_offset(offset_y: f32, max_offset_y: f32) -> f32 {
offset_y.clamp(0.0, max_offset_y.max(0.0))
}
pub(crate) fn apply_text_selection_overlay(
scene: &mut ruin_ui::SceneSnapshot,
selection: Option<TextSelection>,
) {
let Some(selection) = selection else {
return;
};
let mut next_items = Vec::with_capacity(scene.items.len());
for item in scene.items.drain(..) {
match item {
DisplayItem::Text(mut text) if text.element_id == Some(selection.element_id) => {
for rect in text.selection_rects(selection.anchor, selection.focus) {
next_items.push(DisplayItem::Quad(Quad::new(
rect,
text.selection_style.highlight_color,
)));
}
text.apply_selected_text_color(selection.anchor, selection.focus);
next_items.push(DisplayItem::Text(text));
}
other => next_items.push(other),
}
}
scene.items = next_items;
}
pub(crate) fn sync_primary_selection(
window: &WindowController,
interaction_tree: &InteractionTree,
selection: Option<TextSelection>,
) -> crate::Result<()> {
#[cfg(not(target_os = "linux"))]
{
let _ = (window, interaction_tree, selection);
return Ok(());
}
#[cfg(target_os = "linux")]
{
let Some(selection) = selection else {
window.set_primary_selection_text(String::new())?;
return Ok(());
};
let Some(text) = interaction_tree.text_for_element(selection.element_id) else {
return Ok(());
};
let copied = text
.selected_text(selection.anchor, selection.focus)
.unwrap_or_default()
.to_owned();
window.set_primary_selection_text(copied)?;
Ok(())
}
}
pub(crate) fn scroll_handler_for_event<'a>(
handlers: &'a HashMap<ElementId, ScrollHandler>,
direct_target: Option<ElementId>,
hovered_targets: &[HitTarget],
) -> Option<&'a ScrollHandler> {
if let Some(element_id) = direct_target
&& let Some(handler) = handlers.get(&element_id)
{
return Some(handler);
}
hovered_targets
.iter()
.rev()
.filter_map(|target| target.element_id)
.find_map(|element_id| handlers.get(&element_id))
}
pub(crate) fn focused_element_for_pointer(
interaction_tree: &InteractionTree,
event: &PointerEvent,
) -> Option<ElementId> {
let hit_path = interaction_tree.hit_path(event.position);
hit_path
.iter()
.rev()
.find_map(|target| target.focusable.then_some(target.element_id).flatten())
.or_else(|| hit_path.iter().rev().find_map(|target| target.element_id))
}
pub(crate) 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)
}
pub(crate) 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()
.find_map(|element_id| handlers.get(&element_id))
}
pub(crate) 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
}
}
pub(crate) 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
}
// View helper methods for attaching handlers — used by primitives
impl View {
pub(crate) fn with_press_handler(
mut self,
element_id: ElementId,
handler: PressHandler,
) -> Self {
self.bindings.on_press.insert(element_id, handler);
self
}
pub(crate) fn with_scroll_handler(
mut self,
element_id: ElementId,
handler: ScrollHandler,
) -> Self {
self.bindings.on_scroll.insert(element_id, handler);
self
}
pub(crate) fn with_key_handler(mut self, element_id: ElementId, handler: KeyHandler) -> Self {
self.bindings.on_key.insert(element_id, handler);
self
}
}