Integrate persistent canvas controls with scenario lifetime rendering
This commit is contained in:
+265
-230
@@ -14,9 +14,11 @@ use iced::{
|
||||
};
|
||||
use oa_core::*;
|
||||
use std::collections::BTreeMap;
|
||||
pub(crate) mod canvas_workspace;
|
||||
#[cfg(test)]
|
||||
mod contextual_source_tests;
|
||||
pub(crate) mod creation;
|
||||
mod form_session;
|
||||
pub(crate) mod scenario;
|
||||
mod scenario_lifecycle;
|
||||
pub(crate) mod source_draft;
|
||||
@@ -37,6 +39,7 @@ mod scenario_scroll {
|
||||
pub(super) fn observe<'a, R: renderer::Renderer + 'a>(
|
||||
context: scenario::Context,
|
||||
area: scenario::ScrollArea,
|
||||
process: bool,
|
||||
restore: Option<scenario::ScrollRestore>,
|
||||
content: impl Into<Element<'a, Message, Theme, R>>,
|
||||
) -> Element<'a, Message, Theme, R> {
|
||||
@@ -44,28 +47,10 @@ mod scenario_scroll {
|
||||
content: content.into(),
|
||||
context,
|
||||
area,
|
||||
process: false,
|
||||
process,
|
||||
restore,
|
||||
})
|
||||
}
|
||||
pub(super) fn observe_topology<'a>(
|
||||
context: topology::Context,
|
||||
area: scenario::ScrollArea,
|
||||
content: impl Into<Element<'a, Message>>,
|
||||
) -> Element<'a, Message> {
|
||||
Element::new(Observer {
|
||||
content: content.into(),
|
||||
context: scenario::Context {
|
||||
document: context.document,
|
||||
scenario: context.owner,
|
||||
revision: context.revision,
|
||||
epoch: context.epoch,
|
||||
},
|
||||
area,
|
||||
process: true,
|
||||
restore: None,
|
||||
})
|
||||
}
|
||||
struct Observer<'a, R = Renderer> {
|
||||
content: Element<'a, Message, Theme, R>,
|
||||
context: scenario::Context,
|
||||
@@ -336,7 +321,7 @@ mod scenario_scroll {
|
||||
fn observer(height: f32, context: scenario::Context) -> Observer<'static, ()> {
|
||||
Observer {
|
||||
context,
|
||||
area: scenario::ScrollArea::Canvas,
|
||||
area: scenario::ScrollArea::Page,
|
||||
process: false,
|
||||
restore: None,
|
||||
content: scrollable(iced::widget::Space::new().width(200.).height(height))
|
||||
@@ -406,97 +391,96 @@ mod scenario_scroll {
|
||||
std::time::Instant::now(),
|
||||
))
|
||||
};
|
||||
for area in [ScrollArea::Canvas, ScrollArea::Page] {
|
||||
for returned_height in [600., 250.] {
|
||||
let (project, [a, _, _], [use_id, _, _]) = navigation_fixture();
|
||||
let mut app = App::with_project(project.clone());
|
||||
let _ = app.update(Message::Select(a));
|
||||
let _ = app.update(Message::Page(crate::app::Page::Behavior));
|
||||
let source = app.document.bundle().clone();
|
||||
let mut view = observer(600., app.scenario_context().unwrap());
|
||||
view.area = area;
|
||||
let mut tree = widget::Tree::new(&view as &dyn Widget<Message, Theme, ()>);
|
||||
for message in update(&mut view, &mut tree, redraw()).0 {
|
||||
let _ = app.update(message);
|
||||
}
|
||||
for message in update(
|
||||
&mut view,
|
||||
&mut tree,
|
||||
iced::Event::Mouse(mouse::Event::WheelScrolled {
|
||||
delta: mouse::ScrollDelta::Pixels { x: 0., y: -180. },
|
||||
}),
|
||||
)
|
||||
.0
|
||||
{
|
||||
let _ = app.update(message);
|
||||
}
|
||||
assert_eq!(actual_offset(&mut view, &mut tree), Vector::new(0., 180.));
|
||||
navigation_event(&mut app, |ctx| CanvasEvent::OpenUse(ctx, use_id));
|
||||
view = observer(600., app.scenario_context().unwrap());
|
||||
view.area = area;
|
||||
view.diff(&mut tree);
|
||||
for message in update(&mut view, &mut tree, redraw()).0 {
|
||||
let _ = app.update(message);
|
||||
}
|
||||
assert_eq!(actual_offset(&mut view, &mut tree), Vector::ZERO);
|
||||
navigation_event(&mut app, CanvasEvent::Back);
|
||||
assert_eq!(app.selected, use_id);
|
||||
let context = app.scenario_context().unwrap();
|
||||
let request = app.behavior.scenario.scroll_restore(context, area).unwrap();
|
||||
view = observer(returned_height, context);
|
||||
view.area = area;
|
||||
view.restore = Some(request);
|
||||
view.diff(&mut tree);
|
||||
let expected = Vector::new(0., (returned_height - 200.).min(180.));
|
||||
let messages = update(&mut view, &mut tree, redraw()).0;
|
||||
assert!(
|
||||
matches!(messages.as_slice(), [Message::ScenarioCanvas(CanvasEvent::Restored(_, _, _, actual))] if *actual == expected)
|
||||
);
|
||||
assert_eq!(actual_offset(&mut view, &mut tree), expected);
|
||||
for message in messages {
|
||||
let _ = app.update(message);
|
||||
}
|
||||
assert!(
|
||||
app.behavior
|
||||
.scenario
|
||||
.scroll_restore(app.scenario_context().unwrap(), area)
|
||||
.is_none()
|
||||
);
|
||||
// A delayed receipt leaves the same request in the view; later input must not snap back.
|
||||
for message in update(
|
||||
&mut view,
|
||||
&mut tree,
|
||||
iced::Event::Mouse(mouse::Event::WheelScrolled {
|
||||
delta: mouse::ScrollDelta::Pixels { x: 0., y: 20. },
|
||||
}),
|
||||
)
|
||||
.0
|
||||
{
|
||||
let _ = app.update(message);
|
||||
}
|
||||
let moved = expected - Vector::new(0., 20.);
|
||||
assert_eq!(actual_offset(&mut view, &mut tree), moved);
|
||||
view.context = app.scenario_context().unwrap();
|
||||
view.diff(&mut tree);
|
||||
assert!(update(&mut view, &mut tree, redraw()).0.is_empty());
|
||||
assert_eq!(actual_offset(&mut view, &mut tree), moved);
|
||||
navigation_event(&mut app, |ctx| {
|
||||
CanvasEvent::Restored(ctx, request.token, area, Vector::new(0., 999.))
|
||||
});
|
||||
let late_context = app.scenario_context().unwrap();
|
||||
let _ = app.update(Message::Page(crate::app::Page::Diagram));
|
||||
let epoch = app.canvas_epoch;
|
||||
let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Restored(
|
||||
late_context,
|
||||
request.token,
|
||||
area,
|
||||
Vector::ZERO,
|
||||
)));
|
||||
assert_eq!(app.canvas_epoch, epoch);
|
||||
assert_eq!(app.project(), &project);
|
||||
assert_eq!(app.document.bundle(), &source);
|
||||
assert!(!app.document.can_undo());
|
||||
let area = ScrollArea::Page;
|
||||
for returned_height in [600., 250.] {
|
||||
let (project, [a, _, _], [use_id, _, _]) = navigation_fixture();
|
||||
let mut app = App::with_project(project.clone());
|
||||
let _ = app.update(Message::Select(a));
|
||||
let _ = app.update(Message::Page(crate::app::Page::Behavior));
|
||||
let source = app.document.bundle().clone();
|
||||
let mut view = observer(600., app.scenario_context().unwrap());
|
||||
view.area = area;
|
||||
let mut tree = widget::Tree::new(&view as &dyn Widget<Message, Theme, ()>);
|
||||
for message in update(&mut view, &mut tree, redraw()).0 {
|
||||
let _ = app.update(message);
|
||||
}
|
||||
for message in update(
|
||||
&mut view,
|
||||
&mut tree,
|
||||
iced::Event::Mouse(mouse::Event::WheelScrolled {
|
||||
delta: mouse::ScrollDelta::Pixels { x: 0., y: -180. },
|
||||
}),
|
||||
)
|
||||
.0
|
||||
{
|
||||
let _ = app.update(message);
|
||||
}
|
||||
assert_eq!(actual_offset(&mut view, &mut tree), Vector::new(0., 180.));
|
||||
navigation_event(&mut app, |ctx| CanvasEvent::OpenUse(ctx, use_id));
|
||||
view = observer(600., app.scenario_context().unwrap());
|
||||
view.area = area;
|
||||
view.diff(&mut tree);
|
||||
for message in update(&mut view, &mut tree, redraw()).0 {
|
||||
let _ = app.update(message);
|
||||
}
|
||||
assert_eq!(actual_offset(&mut view, &mut tree), Vector::ZERO);
|
||||
navigation_event(&mut app, CanvasEvent::Back);
|
||||
assert_eq!(app.selected, use_id);
|
||||
let context = app.scenario_context().unwrap();
|
||||
let request = app.behavior.scenario.scroll_restore(context, area).unwrap();
|
||||
view = observer(returned_height, context);
|
||||
view.area = area;
|
||||
view.restore = Some(request);
|
||||
view.diff(&mut tree);
|
||||
let expected = Vector::new(0., (returned_height - 200.).min(180.));
|
||||
let messages = update(&mut view, &mut tree, redraw()).0;
|
||||
assert!(
|
||||
matches!(messages.as_slice(), [Message::ScenarioCanvas(CanvasEvent::Restored(_, _, _, actual))] if *actual == expected)
|
||||
);
|
||||
assert_eq!(actual_offset(&mut view, &mut tree), expected);
|
||||
for message in messages {
|
||||
let _ = app.update(message);
|
||||
}
|
||||
assert!(
|
||||
app.behavior
|
||||
.scenario
|
||||
.scroll_restore(app.scenario_context().unwrap(), area)
|
||||
.is_none()
|
||||
);
|
||||
// A delayed receipt leaves the same request in the view; later input must not snap back.
|
||||
for message in update(
|
||||
&mut view,
|
||||
&mut tree,
|
||||
iced::Event::Mouse(mouse::Event::WheelScrolled {
|
||||
delta: mouse::ScrollDelta::Pixels { x: 0., y: 20. },
|
||||
}),
|
||||
)
|
||||
.0
|
||||
{
|
||||
let _ = app.update(message);
|
||||
}
|
||||
let moved = expected - Vector::new(0., 20.);
|
||||
assert_eq!(actual_offset(&mut view, &mut tree), moved);
|
||||
view.context = app.scenario_context().unwrap();
|
||||
view.diff(&mut tree);
|
||||
assert!(update(&mut view, &mut tree, redraw()).0.is_empty());
|
||||
assert_eq!(actual_offset(&mut view, &mut tree), moved);
|
||||
navigation_event(&mut app, |ctx| {
|
||||
CanvasEvent::Restored(ctx, request.token, area, Vector::new(0., 999.))
|
||||
});
|
||||
let late_context = app.scenario_context().unwrap();
|
||||
let _ = app.update(Message::Page(crate::app::Page::Diagram));
|
||||
let epoch = app.canvas_epoch;
|
||||
let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Restored(
|
||||
late_context,
|
||||
request.token,
|
||||
area,
|
||||
Vector::ZERO,
|
||||
)));
|
||||
assert_eq!(app.canvas_epoch, epoch);
|
||||
assert_eq!(app.project(), &project);
|
||||
assert_eq!(app.document.bundle(), &source);
|
||||
assert!(!app.document.can_undo());
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
@@ -578,9 +562,9 @@ mod scenario_scroll {
|
||||
other.scenario = Id::new_v4();
|
||||
let mut state = topology::State::default();
|
||||
state.enter(context.document, context.scenario);
|
||||
state.scrolled(scenario::ScrollArea::Canvas, Vector::new(0., 180.));
|
||||
state.scrolled(scenario::ScrollArea::Page, Vector::new(0., 180.));
|
||||
state.enter(other.document, other.scenario);
|
||||
assert_eq!(state.scroll_offset(), Vector::ZERO);
|
||||
assert_eq!(state.details_offset(), Vector::ZERO);
|
||||
view = observer(600., other);
|
||||
view.process = true;
|
||||
view.diff(&mut tree);
|
||||
@@ -597,92 +581,91 @@ mod scenario_scroll {
|
||||
{
|
||||
use widget::operation::scrollable::{AbsoluteOffset, scroll_to};
|
||||
for process in [false, true] {
|
||||
for area in [scenario::ScrollArea::Canvas, scenario::ScrollArea::Page] {
|
||||
for change in 0..4 {
|
||||
let context = scenario::direct_tests::context(Id::new_v4());
|
||||
let mut view = observer(600., context);
|
||||
view.process = process;
|
||||
view.area = area;
|
||||
let mut tree = widget::Tree::new(&view as &dyn Widget<Message, Theme, ()>);
|
||||
let node = view.layout(
|
||||
&mut tree,
|
||||
&(),
|
||||
&layout::Limits::new(Size::ZERO, Size::new(800., 700.)),
|
||||
);
|
||||
// Model a programmatic reveal before the observer has reported.
|
||||
// Mounted identity must not depend on its last scroll notification.
|
||||
let mut operation = scroll_to::<()>(
|
||||
widget::Id::new("observer-test"),
|
||||
AbsoluteOffset {
|
||||
x: None,
|
||||
y: Some(180.),
|
||||
},
|
||||
);
|
||||
view.operate(&mut tree, Layout::new(&node), &(), &mut operation);
|
||||
let mut next = context;
|
||||
match change {
|
||||
0 => next.scenario = Id::new_v4(),
|
||||
1 => next.document = Id::new_v4(),
|
||||
2 => {
|
||||
next.revision += 1;
|
||||
next.epoch += 5;
|
||||
}
|
||||
_ => {}
|
||||
let area = scenario::ScrollArea::Page;
|
||||
for change in 0..4 {
|
||||
let context = scenario::direct_tests::context(Id::new_v4());
|
||||
let mut view = observer(600., context);
|
||||
view.process = process;
|
||||
view.area = area;
|
||||
let mut tree = widget::Tree::new(&view as &dyn Widget<Message, Theme, ()>);
|
||||
let node = view.layout(
|
||||
&mut tree,
|
||||
&(),
|
||||
&layout::Limits::new(Size::ZERO, Size::new(800., 700.)),
|
||||
);
|
||||
// Model a programmatic reveal before the observer has reported.
|
||||
// Mounted identity must not depend on its last scroll notification.
|
||||
let mut operation = scroll_to::<()>(
|
||||
widget::Id::new("observer-test"),
|
||||
AbsoluteOffset {
|
||||
x: None,
|
||||
y: Some(180.),
|
||||
},
|
||||
);
|
||||
view.operate(&mut tree, Layout::new(&node), &(), &mut operation);
|
||||
let mut next = context;
|
||||
match change {
|
||||
0 => next.scenario = Id::new_v4(),
|
||||
1 => next.document = Id::new_v4(),
|
||||
2 => {
|
||||
next.revision += 1;
|
||||
next.epoch += 5;
|
||||
}
|
||||
view = observer(if change == 3 { 800. } else { 600. }, next);
|
||||
view.process = process;
|
||||
view.area = area;
|
||||
view.diff(&mut tree);
|
||||
let redraw = || {
|
||||
iced::Event::Window(iced::window::Event::RedrawRequested(
|
||||
std::time::Instant::now(),
|
||||
))
|
||||
};
|
||||
let (messages, _) = update(&mut view, &mut tree, redraw());
|
||||
let actual = messages
|
||||
.into_iter()
|
||||
.filter_map(|message| match message {
|
||||
Message::TopologyCanvas(topology::CanvasEvent::Scrolled(
|
||||
c,
|
||||
reported_area,
|
||||
offset,
|
||||
)) => {
|
||||
assert_eq!(
|
||||
(c.document, c.owner, c.revision, c.epoch),
|
||||
(next.document, next.scenario, next.revision, next.epoch)
|
||||
);
|
||||
assert_eq!(
|
||||
std::mem::discriminant(&reported_area),
|
||||
std::mem::discriminant(&area)
|
||||
);
|
||||
Some(offset)
|
||||
}
|
||||
Message::ScenarioCanvas(scenario::CanvasEvent::Scrolled(
|
||||
c,
|
||||
reported_area,
|
||||
offset,
|
||||
)) => {
|
||||
assert_eq!(c, next);
|
||||
assert_eq!(
|
||||
std::mem::discriminant(&reported_area),
|
||||
std::mem::discriminant(&area)
|
||||
);
|
||||
Some(offset)
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
actual,
|
||||
vec![if change < 2 {
|
||||
Vector::ZERO
|
||||
} else {
|
||||
Vector::new(0., 180.)
|
||||
}],
|
||||
"process={process} area={area:?} change={change}"
|
||||
);
|
||||
assert!(update(&mut view, &mut tree, redraw()).0.is_empty());
|
||||
_ => {}
|
||||
}
|
||||
view = observer(if change == 3 { 800. } else { 600. }, next);
|
||||
view.process = process;
|
||||
view.area = area;
|
||||
view.diff(&mut tree);
|
||||
let redraw = || {
|
||||
iced::Event::Window(iced::window::Event::RedrawRequested(
|
||||
std::time::Instant::now(),
|
||||
))
|
||||
};
|
||||
let (messages, _) = update(&mut view, &mut tree, redraw());
|
||||
let actual = messages
|
||||
.into_iter()
|
||||
.filter_map(|message| match message {
|
||||
Message::TopologyCanvas(topology::CanvasEvent::Scrolled(
|
||||
c,
|
||||
reported_area,
|
||||
offset,
|
||||
)) => {
|
||||
assert_eq!(
|
||||
(c.document, c.owner, c.revision, c.epoch),
|
||||
(next.document, next.scenario, next.revision, next.epoch)
|
||||
);
|
||||
assert_eq!(
|
||||
std::mem::discriminant(&reported_area),
|
||||
std::mem::discriminant(&area)
|
||||
);
|
||||
Some(offset)
|
||||
}
|
||||
Message::ScenarioCanvas(scenario::CanvasEvent::Scrolled(
|
||||
c,
|
||||
reported_area,
|
||||
offset,
|
||||
)) => {
|
||||
assert_eq!(c, next);
|
||||
assert_eq!(
|
||||
std::mem::discriminant(&reported_area),
|
||||
std::mem::discriminant(&area)
|
||||
);
|
||||
Some(offset)
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
actual,
|
||||
vec![if change < 2 {
|
||||
Vector::ZERO
|
||||
} else {
|
||||
Vector::new(0., 180.)
|
||||
}],
|
||||
"process={process} area={area:?} change={change}"
|
||||
);
|
||||
assert!(update(&mut view, &mut tree, redraw()).0.is_empty());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1141,12 +1124,26 @@ struct BoxItem {
|
||||
detail: String,
|
||||
marker: Option<Kind>,
|
||||
}
|
||||
fn box_text_clip(rect: Rectangle, scale: f32, viewport: Size) -> Option<Rectangle> {
|
||||
if !scale.is_finite() || scale <= 0. || rect.width <= 20. || rect.height <= 16. {
|
||||
fn box_text_clip(
|
||||
pan: iced::Vector,
|
||||
rect: Rectangle,
|
||||
scale: f32,
|
||||
viewport: Size,
|
||||
) -> Option<Rectangle> {
|
||||
if !pan.x.is_finite()
|
||||
|| !pan.y.is_finite()
|
||||
|| !scale.is_finite()
|
||||
|| scale <= 0.
|
||||
|| rect.width <= 20.
|
||||
|| rect.height <= 16.
|
||||
{
|
||||
return None;
|
||||
}
|
||||
Rectangle::new(
|
||||
Point::new((rect.x + 10.) * scale, (rect.y + 8.) * scale),
|
||||
Point::new(
|
||||
pan.x + (rect.x + 10.) * scale,
|
||||
pan.y + (rect.y + 8.) * scale,
|
||||
),
|
||||
Size::new((rect.width - 20.) * scale, (rect.height - 16.) * scale),
|
||||
)
|
||||
.intersection(&Rectangle::new(Point::ORIGIN, viewport))
|
||||
@@ -1165,7 +1162,7 @@ struct Scene {
|
||||
size: Size,
|
||||
selected: Id,
|
||||
scale: f32,
|
||||
scroll_offset: iced::Vector,
|
||||
camera_pan: iced::Vector,
|
||||
interaction: Option<(scenario::Context, scenario::CanvasTool, bool)>,
|
||||
pending_placement: Option<(scenario::Slot, Option<Id>)>,
|
||||
lanes: BTreeMap<Id, f32>,
|
||||
@@ -1220,7 +1217,7 @@ impl Scene {
|
||||
fn sequence(p: &Project, owner: Id, selected: Id) -> Self {
|
||||
let mut scene = Self {
|
||||
scale: 1.,
|
||||
scroll_offset: iced::Vector::ZERO,
|
||||
camera_pan: iced::Vector::ZERO,
|
||||
boxes: vec![],
|
||||
lines: vec![],
|
||||
size: Size::new(700., 200.),
|
||||
@@ -1931,8 +1928,7 @@ impl canvas::Program<Message> for Scene {
|
||||
if !editable {
|
||||
state.cancel();
|
||||
}
|
||||
// Scrollable marks a pointer outside its visible viewport unavailable,
|
||||
// even when it lies inside the full (scrolled) canvas rectangle.
|
||||
// The viewport masks input outside its visible canvas bounds.
|
||||
let visible = cursor.position().is_some();
|
||||
if !visible
|
||||
&& matches!(
|
||||
@@ -1955,7 +1951,7 @@ impl canvas::Program<Message> for Scene {
|
||||
SequencePress::Occurrence { .. } => {}
|
||||
}
|
||||
}
|
||||
mouse::Cursor::Available(*position + self.scroll_offset)
|
||||
mouse::Cursor::Available(*position)
|
||||
}
|
||||
canvas::Event::Mouse(mouse::Event::WheelScrolled { .. })
|
||||
| canvas::Event::Window(iced::window::Event::Resized(_))
|
||||
@@ -1967,11 +1963,9 @@ impl canvas::Program<Message> for Scene {
|
||||
}
|
||||
canvas::Event::Mouse(
|
||||
mouse::Event::ButtonPressed(_) | mouse::Event::ButtonReleased(_),
|
||||
) => state
|
||||
.pointer
|
||||
.map_or(cursor, |p| mouse::Cursor::Available(p + self.scroll_offset)),
|
||||
) => state.pointer.map_or(cursor, mouse::Cursor::Available),
|
||||
canvas::Event::Window(iced::window::Event::RedrawRequested(_)) => {
|
||||
state.pointer = cursor.position().map(|p| p - self.scroll_offset);
|
||||
state.pointer = cursor.position();
|
||||
cursor
|
||||
}
|
||||
canvas::Event::Keyboard(iced::keyboard::Event::KeyPressed {
|
||||
@@ -1983,9 +1977,12 @@ impl canvas::Program<Message> for Scene {
|
||||
}
|
||||
_ => cursor,
|
||||
};
|
||||
let world = cursor
|
||||
.position_in(bounds)
|
||||
.map(|p| Point::new(p.x / self.scale, p.y / self.scale));
|
||||
let world = cursor.position_in(bounds).map(|p| {
|
||||
Point::new(
|
||||
(p.x - self.camera_pan.x) / self.scale,
|
||||
(p.y - self.camera_pan.y) / self.scale,
|
||||
)
|
||||
});
|
||||
match event {
|
||||
canvas::Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
|
||||
if !visible {
|
||||
@@ -2005,14 +2002,14 @@ impl canvas::Program<Message> for Scene {
|
||||
state.press = Some(SequencePress::Message {
|
||||
source,
|
||||
slot,
|
||||
start: cursor.position()? - self.scroll_offset,
|
||||
start: cursor.position()?,
|
||||
first,
|
||||
moved: false,
|
||||
});
|
||||
return Some(Action::request_redraw().and_capture());
|
||||
}
|
||||
if let CanvasTool::Add(tool) = tool {
|
||||
if !tool.canvas_occurrence() {
|
||||
if !tool.canvas_occurrence() && tool != scenario::Tool::Lifeline {
|
||||
return None;
|
||||
}
|
||||
state.press = Some(SequencePress::Occurrence {
|
||||
@@ -2022,11 +2019,19 @@ impl canvas::Program<Message> for Scene {
|
||||
});
|
||||
return Some(Action::request_redraw().and_capture());
|
||||
}
|
||||
if self.reorder_handle().is_some_and(|r| r.contains(point)) {
|
||||
if let Some(occurrence) = self
|
||||
.hit(point)
|
||||
.filter(|id| self.rows.contains_key(id))
|
||||
.or_else(|| {
|
||||
self.reorder_handle()
|
||||
.filter(|r| r.contains(point))
|
||||
.map(|_| self.selected)
|
||||
})
|
||||
{
|
||||
state.press = Some(SequencePress::Reorder {
|
||||
occurrence: self.selected,
|
||||
parent: self.rows[&self.selected].0,
|
||||
start: cursor.position()? - self.scroll_offset,
|
||||
occurrence,
|
||||
parent: self.rows[&occurrence].0,
|
||||
start: cursor.position()?,
|
||||
moved: false,
|
||||
});
|
||||
return Some(Action::request_redraw().and_capture());
|
||||
@@ -2100,6 +2105,18 @@ impl canvas::Program<Message> for Scene {
|
||||
);
|
||||
}
|
||||
}
|
||||
SequencePress::Reorder {
|
||||
occurrence,
|
||||
moved: false,
|
||||
..
|
||||
} => {
|
||||
return Some(
|
||||
Action::publish(Message::ScenarioCanvas(CanvasEvent::Select(
|
||||
context, occurrence,
|
||||
)))
|
||||
.and_capture(),
|
||||
);
|
||||
}
|
||||
SequencePress::Reorder {
|
||||
occurrence,
|
||||
parent,
|
||||
@@ -2137,6 +2154,7 @@ impl canvas::Program<Message> for Scene {
|
||||
) -> Vec<canvas::Geometry> {
|
||||
let mut f = Frame::new(renderer, bounds.size());
|
||||
f.fill_rectangle(Point::ORIGIN, bounds.size(), Color::WHITE);
|
||||
f.translate(self.camera_pan);
|
||||
f.scale(self.scale);
|
||||
self.lifecycle.draw_lanes(&mut f, self.selected);
|
||||
for b in &self.boxes {
|
||||
@@ -2217,10 +2235,11 @@ impl canvas::Program<Message> for Scene {
|
||||
}
|
||||
}
|
||||
for b in &self.boxes {
|
||||
if let Some(clip) = box_text_clip(b.rect, self.scale, f.size()) {
|
||||
if let Some(clip) = box_text_clip(self.camera_pan, b.rect, self.scale, f.size()) {
|
||||
// Clipped child frames start with an identity transform. Match
|
||||
// the card's scale explicitly, as for lifecycle Create labels.
|
||||
// the card's pan and scale, as for lifecycle Create labels.
|
||||
f.with_clip(clip, |label| {
|
||||
label.translate(self.camera_pan);
|
||||
label.scale(self.scale);
|
||||
label.fill_text(Text {
|
||||
content: short(&b.title, ((b.rect.width - 25.) / 7.) as usize),
|
||||
@@ -2255,12 +2274,15 @@ impl canvas::Program<Message> for Scene {
|
||||
});
|
||||
}
|
||||
self.lifecycle
|
||||
.draw_markers(&mut f, self.selected, self.scale);
|
||||
.draw_markers(&mut f, self.selected, self.scale, self.camera_pan);
|
||||
if let Some((context, tool, editable)) = self.interaction {
|
||||
let active = editable && state.context == Some(context) && state.tool == tool;
|
||||
let point = cursor
|
||||
.position_in(bounds)
|
||||
.map(|p| Point::new(p.x / self.scale, p.y / self.scale));
|
||||
let point = cursor.position_in(bounds).map(|p| {
|
||||
Point::new(
|
||||
(p.x - self.camera_pan.x) / self.scale,
|
||||
(p.y - self.camera_pan.y) / self.scale,
|
||||
)
|
||||
});
|
||||
let source = active.then_some(state.source).flatten();
|
||||
let reorder = active.then_some(state.press).flatten().and_then(|press| {
|
||||
if let SequencePress::Reorder { parent, .. } = press {
|
||||
@@ -2305,6 +2327,18 @@ impl canvas::Program<Message> for Scene {
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if reorder.is_some()
|
||||
&& slot.is_none()
|
||||
&& let Some(point) = point
|
||||
{
|
||||
f.fill_text(Text {
|
||||
content: "Other operand: move stays in the current sequence".into(),
|
||||
position: Point::new(point.x + 8., point.y + 12.),
|
||||
size: (11. / self.scale).into(),
|
||||
color: Color::from_rgb(0.75, 0.2, 0.15),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
if let Some(slot) = slot {
|
||||
f.stroke(
|
||||
&Path::line(
|
||||
@@ -2510,25 +2544,26 @@ mod direct_canvas_tests {
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn direct_scrolled_canvas_maps_raw_pointer_events_to_the_visible_slot() {
|
||||
fn direct_panned_canvas_maps_ordered_pointer_events_to_the_visible_slot() {
|
||||
let (f, mut scene) = scene();
|
||||
scene.scroll_offset = iced::Vector::new(35., 140.);
|
||||
scene.camera_pan = iced::Vector::new(-35., -140.);
|
||||
let bounds = Rectangle::new(Point::new(20., 230.), scene.size);
|
||||
let slot = scene.slots.last().unwrap().slot;
|
||||
let y = scene.slots.last().unwrap().y;
|
||||
let a = Point::new(bounds.x + scene.lanes[&f.sender], bounds.y + y) - scene.scroll_offset;
|
||||
let b = Point::new(bounds.x + scene.lanes[&f.receiver], bounds.y + y) - scene.scroll_offset;
|
||||
let a = Point::new(bounds.x + scene.lanes[&f.sender], bounds.y + y) + scene.camera_pan;
|
||||
let b = Point::new(bounds.x + scene.lanes[&f.receiver], bounds.y + y) + scene.camera_pan;
|
||||
assert!(bounds.contains(a) && bounds.contains(b));
|
||||
let mut state = SequenceInput::default();
|
||||
// Iced Scrollable passes raw event coordinates and a translated cursor.
|
||||
// Batched pointer events use their own positions with the final cursor.
|
||||
for event in [
|
||||
mouse::Event::CursorMoved { position: a },
|
||||
mouse::Event::ButtonPressed(mouse::Button::Left),
|
||||
mouse::Event::CursorMoved { position: b },
|
||||
] {
|
||||
send(&scene, &mut state, event, b + scene.scroll_offset, bounds);
|
||||
send(&scene, &mut state, event, b, bounds);
|
||||
}
|
||||
assert!(
|
||||
matches!(send(&scene, &mut state, mouse::Event::ButtonReleased(mouse::Button::Left), b + scene.scroll_offset, bounds), Some(CanvasEvent::Begin { sender, receiver, slot: actual, .. }) if sender == f.sender && receiver == f.receiver && actual == slot)
|
||||
matches!(send(&scene, &mut state, mouse::Event::ButtonReleased(mouse::Button::Left), b, bounds), Some(CanvasEvent::Begin { sender, receiver, slot: actual, .. }) if sender == f.sender && receiver == f.receiver && actual == slot)
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user