Integrate scoped reference chooser and scenario form focus and reveal
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
//! Scenario presentation. Layout/camera controls never construct model commands.
|
||||
use super::super::scenario_scroll;
|
||||
use super::*;
|
||||
use iced::widget::{Space, column, sensor};
|
||||
use iced::widget::{Space, column, keyed_column, sensor};
|
||||
|
||||
pub(crate) const NAME_INPUT: &str = "scenario-form-name";
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Shelf {
|
||||
@@ -82,10 +84,21 @@ pub fn view<'a>(app: &'a App, owner: Id, out: Column<'a, Message>) -> Element<'a
|
||||
// The outline is a sibling shelf, not an ancestor of this canvas.
|
||||
scene.scroll_offset = state.scroll_offset();
|
||||
scene.pending_placement = state.pending_placement(owner);
|
||||
let reveal = state.form_session().and_then(|token| {
|
||||
let (slot, _) = scene.pending_placement?;
|
||||
let marker = scene.slots.iter().find(|marker| marker.slot == slot)?;
|
||||
Some(ScrollReveal {
|
||||
token,
|
||||
slot,
|
||||
y: marker.y * scale,
|
||||
padding: (24. * scale).max(12.),
|
||||
})
|
||||
});
|
||||
let size = Size::new(scene.size.width * scale, scene.size.height * scale);
|
||||
let canvas = canvas_pane(
|
||||
context,
|
||||
state.scroll_restore(context, ScrollArea::Canvas),
|
||||
reveal,
|
||||
canvas::Canvas::new(scene)
|
||||
.width(size.width)
|
||||
.height(size.height)
|
||||
@@ -287,7 +300,11 @@ pub fn view<'a>(app: &'a App, owner: Id, out: Column<'a, Message>) -> Element<'a
|
||||
presentation.shelf.is_some(),
|
||||
pending,
|
||||
);
|
||||
let mut form_panel = editor_panel(Space::new().height(0).into(), Space::new().height(0).into());
|
||||
let mut form_panel = editor_panel(
|
||||
Id::nil(),
|
||||
Space::new().height(0).into(),
|
||||
Space::new().height(0).into(),
|
||||
);
|
||||
if let Some(d) = app
|
||||
.behavior
|
||||
.scenario
|
||||
@@ -298,6 +315,7 @@ pub fn view<'a>(app: &'a App, owner: Id, out: Column<'a, Message>) -> Element<'a
|
||||
let mut form = column![
|
||||
text(d.tool.label()).size(16),
|
||||
text_input("Name (defaults to selected source)", &d.name)
|
||||
.id(NAME_INPUT)
|
||||
.on_input(|s| event(Event::Name(s)))
|
||||
.padding(9)
|
||||
.size(12)
|
||||
@@ -560,7 +578,11 @@ pub fn view<'a>(app: &'a App, owner: Id, out: Column<'a, Message>) -> Element<'a
|
||||
action("Cancel", event(Event::Cancel))
|
||||
]
|
||||
.spacing(7);
|
||||
form_panel = editor_panel(footer.into(), form.into());
|
||||
let session = state.form_session().expect("visible scenario draft");
|
||||
form_panel = sensor(editor_panel(session, footer.into(), form.into()))
|
||||
.key(session)
|
||||
.on_show(move |_| Message::ScenarioCanvas(CanvasEvent::FormShown(context, session)))
|
||||
.into();
|
||||
}
|
||||
workspace_layout(header.into(), details, canvas, form_panel, pending)
|
||||
}
|
||||
@@ -633,13 +655,14 @@ fn details_pane<'a, R: iced::advanced::text::Renderer + 'a>(
|
||||
fn canvas_pane<'a, R: iced::advanced::text::Renderer + 'a>(
|
||||
context: Context,
|
||||
restore: Option<ScrollRestore>,
|
||||
reveal: Option<ScrollReveal>,
|
||||
content: iced::Element<'a, Message, Theme, R>,
|
||||
) -> iced::Element<'a, Message, Theme, R> {
|
||||
container(
|
||||
sensor(scenario_scroll::observe(
|
||||
sensor(scenario_scroll::observe_canvas(
|
||||
context,
|
||||
ScrollArea::Canvas,
|
||||
restore,
|
||||
reveal,
|
||||
scrollable(content)
|
||||
.id("scenario-canvas")
|
||||
.direction(scrollable::Direction::Both {
|
||||
@@ -661,16 +684,23 @@ fn canvas_pane<'a, R: iced::advanced::text::Renderer + 'a>(
|
||||
}
|
||||
|
||||
fn editor_panel<'a, R: iced::advanced::text::Renderer + 'a>(
|
||||
session: Id,
|
||||
actions: iced::Element<'a, Message, Theme, R>,
|
||||
fields: iced::Element<'a, Message, Theme, R>,
|
||||
) -> iced::Element<'a, Message, Theme, R> {
|
||||
container(
|
||||
column![
|
||||
container(actions).id("scenario-actions").width(Fill),
|
||||
scrollable(fields)
|
||||
.id("scenario-form")
|
||||
.width(Fill)
|
||||
.height(Fill),
|
||||
keyed_column([(
|
||||
session,
|
||||
scrollable(fields)
|
||||
.id("scenario-form")
|
||||
.width(Fill)
|
||||
.height(Fill)
|
||||
.into(),
|
||||
)])
|
||||
.width(Fill)
|
||||
.height(Fill),
|
||||
]
|
||||
.spacing(6),
|
||||
)
|
||||
|
||||
@@ -49,15 +49,278 @@ fn fixture_view(state: &State, context: Context, pending: bool, header: f32) ->
|
||||
canvas_pane(
|
||||
context,
|
||||
state.scroll_restore(context, ScrollArea::Canvas),
|
||||
None,
|
||||
Space::new().width(1800.).height(2400.).into(),
|
||||
),
|
||||
editor_panel(
|
||||
state.form_session().unwrap_or_default(),
|
||||
Space::new().height(32.).into(),
|
||||
Space::new().height(2000.).into(),
|
||||
),
|
||||
pending,
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_scenario_form_resets_only_its_scroll_and_recovery_gets_a_fresh_session() {
|
||||
let f = super::super::direct_tests::fixture();
|
||||
let mut app = App::with_project(f.project);
|
||||
let _ = app.update(Message::Select(f.owner));
|
||||
let _ = app.update(Message::Page(crate::app::Page::Behavior));
|
||||
let _ = app.update(event(Event::Start(f.owner, Tool::Action)));
|
||||
let first = app.behavior.scenario.form_session().unwrap();
|
||||
let size = Size::new(500., 570.);
|
||||
let mut view = fixture_view(
|
||||
&app.behavior.scenario,
|
||||
app.scenario_context().unwrap(),
|
||||
true,
|
||||
132.,
|
||||
);
|
||||
let mut tree = widget::Tree::new(view.as_widget());
|
||||
let (node, _) = geometry(&mut view, &mut tree, size);
|
||||
scroll(
|
||||
&mut view,
|
||||
&mut tree,
|
||||
&node,
|
||||
"scenario-form",
|
||||
Vector::new(0., 700.),
|
||||
);
|
||||
scroll(
|
||||
&mut view,
|
||||
&mut tree,
|
||||
&node,
|
||||
"scenario-canvas",
|
||||
Vector::new(137., 850.),
|
||||
);
|
||||
redraw(&mut view, &mut tree, size, &mut app);
|
||||
let accepted = app.document.snapshot();
|
||||
let _ = app.update(event(Event::Name(" retained raw 雨 ".into())));
|
||||
let raw = app.behavior.scenario.recovery_snapshot().unwrap();
|
||||
view = fixture_view(
|
||||
&app.behavior.scenario,
|
||||
app.scenario_context().unwrap(),
|
||||
true,
|
||||
132.,
|
||||
);
|
||||
tree.diff(view.as_widget());
|
||||
let (_, same_form) = geometry(&mut view, &mut tree, size);
|
||||
assert_eq!(same_form.scrolls["scenario-form"].1.y, 700.);
|
||||
assert_eq!(app.behavior.scenario.form_session(), Some(first));
|
||||
|
||||
// No intervening closed render: replacing the editing session itself must
|
||||
// invalidate the old scroll, while preserving the canvas's actual offset.
|
||||
let _ = app.update(event(Event::Cancel));
|
||||
let _ = app.update(event(Event::Start(f.owner, Tool::Observation)));
|
||||
assert_ne!(app.behavior.scenario.form_session(), Some(first));
|
||||
view = fixture_view(
|
||||
&app.behavior.scenario,
|
||||
app.scenario_context().unwrap(),
|
||||
true,
|
||||
132.,
|
||||
);
|
||||
tree.diff(view.as_widget());
|
||||
let (_, next_form) = geometry(&mut view, &mut tree, size);
|
||||
assert_eq!(next_form.scrolls["scenario-form"].1, Vector::ZERO);
|
||||
assert_eq!(
|
||||
next_form.scrolls["scenario-canvas"].1,
|
||||
Vector::new(137., 850.)
|
||||
);
|
||||
let recovered = State::from_recovery(raw.unwrap()).unwrap();
|
||||
assert_ne!(recovered.form_session(), Some(first));
|
||||
assert_eq!(
|
||||
recovered.draft.as_ref().unwrap().name,
|
||||
" retained raw 雨 "
|
||||
);
|
||||
assert!(std::sync::Arc::ptr_eq(&accepted, &app.document.snapshot()));
|
||||
assert!(!app.document.can_undo());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pending_marker_reveals_in_the_actual_smaller_canvas_once_without_horizontal_or_history_drift() {
|
||||
let f = super::super::direct_tests::fixture();
|
||||
let mut app = App::with_project(f.project);
|
||||
let _ = app.update(Message::Select(f.owner));
|
||||
let _ = app.update(Message::Page(crate::app::Page::Behavior));
|
||||
let context = app.scenario_context().unwrap();
|
||||
let make = |reveal| -> TestView {
|
||||
canvas_pane(
|
||||
context,
|
||||
None,
|
||||
reveal,
|
||||
Space::new().width(1800.).height(2400.).into(),
|
||||
)
|
||||
};
|
||||
let mut view = make(None);
|
||||
let mut tree = widget::Tree::new(view.as_widget());
|
||||
let wide = Size::new(500., 500.);
|
||||
let (node, _) = geometry(&mut view, &mut tree, wide);
|
||||
scroll(
|
||||
&mut view,
|
||||
&mut tree,
|
||||
&node,
|
||||
"scenario-canvas",
|
||||
Vector::new(137., 800.),
|
||||
);
|
||||
redraw(&mut view, &mut tree, wide, &mut app);
|
||||
let accepted = app.document.snapshot();
|
||||
let request = ScrollReveal {
|
||||
token: Id::new_v4(),
|
||||
slot: Slot {
|
||||
parent: f.owner,
|
||||
before: Some(f.reply),
|
||||
},
|
||||
y: 1100.,
|
||||
padding: 24.,
|
||||
};
|
||||
view = make(Some(request));
|
||||
tree.diff(view.as_widget());
|
||||
let small = Size::new(500., 200.);
|
||||
redraw(&mut view, &mut tree, small, &mut app);
|
||||
let (node, shown) = geometry(&mut view, &mut tree, small);
|
||||
let (bounds, offset) = shown.scrolls["scenario-canvas"];
|
||||
assert_eq!(offset.x, 137.);
|
||||
assert_eq!(offset.y, 1100. + 24. - bounds.height);
|
||||
assert!(1100. - 24. >= offset.y);
|
||||
assert!(1100. + 24. <= offset.y + bounds.height);
|
||||
scroll(
|
||||
&mut view,
|
||||
&mut tree,
|
||||
&node,
|
||||
"scenario-canvas",
|
||||
Vector::new(137., 960.),
|
||||
);
|
||||
redraw(&mut view, &mut tree, small, &mut app);
|
||||
view = make(Some(request));
|
||||
tree.diff(view.as_widget());
|
||||
redraw(&mut view, &mut tree, small, &mut app);
|
||||
assert_eq!(
|
||||
geometry(&mut view, &mut tree, small).1.scrolls["scenario-canvas"].1,
|
||||
Vector::new(137., 960.)
|
||||
);
|
||||
|
||||
view = make(Some(ScrollReveal {
|
||||
token: Id::new_v4(),
|
||||
y: 780.,
|
||||
..request
|
||||
}));
|
||||
tree.diff(view.as_widget());
|
||||
redraw(&mut view, &mut tree, small, &mut app);
|
||||
assert_eq!(
|
||||
geometry(&mut view, &mut tree, small).1.scrolls["scenario-canvas"].1,
|
||||
Vector::new(137., 756.)
|
||||
);
|
||||
assert!(std::sync::Arc::ptr_eq(&accepted, &app.document.snapshot()));
|
||||
assert!(!app.document.can_undo());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn scenario_form_focus_targets_the_name_and_rejects_stale_sessions_or_modals() {
|
||||
use iced::futures::StreamExt;
|
||||
let f = super::super::direct_tests::fixture();
|
||||
let mut app = App::with_project(f.project);
|
||||
let _ = app.update(Message::Select(f.owner));
|
||||
let _ = app.update(Message::Page(crate::app::Page::Behavior));
|
||||
let _ = app.update(event(Event::Start(f.owner, Tool::Fragment)));
|
||||
let _ = app.update(event(Event::Name(" retained 雨 ".into())));
|
||||
let context = app.scenario_context().unwrap();
|
||||
let session = app.behavior.scenario.form_session().unwrap();
|
||||
let raw = app.behavior.recovery_snapshot().unwrap();
|
||||
let accepted = app.document.snapshot();
|
||||
for mismatch in 0..4 {
|
||||
let mut stale = context;
|
||||
let mut token = session;
|
||||
match mismatch {
|
||||
0 => stale.document = Id::new_v4(),
|
||||
1 => stale.scenario = Id::new_v4(),
|
||||
2 => stale.revision += 1,
|
||||
_ => token = Id::new_v4(),
|
||||
}
|
||||
let task = app.update(Message::ScenarioCanvas(CanvasEvent::FormShown(
|
||||
stale, token,
|
||||
)));
|
||||
assert!(iced_runtime::task::into_stream(task).is_none());
|
||||
}
|
||||
app.hosted.visible = true;
|
||||
let task = app.update(Message::ScenarioCanvas(CanvasEvent::FormShown(
|
||||
context, session,
|
||||
)));
|
||||
assert!(iced_runtime::task::into_stream(task).is_none());
|
||||
app.hosted.visible = false;
|
||||
app.busy = true;
|
||||
let task = app.update(Message::ScenarioCanvas(CanvasEvent::FormShown(
|
||||
context, session,
|
||||
)));
|
||||
assert!(iced_runtime::task::into_stream(task).is_none());
|
||||
app.busy = false;
|
||||
|
||||
// The viewport report from the same mount may advance its geometry epoch.
|
||||
// That does not change the identity of the field awaiting its first focus.
|
||||
let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Viewport(
|
||||
context,
|
||||
Size::new(480., 180.),
|
||||
)));
|
||||
assert_ne!(app.scenario_context(), Some(context));
|
||||
let task = app.update(Message::ScenarioCanvas(CanvasEvent::FormShown(
|
||||
context, session,
|
||||
)));
|
||||
let mut fields: iced::Element<'_, (), Theme, ()> = column![
|
||||
text_input("Scenario name", " retained 雨 ")
|
||||
.id(NAME_INPUT)
|
||||
.on_input(|_| ()),
|
||||
text_input("Other field", "other")
|
||||
.id("another-focus-target")
|
||||
.on_input(|_| ()),
|
||||
]
|
||||
.into();
|
||||
let mut tree = widget::Tree::new(fields.as_widget());
|
||||
let node = fields.as_widget_mut().layout(
|
||||
&mut tree,
|
||||
&(),
|
||||
&layout::Limits::new(Size::ZERO, Size::new(320., 100.)),
|
||||
);
|
||||
tree.children[1]
|
||||
.state
|
||||
.downcast_mut::<iced::widget::text_input::State<()>>()
|
||||
.focus();
|
||||
let mut stream = iced_runtime::task::into_stream(task).expect("valid form receives focus");
|
||||
let mut count = 0;
|
||||
while let Some(action) = stream.next().await {
|
||||
let iced_runtime::Action::Widget(mut operation) = action else {
|
||||
panic!("Form focus must not apply, lint or load a model");
|
||||
};
|
||||
fields
|
||||
.as_widget_mut()
|
||||
.operate(&mut tree, Layout::new(&node), &(), operation.as_mut());
|
||||
count += 1;
|
||||
}
|
||||
assert_eq!(count, 1);
|
||||
let repeated = app.update(Message::ScenarioCanvas(CanvasEvent::FormShown(
|
||||
context, session,
|
||||
)));
|
||||
assert!(iced_runtime::task::into_stream(repeated).is_none());
|
||||
assert!(
|
||||
tree.children[0]
|
||||
.state
|
||||
.downcast_ref::<iced::widget::text_input::State<()>>()
|
||||
.is_focused()
|
||||
);
|
||||
assert!(
|
||||
!tree.children[1]
|
||||
.state
|
||||
.downcast_ref::<iced::widget::text_input::State<()>>()
|
||||
.is_focused()
|
||||
);
|
||||
assert_eq!(app.behavior.recovery_snapshot().unwrap(), raw);
|
||||
assert!(std::sync::Arc::ptr_eq(&accepted, &app.document.snapshot()));
|
||||
assert!(!app.document.can_undo());
|
||||
let _ = app.update(event(Event::Cancel));
|
||||
let _ = app.update(event(Event::Start(f.owner, Tool::Fragment)));
|
||||
let task = app.update(Message::ScenarioCanvas(CanvasEvent::FormShown(
|
||||
app.scenario_context().unwrap(),
|
||||
session,
|
||||
)));
|
||||
assert!(iced_runtime::task::into_stream(task).is_none());
|
||||
}
|
||||
fn geometry(view: &mut TestView, tree: &mut widget::Tree, size: Size) -> (layout::Node, Geometry) {
|
||||
let node = view
|
||||
.as_widget_mut()
|
||||
|
||||
Reference in New Issue
Block a user