//! Keyboard entry and focus operations scoped to one property form. Focus //! targets include the draft generation so delayed operations cannot focus a //! replacement array row or a recovered document with the same semantic IDs. use crate::style; use iced::{ Border, Color, Element, Event, Length, Rectangle, Size, Task, Theme, Vector, advanced::{ Clipboard, Layout, Shell, Widget, layout, overlay, renderer, widget::{Id, Operation, Tree, operation, tree}, }, keyboard, mouse, widget::container, }; use oa_core::Id as ModelId; mod focus; #[derive(Clone, Copy)] pub struct FormScope { pub session: ModelId, pub modal: bool, } pub(super) struct FormOwner(pub Option); const REVIEW_MODAL: &str = "reference-owning-review-modal"; /// Marks the entire Review dialog without changing its layout or event flow. pub fn modal<'a, Message: 'a, R: renderer::Renderer + 'a>( content: impl Into>, ) -> Element<'a, Message, Theme, R> { Field { content: content.into(), id: Some(Id::new(REVIEW_MODAL)), on_open: None, } .into() } pub fn field_id(generation: ModelId, path: &str) -> Id { Id::from(format!("reference:{generation}:{path}")) } pub fn scope_id(session: ModelId) -> Id { Id::from(format!("reference-form:{session}")) } #[derive(Default)] struct Interaction { focused: bool, pressed: bool, identity: Option, anchor: Option, } impl operation::Focusable for Interaction { fn is_focused(&self) -> bool { self.focused } fn focus(&mut self) { self.focused = true; } fn unfocus(&mut self) { self.focused = false; self.pressed = false; } } pub struct Field<'a, Message, R = iced::Renderer> where R: renderer::Renderer, { content: Element<'a, Message, Theme, R>, id: Option, on_open: Option Message + 'a>>, } impl<'a, Message, R: renderer::Renderer> Field<'a, Message, R> { pub fn new( content: impl Into>, id: Id, on_open: impl Fn(Rectangle) -> Message + 'a, ) -> Self { Self { content: content.into(), id: Some(id), on_open: Some(Box::new(on_open)), } } } pub fn scope<'a, Message: 'a, R: renderer::Renderer + 'a>( content: impl Into>, session: ModelId, ) -> Element<'a, Message, Theme, R> { container(Field { content: content.into(), id: None, on_open: None, }) .id(scope_id(session)) .width(Length::Fill) .into() } fn run( content: &mut Element<'_, Message, Theme, R>, tree: &mut Tree, layout: Layout<'_>, renderer: &R, mut operation: Box, ) -> bool { let mut changed = false; loop { content .as_widget_mut() .operate(tree, layout, renderer, &mut *operation); match operation.finish() { operation::Outcome::Chain(next) => { operation = next; changed = true; } _ => break, } } changed } pub(super) fn tab( content: &mut Element<'_, Message, Theme, R>, tree: &mut Tree, layout: Layout<'_>, renderer: &R, scope: FormScope, backwards: bool, ) -> bool { run( content, tree, layout, renderer, Box::new(focus::relative(scope.session, backwards, scope.modal)), ) } pub(super) fn own_input( content: &mut Element<'_, Message, Theme, R>, tree: &mut Tree, layout: Layout<'_>, renderer: &R, ) { content .as_widget_mut() .operate(tree, layout, renderer, &mut focus::exclusive()); } impl Widget for Field<'_, Message, R> { fn tag(&self) -> tree::Tag { tree::Tag::of::() } fn state(&self) -> tree::State { tree::State::new(Interaction { identity: self.id.clone(), ..Default::default() }) } fn children(&self) -> Vec { vec![Tree::new(self.content.as_widget())] } fn diff(&self, tree: &mut Tree) { let state = tree.state.downcast_mut::(); if state.identity != self.id { *state = Interaction { identity: self.id.clone(), ..Default::default() }; } tree.diff_children(std::slice::from_ref(&self.content)); } fn size(&self) -> Size { self.content.as_widget().size() } fn layout(&mut self, tree: &mut Tree, renderer: &R, limits: &layout::Limits) -> layout::Node { self.content .as_widget_mut() .layout(&mut tree.children[0], renderer, limits) } fn operate( &mut self, tree: &mut Tree, layout: Layout<'_>, renderer: &R, operation: &mut dyn Operation, ) { if let Some(id) = &self.id && self.on_open.is_some() { operation.focusable( Some(id), layout.bounds(), tree.state.downcast_mut::(), ); operation.custom( Some(id), layout.bounds(), tree.state.downcast_mut::(), ); } if self.on_open.is_none() { operation.container(self.id.as_ref(), layout.bounds()); operation.traverse(&mut |operation| { self.content.as_widget_mut().operate( &mut tree.children[0], layout, renderer, operation, ) }); } else { self.content.as_widget_mut().operate( &mut tree.children[0], layout, renderer, operation, ); } } fn update( &mut self, tree: &mut Tree, event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, renderer: &R, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, ) { if let Some(on_open) = &self.on_open { let state = tree.state.downcast_mut::(); let visible = layout.bounds().intersection(viewport); let hovered = visible.is_some_and(|bounds| cursor.is_over(bounds)); let activate = match event { Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) if hovered => { state.focused = true; state.pressed = true; shell.capture_event(); shell.request_redraw(); false } Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => { let activate = state.pressed && hovered; if state.pressed { shell.capture_event(); } state.pressed = false; activate } Event::Keyboard(keyboard::Event::KeyPressed { key: keyboard::Key::Named(keyboard::key::Named::Enter | keyboard::key::Named::Space), repeat: false, .. }) if state.focused => true, Event::Mouse(mouse::Event::ButtonPressed(_)) => { state.focused = false; state.pressed = false; false } _ => false, }; if activate && let Some(bounds) = state.anchor { shell.publish(on_open(bounds)); shell.capture_event(); return; } } self.content.as_widget_mut().update( &mut tree.children[0], event, layout, cursor, renderer, clipboard, shell, viewport, ); } fn draw( &self, tree: &Tree, renderer: &mut R, theme: &Theme, renderer_style: &renderer::Style, layout: Layout<'_>, cursor: mouse::Cursor, viewport: &Rectangle, ) { if self.on_open.is_some() { let focus = tree.state.downcast_ref::().focused; renderer.fill_quad( renderer::Quad { bounds: layout.bounds(), border: Border { color: if focus || cursor.is_over(layout.bounds()) { style::TEAL } else { style::LINE }, width: if focus { 2. } else { 1. }, radius: 5.into(), }, ..Default::default() }, Color::WHITE, ); } self.content.as_widget().draw( &tree.children[0], renderer, theme, renderer_style, layout, cursor, viewport, ); } fn mouse_interaction( &self, tree: &Tree, layout: Layout<'_>, cursor: mouse::Cursor, viewport: &Rectangle, renderer: &R, ) -> mouse::Interaction { if self.on_open.is_some() && cursor.is_over(layout.bounds()) { mouse::Interaction::Pointer } else { self.content.as_widget().mouse_interaction( &tree.children[0], layout, cursor, viewport, renderer, ) } } fn overlay<'b>( &'b mut self, tree: &'b mut Tree, layout: Layout<'b>, renderer: &R, viewport: &Rectangle, translation: Vector, ) -> Option> { self.content.as_widget_mut().overlay( &mut tree.children[0], layout, renderer, viewport, translation, ) } } /// Scrollable child updates use content coordinates. The root visits the /// retained tree before each input, so field anchors use current window /// coordinates even when a wheel event and activation share one runtime batch. pub(super) fn measure( content: &mut Element<'_, Message, Theme, R>, tree: &mut Tree, layout: Layout<'_>, renderer: &R, viewport: Rectangle, ) { content.as_widget_mut().operate( tree, layout, renderer, &mut Measure { translation: Vector::ZERO, viewport: Some(viewport), pending: None, }, ); } struct Measure { translation: Vector, viewport: Option, pending: Option<(Vector, Option)>, } impl Operation for Measure { fn traverse(&mut self, visit: &mut dyn FnMut(&mut dyn Operation)) { let previous = (self.translation, self.viewport); if let Some((translation, viewport)) = self.pending.take() { self.translation = translation; self.viewport = viewport; } visit(self); (self.translation, self.viewport) = previous; } fn scrollable( &mut self, _: Option<&Id>, bounds: Rectangle, _: Rectangle, translation: Vector, _: &mut dyn operation::Scrollable, ) { let bounds = Rectangle { x: bounds.x - self.translation.x, y: bounds.y - self.translation.y, ..bounds }; self.pending = Some(( self.translation + translation, self.viewport .and_then(|viewport| viewport.intersection(&bounds)), )); } fn custom(&mut self, _: Option<&Id>, bounds: Rectangle, state: &mut dyn std::any::Any) { if let Some(state) = state.downcast_mut::() { let bounds = Rectangle { x: bounds.x - self.translation.x, y: bounds.y - self.translation.y, ..bounds }; state.anchor = self .viewport .and_then(|viewport| viewport.intersection(&bounds)); } } } impl<'a, Message: 'a, R: renderer::Renderer + 'a> From> for Element<'a, Message, Theme, R> { fn from(field: Field<'a, Message, R>) -> Self { Element::new(field) } } pub fn focus_operation( session: ModelId, generation: ModelId, path: &str, step: i32, ) -> impl Operation + 'static { focus::exact(session, generation, path, step) } pub fn focus( session: ModelId, generation: ModelId, path: &str, step: i32, ) -> Task { iced::advanced::widget::operate(focus_operation(session, generation, path, step)).discard() }