Files
ARCADIA-Studio/crates/oa-desktop/src/forms/reference_chooser/trigger.rs
T

448 lines
13 KiB
Rust

//! 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<FormScope>);
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>>,
) -> 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<Id>,
anchor: Option<Rectangle>,
}
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<Id>,
on_open: Option<Box<dyn Fn(Rectangle) -> Message + 'a>>,
}
impl<'a, Message, R: renderer::Renderer> Field<'a, Message, R> {
pub fn new(
content: impl Into<Element<'a, Message, Theme, R>>,
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<Element<'a, Message, Theme, R>>,
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<Message, R: renderer::Renderer>(
content: &mut Element<'_, Message, Theme, R>,
tree: &mut Tree,
layout: Layout<'_>,
renderer: &R,
mut operation: Box<dyn Operation>,
) -> 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<Message, R: renderer::Renderer>(
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<Message, R: renderer::Renderer>(
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<Message, R: renderer::Renderer> Widget<Message, Theme, R> for Field<'_, Message, R> {
fn tag(&self) -> tree::Tag {
tree::Tag::of::<Interaction>()
}
fn state(&self) -> tree::State {
tree::State::new(Interaction {
identity: self.id.clone(),
..Default::default()
})
}
fn children(&self) -> Vec<Tree> {
vec![Tree::new(self.content.as_widget())]
}
fn diff(&self, tree: &mut Tree) {
let state = tree.state.downcast_mut::<Interaction>();
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<Length> {
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::<Interaction>(),
);
operation.custom(
Some(id),
layout.bounds(),
tree.state.downcast_mut::<Interaction>(),
);
}
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::<Interaction>();
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::<Interaction>().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<overlay::Element<'b, Message, Theme, R>> {
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<Message, R: renderer::Renderer>(
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<Rectangle>,
pending: Option<(Vector, Option<Rectangle>)>,
}
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::<Interaction>() {
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<Field<'a, Message, R>>
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<Message: Send + 'static>(
session: ModelId,
generation: ModelId,
path: &str,
step: i32,
) -> Task<Message> {
iced::advanced::widget::operate(focus_operation(session, generation, path, step)).discard()
}