mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-25 19:06:47 -06:00
fix(ui): scroll a long pages list in its own area so the layers stay in view (fixes #258)
This commit is contained in:
@@ -215,64 +215,73 @@ fn pages(app: &mut FigApp, ui: &mut Ui) {
|
||||
let mut move_to: Option<(NodeId, String)> = None;
|
||||
// Pages are dragged up and down the list to reorder them.
|
||||
let mut reorder = crate::list_drag::ListDrag::in_order(ui, "pages");
|
||||
for (k, (id, name, divider)) in pages.iter().cloned().enumerate() {
|
||||
let (r, resp) = ui.allocate_exact_size(vec2(w, 32.0), Sense::click_and_drag());
|
||||
reorder.row_by(&resp, k, r, || if divider { tr("Divider").to_string() } else { name.clone() });
|
||||
a11y::list_item(&resp, if divider { tr("Divider") } else { &name }, id == cur);
|
||||
let r = r.shrink2(vec2(8.0, 0.0));
|
||||
a11y::ring_around(ui, r, &resp);
|
||||
let renaming = app.ui.rename_page.as_ref().is_some_and(|(i, _)| *i == id);
|
||||
if divider && !renaming {
|
||||
// A divider is a line between pages: it opens nothing and takes no layers.
|
||||
ui.painter().line_segment([pos2(r.min.x + 10.0, r.center().y), pos2(r.max.x - 10.0, r.center().y)], Stroke::new(1.0, t.border));
|
||||
} else if resp.hovered() || id == cur {
|
||||
ui.painter().rect_filled(r, CornerRadius::same(5), t.hover);
|
||||
}
|
||||
if dragging.is_some()
|
||||
&& id != cur
|
||||
&& !divider
|
||||
// The gap between rows counts too.
|
||||
&& pointer.is_some_and(|p| resp.interact_rect.expand2(vec2(0.0, 3.0)).contains(p))
|
||||
{
|
||||
ui.painter().rect_stroke(r, CornerRadius::same(5), Stroke::new(2.0, ACCENT), egui::StrokeKind::Inside);
|
||||
if ui.input(|i| i.pointer.any_released()) {
|
||||
move_to = Some((id, name.clone()));
|
||||
// A long list scrolls in its own area, so the layers keep their room below it.
|
||||
let max_h = (ui.available_height() * 0.4).max(3.0 * ROW);
|
||||
let moved = egui::ScrollArea::vertical().id_salt("pages_list").max_height(max_h).show(ui, |ui| {
|
||||
for (k, (id, name, divider)) in pages.iter().cloned().enumerate() {
|
||||
let (r, resp) = ui.allocate_exact_size(vec2(w, 32.0), Sense::click_and_drag());
|
||||
reorder.row_by(&resp, k, r, || if divider { tr("Divider").to_string() } else { name.clone() });
|
||||
a11y::list_item(&resp, if divider { tr("Divider") } else { &name }, id == cur);
|
||||
let r = r.shrink2(vec2(8.0, 0.0));
|
||||
a11y::ring_around(ui, r, &resp);
|
||||
let renaming = app.ui.rename_page.as_ref().is_some_and(|(i, _)| *i == id);
|
||||
if divider && !renaming {
|
||||
// A divider is a line between pages: it opens nothing and takes no layers.
|
||||
ui.painter().line_segment([pos2(r.min.x + 10.0, r.center().y), pos2(r.max.x - 10.0, r.center().y)], Stroke::new(1.0, t.border));
|
||||
} else if resp.hovered() || id == cur {
|
||||
ui.painter().rect_filled(r, CornerRadius::same(5), t.hover);
|
||||
}
|
||||
}
|
||||
if renaming {
|
||||
let tr = Rect::from_min_max(pos2(r.min.x + 8.0, r.min.y + 4.0), pos2(r.max.x - 4.0, r.max.y - 4.0));
|
||||
if let Some(new) =
|
||||
crate::widgets::text_field_inline(ui, Id::new(("pagename", id)), &name, tr, if id == cur { semibold(11.0) } else { regular(11.0) })
|
||||
if dragging.is_some()
|
||||
&& id != cur
|
||||
&& !divider
|
||||
// The gap between rows counts too.
|
||||
&& pointer.is_some_and(|p| resp.interact_rect.expand2(vec2(0.0, 3.0)).contains(p))
|
||||
{
|
||||
rename_done = Some((id, new));
|
||||
ui.painter().rect_stroke(r, CornerRadius::same(5), Stroke::new(2.0, ACCENT), egui::StrokeKind::Inside);
|
||||
if ui.input(|i| i.pointer.any_released()) {
|
||||
move_to = Some((id, name.clone()));
|
||||
}
|
||||
}
|
||||
} else if !divider {
|
||||
let font = if id == cur { semibold(11.0) } else { regular(11.0) };
|
||||
crate::bidi::paint_name(ui.painter(), pos2(r.min.x + 10.0, r.center().y), &name, font, t.text, r.width() - 14.0);
|
||||
}
|
||||
if resp.clicked() && !divider {
|
||||
select = Some(id);
|
||||
}
|
||||
if resp.double_clicked() {
|
||||
app.ui.rename_page = Some((id, name.clone()));
|
||||
ui.memory_mut(|m| m.request_focus(Id::new(("pagename", id)).with("te")));
|
||||
}
|
||||
crate::right::context_popup(&resp).show(|ui| {
|
||||
if ui.button(tr("Rename page")).clicked() {
|
||||
if renaming {
|
||||
let tr = Rect::from_min_max(pos2(r.min.x + 8.0, r.min.y + 4.0), pos2(r.max.x - 4.0, r.max.y - 4.0));
|
||||
if let Some(new) = crate::widgets::text_field_inline(
|
||||
ui,
|
||||
Id::new(("pagename", id)),
|
||||
&name,
|
||||
tr,
|
||||
if id == cur { semibold(11.0) } else { regular(11.0) },
|
||||
) {
|
||||
rename_done = Some((id, new));
|
||||
}
|
||||
} else if !divider {
|
||||
let font = if id == cur { semibold(11.0) } else { regular(11.0) };
|
||||
crate::bidi::paint_name(ui.painter(), pos2(r.min.x + 10.0, r.center().y), &name, font, t.text, r.width() - 14.0);
|
||||
}
|
||||
if resp.clicked() && !divider {
|
||||
select = Some(id);
|
||||
}
|
||||
if resp.double_clicked() {
|
||||
app.ui.rename_page = Some((id, name.clone()));
|
||||
ui.close();
|
||||
ui.memory_mut(|m| m.request_focus(Id::new(("pagename", id)).with("te")));
|
||||
}
|
||||
if ui.button(tr("Duplicate page")).clicked() {
|
||||
app.run("page.duplicate", json!({"id": id}));
|
||||
ui.close();
|
||||
}
|
||||
if ui.button(tr("Delete page")).clicked() {
|
||||
app.run("page.delete", json!({"id": id}));
|
||||
ui.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
if let Some((from, to)) = reorder.finish(ui)
|
||||
crate::right::context_popup(&resp).show(|ui| {
|
||||
if ui.button(tr("Rename page")).clicked() {
|
||||
app.ui.rename_page = Some((id, name.clone()));
|
||||
ui.close();
|
||||
}
|
||||
if ui.button(tr("Duplicate page")).clicked() {
|
||||
app.run("page.duplicate", json!({"id": id}));
|
||||
ui.close();
|
||||
}
|
||||
if ui.button(tr("Delete page")).clicked() {
|
||||
app.run("page.delete", json!({"id": id}));
|
||||
ui.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
reorder.finish(ui)
|
||||
});
|
||||
if let Some((from, to)) = moved.inner
|
||||
&& let Some((id, ..)) = pages.get(from)
|
||||
{
|
||||
app.run("page.move", json!({"id": id, "index": to}));
|
||||
|
||||
@@ -226,3 +226,47 @@ fn pages_drag_to_reorder_and_dividers_open_nothing() {
|
||||
assert_eq!(u.app.session.page().unwrap(), p1, "clicking a divider opens nothing");
|
||||
assert!(u.app.ui.last_error.is_none(), "{:?}", u.app.ui.last_error);
|
||||
}
|
||||
|
||||
/// The page opened by a click on each spot from just under the pages header down to `to`.
|
||||
fn pages_down_to(u: &mut Ui, to: f32) -> Vec<NodeId> {
|
||||
let mut opened = Vec::new();
|
||||
for y in (190..to as i32).step_by(8) {
|
||||
click(u, Pos2::new(150.0, y as f32), Modifiers::NONE);
|
||||
opened.push(u.app.session.page().unwrap());
|
||||
}
|
||||
opened
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_long_pages_list_scrolls_and_leaves_room_for_the_layers() {
|
||||
let mut s = Session::new();
|
||||
let first = s.page().unwrap();
|
||||
let rect = id(s.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 10, "height": 10})).unwrap());
|
||||
for _ in 0..59 {
|
||||
s.execute("page.add", &json!({})).unwrap();
|
||||
}
|
||||
let pages = s.doc().unwrap().pages.clone();
|
||||
let last = *pages.last().unwrap();
|
||||
s.execute("page.select", &json!({"id": first})).unwrap();
|
||||
s.execute("select.none", &json!({})).unwrap();
|
||||
let mut u = Ui::new(s);
|
||||
// The layers list stays on screen under 60 pages.
|
||||
let layer = u.row_at(rect);
|
||||
// The wheel over the pages list scrolls it to its last page.
|
||||
let wheel = Event::MouseWheel {
|
||||
unit: egui::MouseWheelUnit::Point,
|
||||
delta: egui::vec2(0.0, -400.0),
|
||||
phase: egui::TouchPhase::Move,
|
||||
modifiers: Default::default(),
|
||||
};
|
||||
for _ in 0..10 {
|
||||
u.frame(vec![Event::PointerMoved(Pos2::new(150.0, 200.0)), wheel.clone()]);
|
||||
}
|
||||
for _ in 0..30 {
|
||||
u.frame(vec![]);
|
||||
}
|
||||
let opened = pages_down_to(&mut u, layer.y - 60.0);
|
||||
assert!(opened.contains(&last), "the last page can be reached");
|
||||
let top = pages.iter().position(|p| Some(p) == opened.first()).unwrap();
|
||||
assert!(top > 30, "the list scrolled past its first pages (top row: page {top})");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user