Improve behavior authoring and typed trace history (untested checkpoint)
Rust checks / check (ubuntu-latest) (push) Canceled after 0s
Rust checks / check (windows-latest) (push) Canceled after 0s
Rust checks / check (ubuntu-latest) (push) Canceled after 0s
Rust checks / check (windows-latest) (push) Canceled after 0s
This commit is contained in:
@@ -32,6 +32,7 @@ mod scenario_scroll {
|
||||
pub(super) fn observe<'a>(
|
||||
context: scenario::Context,
|
||||
area: scenario::ScrollArea,
|
||||
restore: Option<scenario::ScrollRestore>,
|
||||
content: impl Into<Element<'a, Message>>,
|
||||
) -> Element<'a, Message> {
|
||||
Element::new(Observer {
|
||||
@@ -39,6 +40,7 @@ mod scenario_scroll {
|
||||
context,
|
||||
area,
|
||||
process: false,
|
||||
restore,
|
||||
})
|
||||
}
|
||||
pub(super) fn observe_topology<'a>(
|
||||
@@ -56,6 +58,7 @@ mod scenario_scroll {
|
||||
},
|
||||
area,
|
||||
process: true,
|
||||
restore: None,
|
||||
})
|
||||
}
|
||||
struct Observer<'a, R = Renderer> {
|
||||
@@ -63,11 +66,13 @@ mod scenario_scroll {
|
||||
context: scenario::Context,
|
||||
area: scenario::ScrollArea,
|
||||
process: bool,
|
||||
restore: Option<scenario::ScrollRestore>,
|
||||
}
|
||||
type Report = Option<(bool, Id, Id, Vector)>;
|
||||
struct ObservedScroll {
|
||||
identity: (Id, Id),
|
||||
report: Report,
|
||||
restored: Option<Id>,
|
||||
}
|
||||
#[derive(Default)]
|
||||
struct Translation(Option<Vector>);
|
||||
@@ -84,6 +89,28 @@ mod scenario_scroll {
|
||||
self.0 = Some(translation);
|
||||
}
|
||||
}
|
||||
struct RestoreTranslation {
|
||||
offset: Vector,
|
||||
applied: bool,
|
||||
}
|
||||
impl widget::Operation for RestoreTranslation {
|
||||
// Restore only this wrapper's root Scrollable, never its nested canvas.
|
||||
fn traverse(&mut self, _: &mut dyn FnMut(&mut dyn widget::Operation)) {}
|
||||
fn scrollable(
|
||||
&mut self,
|
||||
_: Option<&widget::Id>,
|
||||
_: Rectangle,
|
||||
_: Rectangle,
|
||||
_: Vector,
|
||||
state: &mut dyn widget::operation::Scrollable,
|
||||
) {
|
||||
state.scroll_to(widget::operation::scrollable::AbsoluteOffset {
|
||||
x: Some(self.offset.x),
|
||||
y: Some(self.offset.y),
|
||||
});
|
||||
self.applied = true;
|
||||
}
|
||||
}
|
||||
impl<R: renderer::Renderer> Observer<'_, R> {
|
||||
fn report(
|
||||
&mut self,
|
||||
@@ -92,6 +119,25 @@ mod scenario_scroll {
|
||||
renderer: &R,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
) {
|
||||
let restore = self.restore.filter(|request| {
|
||||
!self.process
|
||||
&& (request.document, request.owner)
|
||||
== (self.context.document, self.context.scenario)
|
||||
&& tree.state.downcast_ref::<ObservedScroll>().restored != Some(request.token)
|
||||
});
|
||||
let receipt = restore.and_then(|request| {
|
||||
let mut operation = RestoreTranslation {
|
||||
offset: request.offset,
|
||||
applied: false,
|
||||
};
|
||||
self.content.as_widget_mut().operate(
|
||||
&mut tree.children[0],
|
||||
layout,
|
||||
renderer,
|
||||
&mut operation,
|
||||
);
|
||||
operation.applied.then_some(request.token)
|
||||
});
|
||||
let mut offset = Translation::default();
|
||||
self.content.as_widget_mut().operate(
|
||||
&mut tree.children[0],
|
||||
@@ -106,7 +152,19 @@ mod scenario_scroll {
|
||||
self.context.scenario,
|
||||
offset,
|
||||
));
|
||||
let previous = &mut tree.state.downcast_mut::<ObservedScroll>().report;
|
||||
let state = tree.state.downcast_mut::<ObservedScroll>();
|
||||
if let Some(token) = receipt {
|
||||
state.restored = Some(token);
|
||||
state.report = next;
|
||||
shell.publish(Message::ScenarioCanvas(scenario::CanvasEvent::Restored(
|
||||
self.context,
|
||||
token,
|
||||
self.area,
|
||||
offset,
|
||||
)));
|
||||
return;
|
||||
}
|
||||
let previous = &mut state.report;
|
||||
if *previous != next {
|
||||
*previous = next;
|
||||
shell.publish(if self.process {
|
||||
@@ -139,6 +197,7 @@ mod scenario_scroll {
|
||||
widget::tree::State::new(ObservedScroll {
|
||||
identity: (self.context.document, self.context.scenario),
|
||||
report: None,
|
||||
restored: None,
|
||||
})
|
||||
}
|
||||
fn children(&self) -> Vec<widget::Tree> {
|
||||
@@ -274,6 +333,7 @@ mod scenario_scroll {
|
||||
context,
|
||||
area: scenario::ScrollArea::Canvas,
|
||||
process: false,
|
||||
restore: None,
|
||||
content: scrollable(iced::widget::Space::new().width(200.).height(height))
|
||||
.id("observer-test")
|
||||
.width(200.)
|
||||
@@ -320,6 +380,154 @@ mod scenario_scroll {
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
fn actual_offset(view: &mut Observer<'_, ()>, tree: &mut widget::Tree) -> Vector {
|
||||
let node = view.layout(
|
||||
tree,
|
||||
&(),
|
||||
&layout::Limits::new(Size::ZERO, Size::new(800., 700.)),
|
||||
);
|
||||
let mut operation = Translation::default();
|
||||
view.operate(tree, Layout::new(&node), &(), &mut operation);
|
||||
operation.0.unwrap()
|
||||
}
|
||||
#[test]
|
||||
fn direct_scenario_caller_restores_mounted_scroll_once_with_clamping_and_app_receipt() {
|
||||
use scenario::{
|
||||
CanvasEvent, ScrollArea,
|
||||
direct_tests::{navigation_event, navigation_fixture},
|
||||
};
|
||||
let redraw = || {
|
||||
iced::Event::Window(iced::window::Event::RedrawRequested(
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn direct_scenario_restore_cannot_scroll_another_document_owner_or_process_view() {
|
||||
for mismatch in 0..3 {
|
||||
let context = scenario::direct_tests::context(Id::new_v4());
|
||||
let mut request = scenario::ScrollRestore {
|
||||
document: context.document,
|
||||
owner: context.scenario,
|
||||
token: Id::new_v4(),
|
||||
offset: Vector::new(0., 180.),
|
||||
};
|
||||
match mismatch {
|
||||
0 => request.document = Id::new_v4(),
|
||||
1 => request.owner = Id::new_v4(),
|
||||
_ => {}
|
||||
}
|
||||
let mut view = observer(600., context);
|
||||
view.process = mismatch == 2;
|
||||
view.restore = Some(request);
|
||||
let mut tree = widget::Tree::new(&view as &dyn Widget<Message, Theme, ()>);
|
||||
let messages = update(
|
||||
&mut view,
|
||||
&mut tree,
|
||||
iced::Event::Window(iced::window::Event::RedrawRequested(
|
||||
std::time::Instant::now(),
|
||||
)),
|
||||
)
|
||||
.0;
|
||||
assert_eq!(actual_offset(&mut view, &mut tree), Vector::ZERO);
|
||||
assert!(!messages.iter().any(|m| matches!(
|
||||
m,
|
||||
Message::ScenarioCanvas(scenario::CanvasEvent::Restored(..))
|
||||
)));
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn independent_process_owner_change_resets_actual_scroll_offsets() {
|
||||
let context = scenario::direct_tests::context(Id::new_v4());
|
||||
@@ -711,8 +919,7 @@ pub fn view(app: &App) -> Element<'_, Message> {
|
||||
return out.push(label("Start with a Process, Scenario or State machine using Create element. Add occurrences or regions inside it to describe behavior.")).padding(24).into();
|
||||
};
|
||||
let e = &p.elements[&selected.id];
|
||||
let compact_process = matches!(e.kind(), Kind::Process | Kind::FunctionalChain)
|
||||
&& (uses_explicit_topology(p, e.id) || e.steps().is_none_or(Vec::is_empty));
|
||||
let compact_process = matches!(e.kind(), Kind::Process | Kind::FunctionalChain);
|
||||
if e.kind() == Kind::StateMachine {
|
||||
out = column![text("States & modes").size(20)].spacing(8);
|
||||
} else if compact_process {
|
||||
@@ -841,6 +1048,7 @@ struct Scene {
|
||||
scale: f32,
|
||||
scroll_offset: iced::Vector,
|
||||
interaction: Option<(scenario::Context, scenario::CanvasTool, bool)>,
|
||||
pending_placement: Option<(scenario::Slot, Option<Id>)>,
|
||||
lanes: BTreeMap<Id, f32>,
|
||||
slots: Vec<SequenceSlot>,
|
||||
rows: BTreeMap<Id, (Id, f32)>,
|
||||
@@ -863,6 +1071,11 @@ struct SequenceInput {
|
||||
}
|
||||
#[derive(Clone, Copy)]
|
||||
enum SequencePress {
|
||||
Occurrence {
|
||||
tool: scenario::Tool,
|
||||
slot: scenario::Slot,
|
||||
lifeline: Option<Id>,
|
||||
},
|
||||
Message {
|
||||
source: Id,
|
||||
slot: scenario::Slot,
|
||||
@@ -893,6 +1106,7 @@ impl Scene {
|
||||
size: Size::new(700., 200.),
|
||||
selected,
|
||||
interaction: None,
|
||||
pending_placement: None,
|
||||
lanes: BTreeMap::new(),
|
||||
slots: vec![],
|
||||
rows: BTreeMap::new(),
|
||||
@@ -1518,6 +1732,7 @@ impl canvas::Program<Message> for Scene {
|
||||
| SequencePress::Reorder { start, moved, .. } => {
|
||||
*moved |= position.distance(*start) >= 3.
|
||||
}
|
||||
SequencePress::Occurrence { .. } => {}
|
||||
}
|
||||
}
|
||||
mouse::Cursor::Available(*position + self.scroll_offset)
|
||||
@@ -1576,6 +1791,17 @@ impl canvas::Program<Message> for Scene {
|
||||
});
|
||||
return Some(Action::request_redraw().and_capture());
|
||||
}
|
||||
if let CanvasTool::Add(tool) = tool {
|
||||
if !tool.canvas_occurrence() {
|
||||
return None;
|
||||
}
|
||||
state.press = Some(SequencePress::Occurrence {
|
||||
tool,
|
||||
slot: self.slot_at(point, None)?.slot,
|
||||
lifeline: tool.uses_lifeline().then(|| self.lane_at(point)).flatten(),
|
||||
});
|
||||
return Some(Action::request_redraw().and_capture());
|
||||
}
|
||||
if self.reorder_handle().is_some_and(|r| r.contains(point)) {
|
||||
state.press = Some(SequencePress::Reorder {
|
||||
occurrence: self.selected,
|
||||
@@ -1597,6 +1823,23 @@ impl canvas::Program<Message> for Scene {
|
||||
let press = state.press.take()?;
|
||||
let context = context?;
|
||||
match press {
|
||||
SequencePress::Occurrence {
|
||||
tool: captured,
|
||||
slot,
|
||||
lifeline,
|
||||
} if tool == CanvasTool::Add(captured) && world.is_some() => {
|
||||
return Some(
|
||||
Action::publish(Message::ScenarioCanvas(
|
||||
CanvasEvent::BeginOccurrence {
|
||||
context,
|
||||
tool: captured,
|
||||
slot,
|
||||
lifeline,
|
||||
},
|
||||
))
|
||||
.and_capture(),
|
||||
);
|
||||
}
|
||||
SequencePress::Message { source, slot, .. }
|
||||
if tool == CanvasTool::Message(scenario::MessageStyle::Timer)
|
||||
&& world.is_some() =>
|
||||
@@ -1783,8 +2026,8 @@ impl canvas::Program<Message> for Scene {
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
if let Some((context, tool, true)) = self.interaction {
|
||||
let active = state.context == Some(context) && state.tool == tool;
|
||||
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));
|
||||
@@ -1796,7 +2039,8 @@ impl canvas::Program<Message> for Scene {
|
||||
None
|
||||
}
|
||||
});
|
||||
if tool == scenario::CanvasTool::Select
|
||||
if editable
|
||||
&& tool == scenario::CanvasTool::Select
|
||||
&& let Some(handle) = self.reorder_handle()
|
||||
{
|
||||
f.fill_rectangle(handle.position(), handle.size(), style::TEAL);
|
||||
@@ -1808,9 +2052,25 @@ impl canvas::Program<Message> for Scene {
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
let slot = if let Some((_, slot)) = source {
|
||||
let placement = self.pending_placement.or_else(|| {
|
||||
active.then_some(state.press).flatten().and_then(|press| {
|
||||
if let SequencePress::Occurrence { slot, lifeline, .. } = press {
|
||||
Some((slot, lifeline))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
});
|
||||
let slot = if let Some((slot, _)) = placement {
|
||||
self.slots.iter().find(|s| s.slot == slot)
|
||||
} else if matches!(tool, scenario::CanvasTool::Message(_)) || reorder.is_some() {
|
||||
} else if let Some((_, slot)) = source {
|
||||
self.slots.iter().find(|s| s.slot == slot)
|
||||
} else if editable
|
||||
&& (matches!(
|
||||
tool,
|
||||
scenario::CanvasTool::Message(_) | scenario::CanvasTool::Add(_)
|
||||
) || reorder.is_some())
|
||||
{
|
||||
point.and_then(|p| self.slot_at(p, reorder))
|
||||
} else {
|
||||
None
|
||||
@@ -1829,12 +2089,29 @@ impl canvas::Program<Message> for Scene {
|
||||
Color::WHITE,
|
||||
);
|
||||
f.fill_text(Text {
|
||||
content: short(&format!("Insert: {}", slot.label), 88),
|
||||
content: short(
|
||||
&format!(
|
||||
"{}: {}",
|
||||
if self.pending_placement.is_some() {
|
||||
"Pending"
|
||||
} else {
|
||||
"Insert"
|
||||
},
|
||||
slot.label
|
||||
),
|
||||
88,
|
||||
),
|
||||
position: Point::new(38., slot.y - 19.),
|
||||
size: 11.into(),
|
||||
color: style::TEAL,
|
||||
..Default::default()
|
||||
});
|
||||
if let Some(x) = placement
|
||||
.and_then(|(_, lifeline)| lifeline)
|
||||
.and_then(|id| self.lanes.get(&id))
|
||||
{
|
||||
f.fill(&Path::circle(Point::new(*x, slot.y), 5.), style::TEAL);
|
||||
}
|
||||
if let Some((source, _)) = source
|
||||
&& let Some(x) = self.lanes.get(&source)
|
||||
&& let Some(point) = point
|
||||
@@ -2246,6 +2523,110 @@ mod direct_canvas_tests {
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn direct_clicked_occurrence_capture_cancels_on_view_and_context_changes() {
|
||||
for tool in [
|
||||
scenario::Tool::Action,
|
||||
scenario::Tool::Observation,
|
||||
scenario::Tool::Reuse,
|
||||
scenario::Tool::Fragment,
|
||||
] {
|
||||
for cancellation in 0..9 {
|
||||
let (f, mut scene) = scene();
|
||||
scene.interaction.as_mut().unwrap().1 = CanvasTool::Add(tool);
|
||||
let mut state = SequenceInput::default();
|
||||
let mut bounds = Rectangle::new(Point::ORIGIN, scene.size);
|
||||
let point = Point::new(scene.lanes[&f.sender], scene.slots[0].y);
|
||||
send(
|
||||
&scene,
|
||||
&mut state,
|
||||
mouse::Event::CursorMoved { position: point },
|
||||
point,
|
||||
bounds,
|
||||
);
|
||||
assert!(
|
||||
send(
|
||||
&scene,
|
||||
&mut state,
|
||||
mouse::Event::ButtonPressed(mouse::Button::Left),
|
||||
point,
|
||||
bounds
|
||||
)
|
||||
.is_none()
|
||||
);
|
||||
assert!(matches!(
|
||||
state.press,
|
||||
Some(SequencePress::Occurrence { .. })
|
||||
));
|
||||
let cancellation_event = match cancellation {
|
||||
0 => {
|
||||
scene.interaction.as_mut().unwrap().0.epoch += 1;
|
||||
None
|
||||
}
|
||||
1 => {
|
||||
scene.interaction.as_mut().unwrap().1 = CanvasTool::Select;
|
||||
None
|
||||
}
|
||||
2 => {
|
||||
scene.interaction.as_mut().unwrap().2 = false;
|
||||
None
|
||||
}
|
||||
3 => Some(canvas::Event::Mouse(mouse::Event::WheelScrolled {
|
||||
delta: mouse::ScrollDelta::Lines { x: 0., y: -1. },
|
||||
})),
|
||||
4 => Some(canvas::Event::Window(iced::window::Event::Resized(
|
||||
Size::new(1100., 700.),
|
||||
))),
|
||||
5 => {
|
||||
bounds.y -= 30.;
|
||||
None
|
||||
}
|
||||
6 => Some(canvas::Event::Window(iced::window::Event::Unfocused)),
|
||||
7 => Some(canvas::Event::Keyboard(iced::keyboard::Event::KeyPressed {
|
||||
key: iced::keyboard::Key::Named(iced::keyboard::key::Named::Escape),
|
||||
modified_key: iced::keyboard::Key::Named(
|
||||
iced::keyboard::key::Named::Escape,
|
||||
),
|
||||
physical_key: iced::keyboard::key::Physical::Code(
|
||||
iced::keyboard::key::Code::Escape,
|
||||
),
|
||||
location: iced::keyboard::Location::Standard,
|
||||
modifiers: iced::keyboard::Modifiers::empty(),
|
||||
text: None,
|
||||
repeat: false,
|
||||
})),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(event) = cancellation_event {
|
||||
let action = canvas::Program::update(
|
||||
&scene,
|
||||
&mut state,
|
||||
&event,
|
||||
bounds,
|
||||
mouse::Cursor::Available(point),
|
||||
);
|
||||
assert!(action.and_then(|action| action.into_inner().0).is_none());
|
||||
}
|
||||
let cursor = if cancellation == 8 {
|
||||
mouse::Cursor::Unavailable
|
||||
} else {
|
||||
mouse::Cursor::Available(point)
|
||||
};
|
||||
let released = canvas::Program::update(
|
||||
&scene,
|
||||
&mut state,
|
||||
&canvas::Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)),
|
||||
bounds,
|
||||
cursor,
|
||||
);
|
||||
assert!(
|
||||
released.and_then(|action| action.into_inner().0).is_none(),
|
||||
"{tool:?} cancellation {cancellation}"
|
||||
);
|
||||
assert!(state.press.is_none());
|
||||
}
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn direct_selected_handle_reorders_and_hits_keep_semantic_identity() {
|
||||
let (f, mut scene) = scene();
|
||||
scene.interaction.as_mut().unwrap().1 = CanvasTool::Select;
|
||||
|
||||
Reference in New Issue
Block a user