mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-13 09:39:52 +00:00
Learn: a located tool's toolbar comes to the front and a tall flyout scrolls
- A Learn link that locates a tool on a toolbar made with New Toolbar brings that toolbar to the front once before its button is marked, so another floating toolbar no longer covers the mark. A group torn off into a strip still brings the strip to the front instead. - A docked flyout whose rows are taller than the window scrolls them, and a Learn link scrolls the located row into view before marking it. The mark is clipped to the rows' view.
This commit is contained in:
@@ -601,6 +601,17 @@ pub(crate) fn show(app: &mut VectorcraftApp, ctx: &egui::Context) {
|
||||
// A Learn link locating a tool of this toolbar: its button, scrolled into view once.
|
||||
let locate = crate::panels::learn::locate::wanted(&app.ui, Some(&bar.name));
|
||||
let scrolled = app.ui.learn_locate.as_ref().is_some_and(|l| l.scrolled);
|
||||
// The toolbar comes to the front once, over other floating toolbars, unless the tool's
|
||||
// group is torn off into a strip, which comes to the front instead.
|
||||
let holder: Option<Vec<&'static str>> = locate
|
||||
.and_then(|tool| bar.slots.iter().find(|s| s.iter().any(|t| t == tool)))
|
||||
.map(|slot| slot.iter().filter_map(|id| tool_info(id).map(|t| t.id)).collect());
|
||||
let torn_off = holder
|
||||
.as_ref()
|
||||
.is_some_and(|tools| tools.len() > 1 && tools.first().is_some_and(|key| crate::toolbar::is_floating(app, Some(&bar.name), key)));
|
||||
if holder.is_some() && !torn_off && crate::panels::learn::locate::raise_once(&mut app.ui) {
|
||||
ctx.move_to_top(egui::LayerId::new(egui::Order::Middle, id));
|
||||
}
|
||||
let (mut found, mut did_scroll) = (None, false);
|
||||
egui::Area::new(id).order(egui::Order::Middle).fixed_pos(pos).show(ctx, |ui| {
|
||||
crate::toolbar::flyout_frame(&t, false).inner_margin(egui::Margin { left: 0, right: 0, top: 0, bottom: 4 }).show(ui, |ui| {
|
||||
|
||||
@@ -42,6 +42,8 @@ pub struct Locate {
|
||||
pub(crate) scrolled: bool,
|
||||
/// The group's flyout was opened or its strip raised.
|
||||
pub(crate) revealed: bool,
|
||||
/// The toolbar made with New Toolbar that holds the tool was brought to the front.
|
||||
raised: bool,
|
||||
/// The mark drew in this pass.
|
||||
drawn: bool,
|
||||
/// Passes in a row in which the mark did not draw.
|
||||
@@ -76,6 +78,17 @@ pub(crate) fn reveal_once(ui: &mut UiState) -> bool {
|
||||
ui.learn_locate.as_mut().is_some_and(|l| !std::mem::replace(&mut l.revealed, true))
|
||||
}
|
||||
|
||||
/// Whether the toolbar made with New Toolbar that holds the tool is still to be brought to the
|
||||
/// front; true once.
|
||||
pub(crate) fn raise_once(ui: &mut UiState) -> bool {
|
||||
ui.learn_locate.as_mut().is_some_and(|l| !std::mem::replace(&mut l.raised, true))
|
||||
}
|
||||
|
||||
/// Whether a locate is under way and its mark has not drawn yet.
|
||||
pub(crate) fn unmarked(ui: &UiState) -> bool {
|
||||
ui.learn_locate.as_ref().is_some_and(|l| l.since.is_none())
|
||||
}
|
||||
|
||||
/// Mark `rect`, the located tool's control as drawn this pass on `layer`, clipped to `clip` and the
|
||||
/// window. The first mark starts the [`LIFETIME`]; once it has passed, the locate ends.
|
||||
pub(crate) fn mark(app: &mut VectorcraftApp, ctx: &Context, layer: LayerId, clip: Rect, rect: Rect) {
|
||||
@@ -129,7 +142,7 @@ fn start(app: &mut VectorcraftApp, tool: &'static str, target: Target) {
|
||||
app.ui.tools_drawer = false;
|
||||
}
|
||||
}
|
||||
app.ui.learn_locate = Some(Locate { tool, target, since: None, scrolled: false, revealed: false, drawn: false, unseen: 0 });
|
||||
app.ui.learn_locate = Some(Locate { tool, target, since: None, scrolled: false, revealed: false, raised: false, drawn: false, unseen: 0 });
|
||||
}
|
||||
|
||||
/// `learn.locateTool {tool}`: mark where `tool` is in the current layout →
|
||||
|
||||
@@ -747,14 +747,27 @@ struct FlyoutInput {
|
||||
close: bool,
|
||||
/// Where the tool a Learn link locates is drawn.
|
||||
located: Option<egui::Rect>,
|
||||
/// What a docked flyout's rows are clipped to (they scroll in a group taller than the window).
|
||||
located_clip: Option<egui::Rect>,
|
||||
}
|
||||
|
||||
/// A flyout's tool rows (a floating flyout's strip of tool buttons) with the grab bar down their
|
||||
/// right side; a floating flyout's bar has a × at its top. The bar's id is the group's, so a drag
|
||||
/// that tears a flyout off carries on moving the floating flyout it becomes (`toolbar`: the toolbar
|
||||
/// made with New Toolbar the group is in, none for the Tools panel's; `locate`: the tool a Learn
|
||||
/// link locates, whose place it returns).
|
||||
fn flyout_body(ui: &mut Ui, t: &Tokens, active: &str, tools: &[String], floating: bool, toolbar: Option<&str>, locate: Option<&str>) -> FlyoutInput {
|
||||
/// link locates, whose place it returns; `reveal_row`: a located row outside the flyout's view
|
||||
/// scrolls into it, and is returned once it shows).
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn flyout_body(
|
||||
ui: &mut Ui,
|
||||
t: &Tokens,
|
||||
active: &str,
|
||||
tools: &[String],
|
||||
floating: bool,
|
||||
toolbar: Option<&str>,
|
||||
locate: Option<&str>,
|
||||
reveal_row: bool,
|
||||
) -> FlyoutInput {
|
||||
let mut out = FlyoutInput::default();
|
||||
let key = tools.first().map_or("", String::as_str);
|
||||
let bar_id = bar_id(toolbar, key);
|
||||
@@ -763,7 +776,29 @@ fn flyout_body(ui: &mut Ui, t: &Tokens, active: &str, tools: &[String], floating
|
||||
let rows = if floating {
|
||||
ui.horizontal(|ui| tool_strip(ui, t, active, tools, locate))
|
||||
} else {
|
||||
ui.vertical(|ui| tool_rows(ui, t, active, tools, locate))
|
||||
// A group taller than the window scrolls its rows (inside the frame's 1 pt border). The
|
||||
// rows' height is given, since the row this sits in starts one widget tall.
|
||||
let max = ui.ctx().content_rect().height() - 2.0;
|
||||
let height = (tools.iter().filter(|id| tool_info(id).is_some()).count() as f32 * ROW_HEIGHT).min(max);
|
||||
ui.vertical(|ui| {
|
||||
egui::ScrollArea::vertical()
|
||||
.id_salt(bar_id.with("rows"))
|
||||
.max_height(max)
|
||||
.min_scrolled_height(height)
|
||||
.show(ui, |ui| {
|
||||
let (chosen, located) = tool_rows(ui, t, active, tools, locate);
|
||||
// The mark is clipped to the rows' view, not to the whole flyout.
|
||||
out.located_clip = Some(ui.clip_rect());
|
||||
match located.filter(|r| reveal_row && !ui.clip_rect().contains_rect(*r)) {
|
||||
Some(r) => {
|
||||
ui.scroll_to_rect_animation(r, Some(egui::Align::Center), egui::style::ScrollAnimation::none());
|
||||
(chosen, None)
|
||||
}
|
||||
None => (chosen, located),
|
||||
}
|
||||
})
|
||||
.inner
|
||||
})
|
||||
};
|
||||
(out.chosen, out.located) = rows.inner;
|
||||
let (bar, _) = ui.allocate_exact_size(vec2(BAR, rows.response.rect.height()), Sense::hover());
|
||||
@@ -810,7 +845,7 @@ fn tool_rows(ui: &mut Ui, t: &Tokens, active: &str, tools: &[String], locate: Op
|
||||
let w = rows_width(rows.iter().map(|(_, text)| text));
|
||||
for (tool, (name, shortcut)) in rows {
|
||||
let is_active = active == tool.id;
|
||||
let (r, resp) = ui.allocate_exact_size(vec2(w, 30.0), Sense::click());
|
||||
let (r, resp) = ui.allocate_exact_size(vec2(w, ROW_HEIGHT), Sense::click());
|
||||
if resp.hovered() {
|
||||
ui.painter().rect_filled(r, 0.0, t.hover);
|
||||
}
|
||||
@@ -843,6 +878,8 @@ type RowText = (Arc<egui::Galley>, Option<Arc<egui::Galley>>);
|
||||
const NAME_X: f32 = 52.0;
|
||||
const NAME_GAP: f32 = 14.0;
|
||||
const ROW_END: f32 = 18.0;
|
||||
/// The height of a flyout row.
|
||||
const ROW_HEIGHT: f32 = 30.0;
|
||||
|
||||
/// The name of `tool` in `lang` and its shortcut in brackets, as a flyout row draws them.
|
||||
fn row_text(ui: &Ui, lang: crate::i18n::Lang, tool: &ToolInfo, color: Color32) -> RowText {
|
||||
@@ -947,18 +984,20 @@ fn flyout(app: &mut VectorcraftApp, ctx: &egui::Context) {
|
||||
let active = app.session.tool_id();
|
||||
let toolbar = ctx.data(|d| d.get_temp::<Option<String>>(flyout_toolbar_id())).flatten();
|
||||
let locate = learn_locate::wanted(&app.ui, toolbar.as_deref()).filter(|tool| tools.iter().any(|id| id == tool));
|
||||
let reveal_row = learn_locate::unmarked(&app.ui);
|
||||
let mut input = FlyoutInput::default();
|
||||
// Beside the tool, on the side with room for it: toward the canvas from a toolbar docked right.
|
||||
let size = ctx.memory(|m| m.area_rect(egui::Id::unique("tool-flyout"))).map_or(vec2(160.0, 0.0), |r| r.size());
|
||||
let x = beside(anchor, size.x, 8.0, room(app, ctx));
|
||||
// The area stays inside the window (egui's `Area::constrain`, on by default): a flyout opened
|
||||
// near the bottom moves up, so every row, a located one included, shows.
|
||||
// near the bottom moves up, and a group taller than the window scrolls a located row into view.
|
||||
let resp =
|
||||
egui::Area::new(egui::Id::unique("tool-flyout")).order(egui::Order::Foreground).fixed_pos(pos2(x, anchor.top() - 1.0)).show(ctx, |ui| {
|
||||
flyout_frame(&t, false).show(ui, |ui| input = flyout_body(ui, &t, active, &tools, false, toolbar.as_deref(), locate));
|
||||
flyout_frame(&t, false).show(ui, |ui| input = flyout_body(ui, &t, active, &tools, false, toolbar.as_deref(), locate, reveal_row));
|
||||
});
|
||||
if let Some(rect) = input.located {
|
||||
learn_locate::mark(app, ctx, resp.response.layer_id, resp.response.rect, rect);
|
||||
let clip = input.located_clip.map_or(resp.response.rect, |c| c.intersect(resp.response.rect));
|
||||
learn_locate::mark(app, ctx, resp.response.layer_id, clip, rect);
|
||||
}
|
||||
// Dragging the bar tears the flyout off, and so does releasing a press over it (a click, or the
|
||||
// end of the long press that opened the flyout): it floats where the pointer is.
|
||||
@@ -1065,7 +1104,7 @@ fn floating_strips(app: &mut VectorcraftApp, ctx: &egui::Context, toolbar: Optio
|
||||
let pos = crate::tools_float::clamp(f.pos, size, screen);
|
||||
let mut input = FlyoutInput::default();
|
||||
let area = egui::Area::new(id).order(egui::Order::Middle).fixed_pos(pos).show(ctx, |ui| {
|
||||
flyout_frame(&t, flash).show(ui, |ui| input = flyout_body(ui, &t, active, &f.tools, true, toolbar, locate));
|
||||
flyout_frame(&t, flash).show(ui, |ui| input = flyout_body(ui, &t, active, &f.tools, true, toolbar, locate, false));
|
||||
});
|
||||
if let Some(rect) = input.located {
|
||||
located = Some((area.response.layer_id, area.response.rect, rect));
|
||||
|
||||
@@ -1301,6 +1301,77 @@ fn a_learn_link_in_a_short_window_marks_a_flyout_row_inside_the_window() {
|
||||
assert_marks(&marks, row, tool);
|
||||
}
|
||||
|
||||
/// A group with more rows than the window holds scrolls its flyout, and a Learn link scrolls the
|
||||
/// located row into view before marking it.
|
||||
#[test]
|
||||
fn a_learn_link_scrolls_the_row_of_a_group_taller_than_the_window_into_view() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
let tools: Vec<&'static str> = slots(&app).into_iter().filter_map(|(_, s)| s.first().copied()).skip(1).take(16).collect();
|
||||
for tool in &tools {
|
||||
app.run("toolbar.addTool", json!({"tool": tool, "slot": 0, "place": "group"})).unwrap();
|
||||
}
|
||||
let tool = *tools.last().expect("a tool");
|
||||
let group = slots(&app).into_iter().map(|(_, s)| s).find(|s| s.contains(&tool)).expect("the group");
|
||||
assert!(group.len() * 30 > 400, "{} rows of 30 points do not fit in 400", group.len());
|
||||
let ctx = reader_context();
|
||||
locate_frame(&mut app, &ctx, 0.0, vec![], 400.0);
|
||||
assert_eq!(app.run("learn.locateTool", json!({"tool": tool})).unwrap()["location"], "flyout", "{tool}");
|
||||
let mut drawn = (vec![], vec![]);
|
||||
for k in 1..20 {
|
||||
drawn = locate_frame(&mut app, &ctx, f64::from(k) * 0.1, vec![], 400.0);
|
||||
}
|
||||
let (marks, nodes) = drawn;
|
||||
let row = tool_rect(&nodes, tool);
|
||||
assert!(row.top() >= 0.0 && row.bottom() <= 400.0, "{tool}: the row is inside the window: {row:?}");
|
||||
assert_marks(&marks, row, tool);
|
||||
assert_eq!(app.session.tool_id(), "selection");
|
||||
// The mark is clipped to the rows' view inside the flyout's 1 pt border, so a row scrolled
|
||||
// part way out shows no mark over the border above or below the rows.
|
||||
let view = ctx.memory(|m| m.area_rect(egui::Id::unique("tool-flyout"))).expect("the flyout shows").shrink(1.0);
|
||||
let screen_rect = Some(egui::Rect::from_min_size(Pos2::ZERO, vec2(400.0, 400.0)));
|
||||
let out = ctx.run_ui(egui::RawInput { time: Some(2.0), screen_rect, ..Default::default() }, |ui| show(&mut app, ui));
|
||||
let color = Tokens::get(&ctx).selection;
|
||||
let clips: Vec<egui::Rect> = out
|
||||
.shapes
|
||||
.iter()
|
||||
.filter(|c| matches!(&c.shape, egui::Shape::Rect(r) if r.stroke.width == MARK_WIDTH && r.stroke.color == color))
|
||||
.map(|c| c.clip_rect)
|
||||
.collect();
|
||||
assert!(
|
||||
!clips.is_empty() && clips.iter().all(|c| c.top() >= view.top() && c.bottom() <= view.bottom()),
|
||||
"the mark is clipped to {view:?}: {clips:?}"
|
||||
);
|
||||
}
|
||||
|
||||
/// A toolbar made with New Toolbar under another floating toolbar comes to the front for a Learn
|
||||
/// link, so its marked button shows.
|
||||
#[test]
|
||||
fn a_learn_link_brings_a_covered_toolbar_made_with_new_toolbar_to_the_front() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
for (name, tool) in [("Mine", "artboard"), ("Other", "slice")] {
|
||||
app.run("window.toolbar.new", json!({"name": name})).unwrap();
|
||||
app.run("toolbar.addTool", json!({"tool": tool, "toolbar": name})).unwrap();
|
||||
}
|
||||
for bar in &mut app.ui.custom_toolbars {
|
||||
bar.pos = [200.0, 200.0];
|
||||
}
|
||||
let (mine, other) = (crate::custom_toolbars::area_id("Mine"), crate::custom_toolbars::area_id("Other"));
|
||||
let ctx = reader_context();
|
||||
locate_frame(&mut app, &ctx, 0.0, vec![], 1200.0);
|
||||
ctx.move_to_top(egui::LayerId::new(egui::Order::Middle, other));
|
||||
let (_, nodes) = locate_frame(&mut app, &ctx, 0.05, vec![], 1200.0);
|
||||
let button = tool_rect(&nodes, "artboard");
|
||||
assert_eq!(ctx.layer_id_at(button.center()).map(|l| l.id), Some(other), "Other covers the button of Mine");
|
||||
assert_eq!(
|
||||
app.run("learn.locateTool", json!({"tool": "artboard"})).unwrap(),
|
||||
json!({"tool": "artboard", "location": "toolbar", "toolbar": "Mine"})
|
||||
);
|
||||
let (marks, nodes) = locate_frame(&mut app, &ctx, 1.0, vec![], 1200.0);
|
||||
let button = tool_rect(&nodes, "artboard");
|
||||
assert_eq!(ctx.layer_id_at(button.center()).map(|l| l.id), Some(mine), "Mine comes to the front");
|
||||
assert_marks(&marks, button, "the toolbar Mine");
|
||||
}
|
||||
|
||||
/// A second Learn link moves the mark; Escape removes it.
|
||||
#[test]
|
||||
fn a_second_learn_link_replaces_the_mark_and_escape_removes_it() {
|
||||
|
||||
Reference in New Issue
Block a user