Integrate scoped reference chooser and scenario form focus and reveal

This commit is contained in:
Codex
2026-09-11 06:24:20 -07:00
parent c7ed894900
commit 7f081ad457
27 changed files with 4544 additions and 79 deletions
@@ -5,8 +5,10 @@
use oa_core::Id;
use std::collections::BTreeSet;
pub mod adapter;
#[cfg(test)]
mod tests;
pub mod trigger;
pub mod widget;
pub const MAX_ROWS: usize = 6;
@@ -0,0 +1,307 @@
//! Live form adapter. Compatibility stays in the existing parent adapter;
//! browser metadata and scoped writes never reinterpret historical UUID text.
use super::super::{Draft, friendly, history_fields, optional_reference, reference, reference_ids};
use super::{Candidate, Context, Current, State, trigger};
use crate::{app::Message, style};
use iced::{
Element, Fill, Rectangle,
widget::{column, container, row, text, tooltip},
};
use oa_core::{Id, Project};
use serde_json::{Value, json};
use std::collections::BTreeSet;
#[derive(Debug, Clone)]
pub struct Open {
pub document: Id,
pub session: Id,
pub generation: Id,
pub record: Id,
pub path: String,
pub anchor: Rectangle,
}
#[derive(Debug, Clone)]
pub struct Set {
pub context: Context,
pub generation: Id,
pub value: Option<Id>,
}
impl Draft {
pub fn reference_session(&self) -> Id {
self.reference_session
}
pub fn reference_generation(&self) -> Id {
self.reference_generation
}
pub fn reference_open(&self, project: &Project, path: &str, anchor: Rectangle) -> Open {
Open {
document: project.id,
session: self.reference_session,
generation: self.reference_generation,
record: self.original,
path: path.into(),
anchor,
}
}
pub fn reference_matches(&self, request: &Open) -> bool {
self.reference_session == request.session
&& self.reference_generation == request.generation
&& self.original == request.record
&& self.live_reference(&request.path)
}
fn live_reference(&self, path: &str) -> bool {
!history_fields::is_path(path)
&& reference(path)
&& self
.value
.pointer(path)
.is_some_and(|v| v.is_string() || v.is_null())
}
pub fn reference_catalog(
&self,
project: &Project,
request: &Open,
revision: u64,
) -> Result<State, String> {
if request.document != project.id || !self.reference_matches(request) {
return Err("The reference field changed. Open its chooser again.".into());
}
let allowed: BTreeSet<_> = reference_ids(self, project, &request.path)
.into_iter()
.filter(|id| !id.is_nil())
.collect();
let candidates = allowed
.iter()
.filter_map(|id| candidate(project, *id))
.collect();
let raw = self
.inputs
.get(&request.path)
.map(|v| v.raw.as_str())
.or_else(|| self.value.pointer(&request.path).and_then(Value::as_str));
let current = match raw {
None => Current::Unassigned,
Some(raw) => match raw.parse::<Id>() {
Ok(id) if id.is_nil() => Current::Unassigned,
Ok(id) => match candidate(project, id) {
Some(c) if allowed.contains(&id) => Current::Valid(c),
Some(c) => Current::Incompatible(c),
None => Current::Missing(id),
},
Err(_) => Current::InvalidRaw(raw.into()),
},
};
let context = Context::live(
project.id,
revision,
self.reference_session,
self.original,
&request.path,
optional_reference(&request.path),
)
.map_err(str::to_owned)?;
State::open(
context,
candidates,
current,
format!(
"{} · compatible references",
friendly(request.path.rsplit('/').next().unwrap_or("Reference"))
),
)
.map_err(str::to_owned)
}
pub fn reference_set(&mut self, project: &Project, intent: &Set) -> Result<(), String> {
let value = self.reference_value(project, intent)?;
self.shape_scoped(&intent.context.path, value)
}
pub fn reference_value(&self, project: &Project, intent: &Set) -> Result<Value, String> {
let context = &intent.context;
if context.document != project.id
|| context.draft != self.reference_session
|| context.record != self.original
|| intent.generation != self.reference_generation
|| !self.live_reference(&context.path)
|| context.optional != optional_reference(&context.path)
{
return Err("The originating reference field is no longer current.".into());
}
let value = match intent.value {
Some(id)
if !id.is_nil() && reference_ids(self, project, &context.path).contains(&id) =>
{
json!(id)
}
None if context.optional => Value::Null,
_ => return Err("Choose a currently compatible reference.".into()),
};
Ok(value)
}
}
fn named(project: &Project, id: Id) -> String {
if let Some(e) = project.elements.get(&id) {
format!("{} · @{}", e.name, e.alias)
} else {
project.named(id).into()
}
}
fn ancestry(project: &Project, id: Id) -> String {
let mut parts = vec![];
let mut seen = BTreeSet::new();
let mut current = project.elements.get(&id).and_then(|e| e.parent);
while let Some(id) = current {
if !seen.insert(id) {
parts.push("[containment cycle]".into());
break;
}
parts.push(named(project, id));
current = project.elements.get(&id).and_then(|e| e.parent);
}
parts.reverse();
if parts.is_empty() {
project.name.clone()
} else {
parts.join(" / ")
}
}
pub(crate) fn candidate(project: &Project, id: Id) -> Option<Candidate> {
let layer = |layer| format!("{layer:?}").to_uppercase();
if id == project.id {
return Some(Candidate {
id,
name: project.name.clone(),
alias: project.alias.clone(),
kind: "Project".into(),
scope: "Model".into(),
containment: "Model root".into(),
});
}
if let Some(e) = project.elements.get(&id) {
return Some(Candidate {
id,
name: e.name.clone(),
alias: e.alias.clone(),
kind: e.kind().to_string(),
scope: layer(e.layer()),
containment: ancestry(project, id),
});
}
if let Some(r) = project.relations.get(&id) {
return Some(Candidate {
id,
name: r.name.clone(),
alias: r.alias.clone(),
kind: r.kind().to_string(),
scope: project.layer_of(id).map_or_else(|| "Model".into(), layer),
containment: format!(
"{} → {}",
named(project, r.source()),
named(project, r.target())
),
});
}
for diagram in project.diagrams.values() {
if id == diagram.id {
return Some(Candidate {
id,
name: diagram.name.clone(),
alias: diagram.alias.clone(),
kind: format!("{:?} diagram", diagram.projection),
scope: layer(diagram.layer),
containment: project.name.clone(),
});
}
if let Some(node) = diagram.nodes.get(&id) {
return Some(Candidate {
id,
name: project.named(node.element).into(),
alias: node.alias.clone(),
kind: "Placement".into(),
scope: layer(diagram.layer),
containment: format!(
"{} · @{} / {}",
diagram.name,
diagram.alias,
named(project, node.element)
),
});
}
}
if let Some(link) = project.artifact_links.get(&id) {
return Some(Candidate {
id,
name: link.name.clone(),
alias: link.alias.clone(),
kind: "Artifact link".into(),
scope: project
.layer_of(link.owner)
.map_or_else(|| "Model".into(), layer),
containment: named(project, link.owner),
});
}
project.traces.get(&id).map(|trace| Candidate {
id,
name: trace.name.clone(),
alias: trace.alias.clone(),
kind: "Trace".into(),
scope: "OA ↔ SA".into(),
containment: format!(
"{} ↔ {}",
named(project, trace.oa),
named(project, trace.sa)
),
})
}
pub fn field<'a, R: iced::advanced::text::Renderer<Font = iced::Font> + 'a>(
draft: &'a Draft,
project: &'a Project,
path: &str,
) -> Element<'a, Message, iced::Theme, R> {
let raw = draft
.inputs
.get(path)
.map(|v| v.raw.as_str())
.or_else(|| draft.value.pointer(path).and_then(Value::as_str));
let (label, detail) = match raw {
None => (
if optional_reference(path) {
"Not assigned"
} else {
"Choose a reference…"
}
.into(),
String::new(),
),
Some(raw) => match raw.parse::<Id>() {
Ok(id) if id.is_nil() => ("Choose a reference…".into(), String::new()),
Ok(id) => match candidate(project, id) {
Some(c) => (format!("{} · @{}", c.name, c.alias), c.detail()),
None => ("Missing reference".into(), raw.into()),
},
Err(_) => ("Invalid reference retained".into(), raw.into()),
},
};
let label = crate::views::short(&label, 35);
let body = container(row![text(label).size(12).width(Fill), text("⌄").size(12)].spacing(6))
.padding(8)
.width(Fill);
let path = path.to_owned();
let id = trigger::field_id(draft.reference_generation, &path);
let field = trigger::Field::new(body, id, move |anchor| {
Message::FieldReferenceOpen(draft.reference_open(project, &path, anchor))
});
if detail.is_empty() {
field.into()
} else {
tooltip(
field,
container(column![text(detail).size(11)])
.padding(8)
.style(style::card),
tooltip::Position::Left,
)
.into()
}
}
@@ -0,0 +1,447 @@
//! 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::new(format!("reference:{generation}:{path}"))
}
pub fn scope_id(session: ModelId) -> Id {
Id::new(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 self.on_open.is_some() {
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(self.on_open.as_ref().unwrap()(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>(
session: ModelId,
generation: ModelId,
path: &str,
step: i32,
) -> Task<Message> {
iced::advanced::widget::operate(focus_operation(session, generation, path, step)).discard()
}
@@ -0,0 +1,303 @@
//! Focus movement and reveal share one root traversal. A form may be inside a
//! scrollable; restricting traversal to its container would hide that ancestor.
use super::{FormOwner, Id, ModelId, Operation, Rectangle, Vector, field_id, operation, scope_id};
use std::collections::BTreeMap;
enum Origin {
Current,
Exact(Id),
}
#[derive(Clone)]
struct Scroll {
index: usize,
bounds: Rectangle,
capacity: Vector,
offset: Vector,
}
struct Field {
id: Option<Id>,
bounds: Rectangle,
ancestors: Vec<Scroll>,
}
pub(super) struct Collect {
scope: Id,
origin: Origin,
step: i32,
inside: bool,
entering: Option<bool>,
translation: Vector,
scrolls: Vec<Scroll>,
pending_scroll: Option<Scroll>,
scroll_index: usize,
fields: Vec<Field>,
focused: Option<usize>,
outside_focused: bool,
modal: bool,
owner_valid: bool,
}
impl Collect {
fn new(scope: Id, origin: Origin, step: i32) -> Self {
let owner_valid = matches!(origin, Origin::Current);
Self {
scope,
origin,
step,
inside: false,
entering: None,
translation: Vector::ZERO,
scrolls: vec![],
pending_scroll: None,
scroll_index: 0,
fields: vec![],
focused: None,
outside_focused: false,
modal: false,
owner_valid,
}
}
}
pub(super) fn relative(session: ModelId, backwards: bool, modal: bool) -> Collect {
Collect {
modal,
..Collect::new(
scope_id(session),
Origin::Current,
if backwards { -1 } else { 1 },
)
}
}
pub(super) fn exact(session: ModelId, generation: ModelId, path: &str, step: i32) -> Collect {
Collect::new(
scope_id(session),
Origin::Exact(field_id(generation, path)),
step,
)
}
impl Operation for Collect {
fn custom(&mut self, _: Option<&Id>, _: Rectangle, state: &mut dyn std::any::Any) {
if let Some(owner) = state.downcast_ref::<FormOwner>() {
self.owner_valid = owner
.0
.is_some_and(|owner| scope_id(owner.session) == self.scope);
self.modal = owner.0.is_some_and(|owner| owner.modal);
}
}
fn container(&mut self, id: Option<&Id>, _: Rectangle) {
self.entering = Some(self.inside || id == Some(&self.scope));
}
fn traverse(&mut self, visit: &mut dyn FnMut(&mut dyn Operation)) {
let previous = (self.inside, self.translation, self.scrolls.len());
if let Some(inside) = self.entering.take() {
self.inside = inside;
}
if let Some(scroll) = self.pending_scroll.take() {
self.translation += scroll.offset;
self.scrolls.push(scroll);
}
visit(self);
self.inside = previous.0;
self.translation = previous.1;
self.scrolls.truncate(previous.2);
}
fn scrollable(
&mut self,
_: Option<&Id>,
bounds: Rectangle,
content: Rectangle,
offset: Vector,
_: &mut dyn operation::Scrollable,
) {
self.pending_scroll = Some(Scroll {
index: self.scroll_index,
bounds: translated(bounds, self.translation),
capacity: Vector::new(
(content.width - bounds.width).max(0.),
(content.height - bounds.height).max(0.),
),
offset,
});
self.scroll_index += 1;
}
fn focusable(
&mut self,
id: Option<&Id>,
bounds: Rectangle,
state: &mut dyn operation::Focusable,
) {
if !self.inside {
self.outside_focused |= state.is_focused();
return;
}
if state.is_focused() {
self.focused = Some(self.fields.len());
}
self.fields.push(Field {
id: id.cloned(),
bounds: translated(bounds, self.translation),
ancestors: self.scrolls.clone(),
});
}
fn finish(&self) -> operation::Outcome<()> {
if !self.owner_valid || self.fields.is_empty() {
return operation::Outcome::None;
}
let origin = match &self.origin {
Origin::Current if self.focused.is_none() && self.outside_focused && !self.modal => {
return operation::Outcome::None;
}
Origin::Current => self
.focused
.map(|index| index as i64)
.unwrap_or(if self.step < 0 {
self.fields.len() as i64
} else {
-1
}),
Origin::Exact(id) => {
let Some(index) = self
.fields
.iter()
.position(|field| field.id.as_ref() == Some(id))
else {
return operation::Outcome::None;
};
index as i64
}
};
// A form keeps a reachable focus at both boundaries. Initial Tab enters
// the first field; initial Shift+Tab enters the last field.
let target = (origin + i64::from(self.step)).rem_euclid(self.fields.len() as i64) as usize;
let field = &self.fields[target];
let mut bounds = field.bounds;
let mut shifts = BTreeMap::new();
for ancestor in field.ancestors.iter().rev() {
let dx = reveal_axis(
bounds.x,
bounds.width,
ancestor.bounds.x,
ancestor.bounds.width,
);
let dy = reveal_axis(
bounds.y,
bounds.height,
ancestor.bounds.y,
ancestor.bounds.height,
);
let delta = Vector::new(
(ancestor.offset.x + dx).clamp(0., ancestor.capacity.x) - ancestor.offset.x,
(ancestor.offset.y + dy).clamp(0., ancestor.capacity.y) - ancestor.offset.y,
);
if delta != Vector::ZERO {
shifts.insert(ancestor.index, delta);
}
bounds = translated(bounds, delta);
}
operation::Outcome::Chain(Box::new(Apply {
scope: self.scope.clone(),
inside: false,
entering: None,
target,
field_index: 0,
scroll_index: 0,
shifts,
owner_valid: matches!(self.origin, Origin::Current),
clear_outside: self.modal,
preserve_inside: false,
}))
}
}
fn translated(bounds: Rectangle, delta: Vector) -> Rectangle {
Rectangle {
x: bounds.x - delta.x,
y: bounds.y - delta.y,
..bounds
}
}
fn reveal_axis(start: f32, length: f32, viewport_start: f32, viewport_length: f32) -> f32 {
if start < viewport_start || length > viewport_length {
start - viewport_start
} else {
(start + length - viewport_start - viewport_length).max(0.)
}
}
pub(super) struct Apply {
scope: Id,
inside: bool,
entering: Option<bool>,
target: usize,
field_index: usize,
scroll_index: usize,
shifts: BTreeMap<usize, Vector>,
owner_valid: bool,
clear_outside: bool,
preserve_inside: bool,
}
pub(super) fn exclusive() -> Apply {
Apply {
scope: Id::new(super::REVIEW_MODAL),
inside: false,
entering: None,
target: 0,
field_index: 0,
scroll_index: 0,
shifts: BTreeMap::new(),
owner_valid: true,
clear_outside: true,
preserve_inside: true,
}
}
impl Operation for Apply {
fn custom(&mut self, _: Option<&Id>, _: Rectangle, state: &mut dyn std::any::Any) {
if let Some(owner) = state.downcast_ref::<FormOwner>() {
self.owner_valid = owner
.0
.is_some_and(|owner| scope_id(owner.session) == self.scope);
self.clear_outside = owner.0.is_some_and(|owner| owner.modal);
}
}
fn container(&mut self, id: Option<&Id>, _: Rectangle) {
self.entering = Some(self.inside || id == Some(&self.scope));
}
fn traverse(&mut self, visit: &mut dyn FnMut(&mut dyn Operation)) {
let previous = self.inside;
if let Some(inside) = self.entering.take() {
self.inside = inside;
}
visit(self);
self.inside = previous;
}
fn scrollable(
&mut self,
_: Option<&Id>,
bounds: Rectangle,
content: Rectangle,
_: Vector,
state: &mut dyn operation::Scrollable,
) {
if self.owner_valid
&& let Some(delta) = self.shifts.get(&self.scroll_index)
{
state.scroll_by(
operation::scrollable::AbsoluteOffset {
x: delta.x,
y: delta.y,
},
bounds,
content,
);
}
self.scroll_index += 1;
}
fn focusable(&mut self, _: Option<&Id>, _: Rectangle, state: &mut dyn operation::Focusable) {
if self.owner_valid && self.inside && !self.preserve_inside {
if self.field_index == self.target {
state.focus();
} else {
state.unfocus();
}
self.field_index += 1;
} else if self.owner_valid && !self.inside && self.clear_outside {
state.unfocus();
}
}
}
@@ -163,6 +163,7 @@ where
chooser: Option<&'a Chooser>,
anchor: Rectangle,
on_event: Rc<dyn Fn(ChoiceEvent) -> Message + 'a>,
focus_scope: Option<super::trigger::FormScope>,
}
impl<'a, Message: Clone + 'a, R: text::Renderer<Font = Font> + 'a> Popup<'a, Message, R> {
pub fn new(
@@ -187,8 +188,13 @@ impl<'a, Message: Clone + 'a, R: text::Renderer<Font = Font> + 'a> Popup<'a, Mes
chooser,
anchor,
on_event: Rc::new(on_event),
focus_scope: None,
}
}
pub fn focus_scope(mut self, scope: Option<super::trigger::FormScope>) -> Self {
self.focus_scope = scope;
self
}
fn reconcile(&self, tree: &mut Tree) {
let state = tree.state.downcast_mut::<Interaction>();
let stamp = self.chooser.map(Chooser::stamp);
@@ -250,6 +256,9 @@ impl<Message: Clone, R: text::Renderer<Font = Font>> Widget<Message, Theme, R>
renderer: &R,
operation: &mut dyn Operation,
) {
let mut owner =
super::trigger::FormOwner(self.focus_scope.filter(|_| self.chooser.is_none()));
operation.custom(None, layout.bounds(), &mut owner);
self.content
.as_widget_mut()
.operate(&mut tree.children[0], layout, renderer, operation);
@@ -270,6 +279,35 @@ impl<Message: Clone, R: text::Renderer<Font = Font>> Widget<Message, Theme, R>
shell.capture_event();
return;
}
if is_input(event) && self.focus_scope.is_some_and(|scope| scope.modal) {
super::trigger::own_input(&mut self.content, &mut tree.children[0], layout, renderer);
}
if let Event::Keyboard(keyboard::Event::KeyPressed {
key: keyboard::Key::Named(keyboard::key::Named::Tab),
modifiers,
..
}) = event
&& let Some(scope) = self.focus_scope
&& super::trigger::tab(
&mut self.content,
&mut tree.children[0],
layout,
renderer,
scope,
modifiers.shift(),
)
{
shell.capture_event();
shell.request_redraw();
return;
}
super::trigger::measure(
&mut self.content,
&mut tree.children[0],
layout,
renderer,
*viewport,
);
self.content.as_widget_mut().update(
&mut tree.children[0],
event,