Preserve editor input context through diagram help
This commit is contained in:
@@ -380,6 +380,7 @@ pub enum Message {
|
||||
Guide(crate::guide::Event),
|
||||
ContextHelp(crate::guide::Topic),
|
||||
CloseContextHelp,
|
||||
ContextHelpScroll(crate::guide::ContextScroll),
|
||||
Help,
|
||||
Traceability(crate::views::traceability::Event),
|
||||
ReviewLayerTraces,
|
||||
@@ -1301,6 +1302,11 @@ impl App {
|
||||
iced::time::every(std::time::Duration::from_secs(10)).map(|_| Message::CheckpointTick),
|
||||
window::close_requests().map(|id| Message::Request(Pending::Close(id))),
|
||||
window::resize_events().map(|(_, size)| Message::WindowResized(size)),
|
||||
if self.context_help {
|
||||
iced::event::listen_with(crate::guide::context_scroll_key)
|
||||
} else {
|
||||
Subscription::none()
|
||||
},
|
||||
iced::event::listen_with(|event, _, _| {
|
||||
matches!(
|
||||
event,
|
||||
@@ -1355,6 +1361,13 @@ impl App {
|
||||
])
|
||||
}
|
||||
pub fn update(&mut self, message: Message) -> Task<Message> {
|
||||
if let Message::ContextHelpScroll(scroll) = message {
|
||||
return if self.context_help {
|
||||
crate::guide::context_scroll_task(scroll)
|
||||
} else {
|
||||
Task::none()
|
||||
};
|
||||
}
|
||||
// Help owns user input before any canvas/reference fast path can act.
|
||||
// Passive results may finish; they retain their normal lifetime checks.
|
||||
if self.context_help
|
||||
@@ -2852,6 +2865,7 @@ impl App {
|
||||
Message::CloseContextHelp => {
|
||||
self.context_help = false;
|
||||
}
|
||||
Message::ContextHelpScroll(_) => {}
|
||||
Message::Help => {
|
||||
let topic = match self.page {
|
||||
Page::Diagram => Some(crate::guide::Topic::Nesting),
|
||||
|
||||
@@ -220,7 +220,8 @@ fn context_help_f1_selects_scenario_topic_without_leaving_the_workspace() {
|
||||
#[test]
|
||||
fn context_help_disables_allocation_input_including_early_work_drag_dispatch() {
|
||||
let mut project = Project::empty();
|
||||
project.ensure_diagram(Layer::Oa);
|
||||
let diagram = project.ensure_diagram(Layer::Oa);
|
||||
project.diagrams.get_mut(&diagram).unwrap().projection = ProjectionKind::Allocation;
|
||||
let mut app = App::with_project(project);
|
||||
assert!(app.allocation_input_owner());
|
||||
send(&mut app, Message::Help);
|
||||
@@ -346,3 +347,356 @@ fn context_help_accepts_live_process_viewport_and_rejects_stale_measurements() {
|
||||
assert!(Arc::ptr_eq(&before, &app.document.snapshot()));
|
||||
assert!(!app.document.can_undo());
|
||||
}
|
||||
|
||||
async fn help_key(
|
||||
mounted: &mut crate::behavior::canvas_workspace::tests::Mounted,
|
||||
app: &mut App,
|
||||
event: iced::Event,
|
||||
) -> Vec<Message> {
|
||||
let (mut messages, statuses) = mounted.input_with_status(
|
||||
app,
|
||||
std::slice::from_ref(&event),
|
||||
iced::mouse::Cursor::Unavailable,
|
||||
);
|
||||
super::escape::tests::subscriptions(app, event, statuses[0], &mut messages);
|
||||
for message in messages.iter().cloned() {
|
||||
let task = app.update(message);
|
||||
mounted.perform(app, task).await;
|
||||
}
|
||||
messages
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn mounted_context_help_keyboard_scroll_preserves_inspector_focus_and_exact_caret() {
|
||||
use crate::behavior::canvas_workspace::tests::{Mounted, character, key};
|
||||
use iced::keyboard::key::Named;
|
||||
let (mut app, diagram, a, _) = architecture();
|
||||
let element = app.project().diagrams[&diagram].nodes[&a].element;
|
||||
send(&mut app, Message::Select(element));
|
||||
let before = app.document.snapshot();
|
||||
let mut mounted = Mounted::new(&mut app).await;
|
||||
let field = mounted
|
||||
.geometry(&app)
|
||||
.text_inputs
|
||||
.into_iter()
|
||||
.filter(|bounds| bounds.x > app.window_size.width - 320.)
|
||||
.min_by(|a, b| a.y.total_cmp(&b.y))
|
||||
.expect("visible Inspector Name input");
|
||||
mounted.click(&mut app, field.center()).await;
|
||||
help_key(&mut mounted, &mut app, key(Named::Home)).await;
|
||||
help_key(&mut mounted, &mut app, character("DRAFT ")).await;
|
||||
help_key(&mut mounted, &mut app, key(Named::ArrowLeft)).await;
|
||||
assert_eq!(
|
||||
app.draft.as_ref().unwrap().inputs["/name"].raw,
|
||||
"DRAFT Send"
|
||||
);
|
||||
let camera = (app.zoom, app.pan);
|
||||
help_key(&mut mounted, &mut app, key(Named::F1)).await;
|
||||
assert!(app.context_help);
|
||||
let article = iced::advanced::widget::Id::new(crate::guide::ARTICLE_SCROLL_ID);
|
||||
let geometry = mounted.geometry(&app);
|
||||
let background_scrolls = geometry
|
||||
.scrolls
|
||||
.iter()
|
||||
.filter(|(id, _)| **id != article)
|
||||
.map(|(id, (_, offset))| (id.clone(), *offset))
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(geometry.scrolls[&article].1.y, 0.);
|
||||
|
||||
help_key(&mut mounted, &mut app, key(Named::PageDown)).await;
|
||||
let down = mounted.geometry(&app).scrolls[&article].1.y;
|
||||
assert!(down > 0., "PageDown scrolls the mounted Help article");
|
||||
help_key(&mut mounted, &mut app, key(Named::End)).await;
|
||||
let end = mounted.geometry(&app).scrolls[&article].1.y;
|
||||
assert!(end > down);
|
||||
help_key(&mut mounted, &mut app, key(Named::PageDown)).await;
|
||||
assert_eq!(mounted.geometry(&app).scrolls[&article].1.y, end);
|
||||
help_key(&mut mounted, &mut app, key(Named::PageUp)).await;
|
||||
assert!(mounted.geometry(&app).scrolls[&article].1.y < end);
|
||||
help_key(&mut mounted, &mut app, key(Named::Home)).await;
|
||||
assert_eq!(mounted.geometry(&app).scrolls[&article].1.y, 0.);
|
||||
help_key(&mut mounted, &mut app, key(Named::ArrowDown)).await;
|
||||
assert_eq!(mounted.geometry(&app).scrolls[&article].1.y, 40.);
|
||||
help_key(&mut mounted, &mut app, key(Named::ArrowUp)).await;
|
||||
help_key(&mut mounted, &mut app, key(Named::PageUp)).await;
|
||||
let geometry = mounted.geometry(&app);
|
||||
assert_eq!(geometry.scrolls[&article].1.y, 0.);
|
||||
for (id, offset) in background_scrolls {
|
||||
assert_eq!(
|
||||
geometry.scrolls[&id].1, offset,
|
||||
"Help only scrolls its own article"
|
||||
);
|
||||
}
|
||||
help_key(&mut mounted, &mut app, character("IGNORED")).await;
|
||||
let messages = mounted
|
||||
.batch(
|
||||
&mut app,
|
||||
&[
|
||||
iced::Event::InputMethod(iced::advanced::input_method::Event::Preedit(
|
||||
"雨".into(),
|
||||
None,
|
||||
)),
|
||||
iced::Event::InputMethod(iced::advanced::input_method::Event::Commit("雨".into())),
|
||||
],
|
||||
iced::mouse::Cursor::Unavailable,
|
||||
)
|
||||
.await;
|
||||
assert!(!messages.iter().any(|m| matches!(m, Message::Field(..))));
|
||||
assert_eq!(
|
||||
app.draft.as_ref().unwrap().inputs["/name"].raw,
|
||||
"DRAFT Send"
|
||||
);
|
||||
help_key(&mut mounted, &mut app, key(Named::Escape)).await;
|
||||
assert!(!app.context_help);
|
||||
help_key(&mut mounted, &mut app, character("X")).await;
|
||||
assert_eq!(
|
||||
app.draft.as_ref().unwrap().inputs["/name"].raw,
|
||||
"DRAFTX Send",
|
||||
"Return keeps the actual mid-string caret without refocusing a guessed field"
|
||||
);
|
||||
assert_eq!((app.zoom, app.pan), camera);
|
||||
assert!(Arc::ptr_eq(&before, &app.document.snapshot()));
|
||||
let messages = help_key(&mut mounted, &mut app, key(Named::PageDown)).await;
|
||||
assert!(
|
||||
!messages
|
||||
.iter()
|
||||
.any(|m| matches!(m, Message::ContextHelpScroll(_)))
|
||||
);
|
||||
assert!(
|
||||
iced_runtime::task::into_stream(
|
||||
app.update(Message::ContextHelpScroll(crate::guide::ContextScroll::End,))
|
||||
)
|
||||
.is_none(),
|
||||
"A late Help scroll cannot operate after closing"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn mounted_context_help_observes_modifier_release_before_plain_canvas_click() {
|
||||
use crate::behavior::canvas_workspace::tests::{Mounted, key};
|
||||
use iced::{
|
||||
keyboard::{Event, Modifiers, key::Named},
|
||||
mouse,
|
||||
};
|
||||
let (mut app, diagram, a, b) = architecture();
|
||||
choose(&mut app, a, multiselect::Choice::Replace);
|
||||
app.window_size = iced::Size::new(2000., 1400.);
|
||||
let mut mounted = Mounted::new(&mut app).await;
|
||||
app.zoom = 1.;
|
||||
app.pan = iced::Vector::ZERO;
|
||||
app.fit_to_view = false;
|
||||
mounted.settle(&mut app).await;
|
||||
mounted
|
||||
.batch(
|
||||
&mut app,
|
||||
&[iced::Event::Keyboard(Event::ModifiersChanged(
|
||||
Modifiers::SHIFT,
|
||||
))],
|
||||
mouse::Cursor::Unavailable,
|
||||
)
|
||||
.await;
|
||||
help_key(&mut mounted, &mut app, key(Named::F1)).await;
|
||||
assert!(app.context_help);
|
||||
mounted
|
||||
.batch(
|
||||
&mut app,
|
||||
&[iced::Event::Keyboard(Event::ModifiersChanged(
|
||||
Modifiers::empty(),
|
||||
))],
|
||||
mouse::Cursor::Unavailable,
|
||||
)
|
||||
.await;
|
||||
help_key(&mut mounted, &mut app, key(Named::Escape)).await;
|
||||
let bounds = mounted
|
||||
.geometry(&app)
|
||||
.bounds(crate::allocation_input::CANVAS_ID);
|
||||
let node = projection::project_view(app.project(), diagram)
|
||||
.nodes
|
||||
.into_iter()
|
||||
.find(|node| node.id == b)
|
||||
.unwrap();
|
||||
let at = iced::Point::new(
|
||||
bounds.x + node.position.x + 40.,
|
||||
bounds.y + node.position.y + 35.,
|
||||
);
|
||||
mounted.click(&mut app, at).await;
|
||||
assert_eq!(
|
||||
app.multiselect_snapshot().unwrap().targets(),
|
||||
&[Target::Placement(b)],
|
||||
"Shift released during Help must not add the next card to the previous selection"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn mounted_context_help_retires_held_pan_without_replaying_its_release() {
|
||||
use crate::behavior::canvas_workspace::tests::{Mounted, key};
|
||||
use iced::{keyboard::key::Named, mouse};
|
||||
let (mut app, _, _, _) = architecture();
|
||||
let mut mounted = Mounted::new(&mut app).await;
|
||||
let canvas = mounted
|
||||
.geometry(&app)
|
||||
.bounds(crate::allocation_input::CANVAS_ID);
|
||||
let start = canvas.center();
|
||||
let moved = start + iced::Vector::new(20., 10.);
|
||||
mounted
|
||||
.batch(
|
||||
&mut app,
|
||||
&[
|
||||
iced::Event::Mouse(mouse::Event::CursorMoved { position: start }),
|
||||
iced::Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Middle)),
|
||||
iced::Event::Mouse(mouse::Event::CursorMoved { position: moved }),
|
||||
],
|
||||
mouse::Cursor::Available(moved),
|
||||
)
|
||||
.await;
|
||||
let camera = (app.zoom, app.pan);
|
||||
let before = app.document.snapshot();
|
||||
help_key(&mut mounted, &mut app, key(Named::F1)).await;
|
||||
assert!(app.context_help);
|
||||
mounted
|
||||
.batch(
|
||||
&mut app,
|
||||
&[iced::Event::Mouse(mouse::Event::ButtonReleased(
|
||||
mouse::Button::Middle,
|
||||
))],
|
||||
mouse::Cursor::Unavailable,
|
||||
)
|
||||
.await;
|
||||
help_key(&mut mounted, &mut app, key(Named::Escape)).await;
|
||||
let next = moved + iced::Vector::new(90., 50.);
|
||||
let messages = mounted
|
||||
.batch(
|
||||
&mut app,
|
||||
&[iced::Event::Mouse(mouse::Event::CursorMoved {
|
||||
position: next,
|
||||
})],
|
||||
mouse::Cursor::Available(next),
|
||||
)
|
||||
.await;
|
||||
assert!(!messages.iter().any(|m| matches!(m, Message::Pan(_))));
|
||||
assert_eq!(
|
||||
(app.zoom, app.pan),
|
||||
camera,
|
||||
"Moving without a held button cannot resume the pre-Help pan"
|
||||
);
|
||||
assert!(Arc::ptr_eq(&before, &app.document.snapshot()));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn context_help_barrier_retires_text_selection_drag_without_changing_the_caret() {
|
||||
use iced::advanced::{Layout, layout, widget};
|
||||
use iced::{Element, Event, Rectangle, mouse};
|
||||
type InputState = iced::widget::text_input::State<
|
||||
<iced::Renderer as iced::advanced::text::Renderer>::Paragraph,
|
||||
>;
|
||||
fn input(open: bool) -> Element<'static, Message> {
|
||||
crate::guide::protect_workspace(
|
||||
open,
|
||||
iced::widget::text_input("Name", "A long retained property name")
|
||||
.on_input(|value| Message::Field("/name".into(), value))
|
||||
.width(400)
|
||||
.padding(8)
|
||||
.into(),
|
||||
)
|
||||
}
|
||||
fn dispatch(
|
||||
root: &mut Element<'_, Message>,
|
||||
tree: &mut widget::Tree,
|
||||
node: &layout::Node,
|
||||
renderer: &iced::Renderer,
|
||||
event: Event,
|
||||
cursor: mouse::Cursor,
|
||||
) -> Vec<Message> {
|
||||
let mut messages = Vec::new();
|
||||
root.as_widget_mut().update(
|
||||
tree,
|
||||
&event,
|
||||
Layout::new(node),
|
||||
cursor,
|
||||
renderer,
|
||||
&mut iced::advanced::clipboard::Null,
|
||||
&mut iced::advanced::Shell::new(&mut messages),
|
||||
&Rectangle::new(iced::Point::ORIGIN, iced::Size::new(400., 60.)),
|
||||
);
|
||||
messages
|
||||
}
|
||||
let mounted = crate::behavior::canvas_workspace::tests::Mounted::unshown().await;
|
||||
let renderer = &mounted.renderer;
|
||||
let mut root = input(false);
|
||||
let mut tree = widget::Tree::new(root.as_widget());
|
||||
let node = root.as_widget_mut().layout(
|
||||
&mut tree,
|
||||
renderer,
|
||||
&layout::Limits::new(iced::Size::ZERO, iced::Size::new(400., 60.)),
|
||||
);
|
||||
let start = iced::Point::new(20., 18.);
|
||||
let end = iced::Point::new(110., 18.);
|
||||
for event in [
|
||||
mouse::Event::CursorMoved { position: start },
|
||||
mouse::Event::ButtonPressed(mouse::Button::Left),
|
||||
mouse::Event::CursorMoved { position: end },
|
||||
] {
|
||||
let cursor = if matches!(event, mouse::Event::CursorMoved { position } if position == end) {
|
||||
mouse::Cursor::Available(end)
|
||||
} else {
|
||||
mouse::Cursor::Available(start)
|
||||
};
|
||||
dispatch(
|
||||
&mut root,
|
||||
&mut tree,
|
||||
&node,
|
||||
renderer,
|
||||
Event::Mouse(event),
|
||||
cursor,
|
||||
);
|
||||
}
|
||||
let caret = tree.children[0].state.downcast_ref::<InputState>().cursor();
|
||||
assert!(
|
||||
tree.children[0]
|
||||
.state
|
||||
.downcast_ref::<InputState>()
|
||||
.is_focused()
|
||||
);
|
||||
root = input(true);
|
||||
tree.diff(root.as_widget());
|
||||
let messages = dispatch(
|
||||
&mut root,
|
||||
&mut tree,
|
||||
&node,
|
||||
renderer,
|
||||
Event::Window(iced::window::Event::RedrawRequested(
|
||||
std::time::Instant::now(),
|
||||
)),
|
||||
mouse::Cursor::Unavailable,
|
||||
);
|
||||
assert!(
|
||||
messages.is_empty(),
|
||||
"Gesture retirement discards release actions"
|
||||
);
|
||||
dispatch(
|
||||
&mut root,
|
||||
&mut tree,
|
||||
&node,
|
||||
renderer,
|
||||
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)),
|
||||
mouse::Cursor::Unavailable,
|
||||
);
|
||||
root = input(false);
|
||||
tree.diff(root.as_widget());
|
||||
let later = iced::Point::new(300., 18.);
|
||||
dispatch(
|
||||
&mut root,
|
||||
&mut tree,
|
||||
&node,
|
||||
renderer,
|
||||
Event::Mouse(mouse::Event::CursorMoved { position: later }),
|
||||
mouse::Cursor::Available(later),
|
||||
);
|
||||
let state = tree.children[0].state.downcast_ref::<InputState>();
|
||||
assert!(state.is_focused());
|
||||
assert_eq!(
|
||||
state.cursor(),
|
||||
caret,
|
||||
"An unheld pointer no longer extends the pre-Help text selection"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1497,7 +1497,9 @@ impl StateCanvas<'_> {
|
||||
format!("{} · {} · right-click to edit", r.name, routing::state_transition_summary(self.project, r))
|
||||
}
|
||||
}).unwrap_or_default(),
|
||||
_ => "Select / move · drag a vertex · drag selected transition endpoints to reconnect · middle-drag to pan".into(),
|
||||
// General gestures are available in Help; keep this stable slot
|
||||
// for the active operation or the item under the pointer.
|
||||
_ => String::new(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,6 +48,11 @@ pub enum Event {
|
||||
|
||||
pub(crate) const ARTICLE_SCROLL_ID: &str = "engineering-guide-article";
|
||||
|
||||
mod context_help;
|
||||
pub(crate) use context_help::{
|
||||
ContextScroll, context_scroll_key, context_scroll_task, protect_workspace,
|
||||
};
|
||||
|
||||
impl State {
|
||||
/// Returns whether navigation selected a different article.
|
||||
pub fn update(&mut self, event: Event) -> bool {
|
||||
|
||||
@@ -0,0 +1,282 @@
|
||||
//! Modal reading input and preservation of the mounted editing workspace.
|
||||
use super::ARTICLE_SCROLL_ID;
|
||||
use crate::app::Message;
|
||||
use iced::Element;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub(crate) enum ContextScroll {
|
||||
LineUp,
|
||||
LineDown,
|
||||
PageUp,
|
||||
PageDown,
|
||||
Home,
|
||||
End,
|
||||
}
|
||||
|
||||
pub(crate) fn context_scroll_key(
|
||||
event: iced::Event,
|
||||
status: iced::event::Status,
|
||||
_: iced::window::Id,
|
||||
) -> Option<Message> {
|
||||
use iced::keyboard::{Event, Key, key::Named};
|
||||
let iced::Event::Keyboard(Event::KeyPressed { key, modifiers, .. }) = event else {
|
||||
return None;
|
||||
};
|
||||
if status == iced::event::Status::Captured || !modifiers.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let scroll = match key {
|
||||
Key::Named(Named::ArrowUp) => ContextScroll::LineUp,
|
||||
Key::Named(Named::ArrowDown) => ContextScroll::LineDown,
|
||||
Key::Named(Named::PageUp) => ContextScroll::PageUp,
|
||||
Key::Named(Named::PageDown) => ContextScroll::PageDown,
|
||||
Key::Named(Named::Home) => ContextScroll::Home,
|
||||
Key::Named(Named::End) => ContextScroll::End,
|
||||
_ => return None,
|
||||
};
|
||||
Some(Message::ContextHelpScroll(scroll))
|
||||
}
|
||||
|
||||
pub(crate) fn context_scroll_task(scroll: ContextScroll) -> iced::Task<Message> {
|
||||
struct Scroll(ContextScroll);
|
||||
impl<T> iced::advanced::widget::Operation<T> for Scroll {
|
||||
fn traverse(
|
||||
&mut self,
|
||||
visit: &mut dyn FnMut(&mut dyn iced::advanced::widget::Operation<T>),
|
||||
) {
|
||||
visit(self);
|
||||
}
|
||||
fn scrollable(
|
||||
&mut self,
|
||||
id: Option<&iced::advanced::widget::Id>,
|
||||
bounds: iced::Rectangle,
|
||||
content_bounds: iced::Rectangle,
|
||||
_: iced::Vector,
|
||||
state: &mut dyn iced::advanced::widget::operation::Scrollable,
|
||||
) {
|
||||
use iced::advanced::widget::operation::scrollable::{AbsoluteOffset, RelativeOffset};
|
||||
if id != Some(&iced::advanced::widget::Id::new(ARTICLE_SCROLL_ID)) {
|
||||
return;
|
||||
}
|
||||
match self.0 {
|
||||
ContextScroll::Home | ContextScroll::End => state.snap_to(RelativeOffset {
|
||||
x: None,
|
||||
y: Some(if matches!(self.0, ContextScroll::End) {
|
||||
1.
|
||||
} else {
|
||||
0.
|
||||
}),
|
||||
}),
|
||||
scroll => {
|
||||
let y = match scroll {
|
||||
ContextScroll::LineUp => -40.,
|
||||
ContextScroll::LineDown => 40.,
|
||||
ContextScroll::PageUp => -bounds.height * 0.9,
|
||||
ContextScroll::PageDown => bounds.height * 0.9,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
// Iced clamps this movement to the actual content bounds.
|
||||
state.scroll_by(AbsoluteOffset { x: 0., y }, bounds, content_bounds);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
iced::advanced::widget::operate(Scroll(scroll))
|
||||
}
|
||||
|
||||
/// Keep the mounted editor and its caret, but prevent modal input from reaching
|
||||
/// widget-internal state before the App message guard can reject edits.
|
||||
pub(crate) fn protect_workspace<'a, R: iced::advanced::renderer::Renderer + 'a>(
|
||||
help_open: bool,
|
||||
content: Element<'a, Message, iced::Theme, R>,
|
||||
) -> Element<'a, Message, iced::Theme, R> {
|
||||
Element::new(WorkspaceInput { help_open, content })
|
||||
}
|
||||
|
||||
struct WorkspaceInput<'a, R> {
|
||||
help_open: bool,
|
||||
content: Element<'a, Message, iced::Theme, R>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct WorkspaceInputState {
|
||||
help_open: bool,
|
||||
}
|
||||
|
||||
impl<R: iced::advanced::renderer::Renderer> iced::advanced::Widget<Message, iced::Theme, R>
|
||||
for WorkspaceInput<'_, R>
|
||||
{
|
||||
fn tag(&self) -> iced::advanced::widget::tree::Tag {
|
||||
iced::advanced::widget::tree::Tag::of::<WorkspaceInputState>()
|
||||
}
|
||||
fn state(&self) -> iced::advanced::widget::tree::State {
|
||||
iced::advanced::widget::tree::State::new(WorkspaceInputState::default())
|
||||
}
|
||||
fn children(&self) -> Vec<iced::advanced::widget::Tree> {
|
||||
vec![iced::advanced::widget::Tree::new(self.content.as_widget())]
|
||||
}
|
||||
fn diff(&self, tree: &mut iced::advanced::widget::Tree) {
|
||||
tree.diff_children(std::slice::from_ref(&self.content));
|
||||
}
|
||||
fn size(&self) -> iced::Size<iced::Length> {
|
||||
self.content.as_widget().size()
|
||||
}
|
||||
fn size_hint(&self) -> iced::Size<iced::Length> {
|
||||
self.content.as_widget().size_hint()
|
||||
}
|
||||
fn layout(
|
||||
&mut self,
|
||||
tree: &mut iced::advanced::widget::Tree,
|
||||
renderer: &R,
|
||||
limits: &iced::advanced::layout::Limits,
|
||||
) -> iced::advanced::layout::Node {
|
||||
self.content
|
||||
.as_widget_mut()
|
||||
.layout(&mut tree.children[0], renderer, limits)
|
||||
}
|
||||
fn operate(
|
||||
&mut self,
|
||||
tree: &mut iced::advanced::widget::Tree,
|
||||
layout: iced::advanced::Layout<'_>,
|
||||
renderer: &R,
|
||||
operation: &mut dyn iced::advanced::widget::Operation,
|
||||
) {
|
||||
self.content
|
||||
.as_widget_mut()
|
||||
.operate(&mut tree.children[0], layout, renderer, operation);
|
||||
}
|
||||
fn update(
|
||||
&mut self,
|
||||
tree: &mut iced::advanced::widget::Tree,
|
||||
event: &iced::Event,
|
||||
layout: iced::advanced::Layout<'_>,
|
||||
cursor: iced::mouse::Cursor,
|
||||
renderer: &R,
|
||||
clipboard: &mut dyn iced::advanced::Clipboard,
|
||||
shell: &mut iced::advanced::Shell<'_, Message>,
|
||||
viewport: &iced::Rectangle,
|
||||
) {
|
||||
let entering = {
|
||||
let state = tree.state.downcast_mut::<WorkspaceInputState>();
|
||||
let entering = self.help_open && !state.help_open;
|
||||
state.help_open = self.help_open;
|
||||
entering
|
||||
};
|
||||
if entering {
|
||||
// Retire held gestures without executing their release actions.
|
||||
// Text controls keep their real caret/focus; drawing tools remain
|
||||
// selected, but an old drag cannot resume when Help is dismissed.
|
||||
for event in [
|
||||
iced::mouse::Event::CursorLeft,
|
||||
iced::mouse::Event::ButtonReleased(iced::mouse::Button::Left),
|
||||
iced::mouse::Event::ButtonReleased(iced::mouse::Button::Middle),
|
||||
iced::mouse::Event::ButtonReleased(iced::mouse::Button::Right),
|
||||
] {
|
||||
let mut discarded = Vec::new();
|
||||
let mut cancellation = iced::advanced::Shell::new(&mut discarded);
|
||||
self.content.as_widget_mut().update(
|
||||
&mut tree.children[0],
|
||||
&iced::Event::Mouse(event),
|
||||
layout,
|
||||
iced::mouse::Cursor::Unavailable,
|
||||
renderer,
|
||||
clipboard,
|
||||
&mut cancellation,
|
||||
viewport,
|
||||
);
|
||||
shell.request_redraw_at(cancellation.redraw_request());
|
||||
if cancellation.is_layout_invalid() {
|
||||
shell.invalidate_layout();
|
||||
}
|
||||
if cancellation.are_widgets_invalid() {
|
||||
shell.invalidate_widgets();
|
||||
}
|
||||
}
|
||||
}
|
||||
if self.help_open
|
||||
&& matches!(
|
||||
event,
|
||||
iced::Event::Keyboard(
|
||||
iced::keyboard::Event::KeyPressed { .. }
|
||||
| iced::keyboard::Event::KeyReleased { .. }
|
||||
) | iced::Event::Mouse(_)
|
||||
| iced::Event::Touch(_)
|
||||
| iced::Event::InputMethod(_)
|
||||
)
|
||||
{
|
||||
return;
|
||||
}
|
||||
// ModifiersChanged and passive window/redraw events still reach the
|
||||
// editor, so releasing Shift during Help cannot stick on return.
|
||||
self.content.as_widget_mut().update(
|
||||
&mut tree.children[0],
|
||||
event,
|
||||
layout,
|
||||
cursor,
|
||||
renderer,
|
||||
clipboard,
|
||||
shell,
|
||||
viewport,
|
||||
);
|
||||
}
|
||||
fn draw(
|
||||
&self,
|
||||
tree: &iced::advanced::widget::Tree,
|
||||
renderer: &mut R,
|
||||
theme: &iced::Theme,
|
||||
style: &iced::advanced::renderer::Style,
|
||||
layout: iced::advanced::Layout<'_>,
|
||||
cursor: iced::mouse::Cursor,
|
||||
viewport: &iced::Rectangle,
|
||||
) {
|
||||
self.content.as_widget().draw(
|
||||
&tree.children[0],
|
||||
renderer,
|
||||
theme,
|
||||
style,
|
||||
layout,
|
||||
cursor,
|
||||
viewport,
|
||||
);
|
||||
}
|
||||
fn mouse_interaction(
|
||||
&self,
|
||||
tree: &iced::advanced::widget::Tree,
|
||||
layout: iced::advanced::Layout<'_>,
|
||||
cursor: iced::mouse::Cursor,
|
||||
viewport: &iced::Rectangle,
|
||||
renderer: &R,
|
||||
) -> iced::mouse::Interaction {
|
||||
if self.help_open {
|
||||
iced::mouse::Interaction::None
|
||||
} else {
|
||||
self.content.as_widget().mouse_interaction(
|
||||
&tree.children[0],
|
||||
layout,
|
||||
cursor,
|
||||
viewport,
|
||||
renderer,
|
||||
)
|
||||
}
|
||||
}
|
||||
fn overlay<'b>(
|
||||
&'b mut self,
|
||||
tree: &'b mut iced::advanced::widget::Tree,
|
||||
layout: iced::advanced::Layout<'b>,
|
||||
renderer: &R,
|
||||
viewport: &iced::Rectangle,
|
||||
translation: iced::Vector,
|
||||
) -> Option<iced::advanced::overlay::Element<'b, Message, iced::Theme, R>> {
|
||||
if self.help_open {
|
||||
None
|
||||
} else {
|
||||
self.content.as_widget_mut().overlay(
|
||||
&mut tree.children[0],
|
||||
layout,
|
||||
renderer,
|
||||
viewport,
|
||||
translation,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -296,6 +296,7 @@ pub fn view(app: &App) -> View<'_, Message> {
|
||||
.padding([4, 16])
|
||||
.style(style::panel);
|
||||
let root = crate::allocation_input::root(app, column![header, task_nav, body, footer]);
|
||||
let root = crate::guide::protect_workspace(app.context_help, root);
|
||||
if app.context_help {
|
||||
return modal_width(
|
||||
root,
|
||||
@@ -495,7 +496,7 @@ fn diagram(app: &App) -> View<'_, Message> {
|
||||
.active_diagram
|
||||
.and_then(|id| app.project().diagrams.get(&id))
|
||||
.map(|d| d.projection);
|
||||
let modes = row![
|
||||
let mut modes = row![
|
||||
label("View content"),
|
||||
pick_list(
|
||||
[
|
||||
@@ -611,6 +612,26 @@ fn diagram(app: &App) -> View<'_, Message> {
|
||||
.center(Fill)
|
||||
.into()
|
||||
};
|
||||
if projection == Some(ProjectionKind::Allocation) {
|
||||
modes = modes.push(tooltip(
|
||||
action(
|
||||
if app.allocation_drag.arrange_next {
|
||||
"Arrange ✓"
|
||||
} else {
|
||||
"Arrange"
|
||||
},
|
||||
Message::WorkDrag(crate::app::allocation_drag::Event::ArrangeNext),
|
||||
),
|
||||
container(text(format!(
|
||||
"Arrange next drag · {}. Move work within its current allocation; Escape cancels.",
|
||||
crate::allocation_input::ARRANGE_SHORTCUT
|
||||
)))
|
||||
.padding(10)
|
||||
.max_width(420)
|
||||
.style(style::card),
|
||||
tooltip::Position::Bottom,
|
||||
));
|
||||
}
|
||||
let mut tools = column![toolbar].spacing(8);
|
||||
if app.active_diagram.is_some() {
|
||||
tools = tools.push(modes);
|
||||
@@ -645,25 +666,7 @@ fn diagram(app: &App) -> View<'_, Message> {
|
||||
.height(26)
|
||||
.width(Fill)
|
||||
.clip(true);
|
||||
tools = tools.push(
|
||||
row![
|
||||
destination_control,
|
||||
action(
|
||||
&format!(
|
||||
"{} · {}",
|
||||
if app.allocation_drag.arrange_next {
|
||||
"Arrange next drag: armed"
|
||||
} else {
|
||||
"Arrange next drag"
|
||||
},
|
||||
crate::allocation_input::ARRANGE_SHORTCUT
|
||||
),
|
||||
Message::WorkDrag(crate::app::allocation_drag::Event::ArrangeNext)
|
||||
)
|
||||
]
|
||||
.spacing(8)
|
||||
.align_y(iced::Center),
|
||||
);
|
||||
tools = tools.push(destination_control);
|
||||
}
|
||||
if let Some(diagram) = app.active_diagram {
|
||||
let view = oa_core::projection::project_view(app.project(), diagram);
|
||||
|
||||
Reference in New Issue
Block a user