448 lines
13 KiB
Rust
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()
|
|
}
|