mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-28 04:16:46 -06:00
fix(ui): drag the line between pages and layers to size the pages list
The pages list took a fixed 40% of the left panel (#272). The line under it now drags to make the list taller or shorter: at least three rows, and always leaving the layers their header and three rows. A double-click puts back the default. The main window keeps the height in the preferences, as it does the panel widths, saved once the drag is over. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YQxjkNSUJhTkTfY74aNeki
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
23bdebd973
commit
afdb21bad6
@@ -606,6 +606,9 @@ pub struct Prefs {
|
||||
/// The widths of the main window's left and right panels (the interface keeps them within its
|
||||
/// limits; other windows have their own while they are open).
|
||||
pub panel_widths: (f32, f32),
|
||||
/// The height asked for the main window's pages list, dragged at its lower edge (`None`: a
|
||||
/// share of the panel; the interface keeps it within its limits).
|
||||
pub pages_height: Option<f32>,
|
||||
/// The interface language: a language code ("en", "es", "pt-BR") or "system" (the OS or browser
|
||||
/// language). Languages without a catalog show English.
|
||||
pub language: String,
|
||||
@@ -666,6 +669,7 @@ impl Default for Prefs {
|
||||
show_dimensions: true,
|
||||
theme: "system".into(),
|
||||
panel_widths: (240.0, 240.0),
|
||||
pages_height: None,
|
||||
language: "system".into(),
|
||||
user_name: cmd::comment::DEFAULT_AUTHOR.into(),
|
||||
reduce_motion: false,
|
||||
|
||||
@@ -217,7 +217,7 @@ fn pages(app: &mut FigApp, ui: &mut Ui) {
|
||||
// Pages are dragged up and down the list to reorder them.
|
||||
let mut reorder = crate::list_drag::ListDrag::in_order(ui, "pages");
|
||||
// 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 max_h = pages_list_height(app.ui.pages_height, ui.available_height());
|
||||
let mut 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());
|
||||
@@ -306,7 +306,54 @@ fn pages(app: &mut FigApp, ui: &mut Ui) {
|
||||
{
|
||||
app.run("page.select", json!({"id": p}));
|
||||
}
|
||||
crate::widgets::separator(ui);
|
||||
pages_edge(app, ui, moved.inner_rect.height());
|
||||
}
|
||||
|
||||
/// The pages list's height in a panel with `avail` below the Pages header: the height asked for
|
||||
/// (by default 40% of it), at least three rows, and leaving the layers their header and three rows.
|
||||
pub(crate) fn pages_list_height(asked: Option<f32>, avail: f32) -> f32 {
|
||||
let least = 3.0 * ROW;
|
||||
let most = (avail - 40.0 - 3.0 * ROW).max(least);
|
||||
asked.filter(|h| h.is_finite()).unwrap_or(avail * 0.4).clamp(least, most)
|
||||
}
|
||||
|
||||
/// The line between the pages and the layers: dragging it sizes the pages list (`shown`: its
|
||||
/// height now), a double-click puts back the default. The main window keeps it in the preferences.
|
||||
fn pages_edge(app: &mut FigApp, ui: &mut Ui, shown: f32) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let w = ui.available_width();
|
||||
let (line, _) = ui.allocate_exact_size(vec2(w, 1.0), Sense::hover());
|
||||
let grab = ui.style().interaction.resize_grab_radius_side;
|
||||
let id = Id::new("pages_edge");
|
||||
let resp = ui.interact(line.expand2(vec2(0.0, grab)), id, Sense::click_and_drag());
|
||||
let active = resp.hovered() || resp.dragged();
|
||||
if active {
|
||||
ui.ctx().set_cursor_icon(egui::CursorIcon::ResizeVertical);
|
||||
}
|
||||
let (width, colour) = if active { (2.0, t.accent) } else { (1.0, t.border) };
|
||||
ui.painter().line_segment([line.left_center(), line.right_center()], Stroke::new(width, colour));
|
||||
let asked = if resp.double_clicked() {
|
||||
Some(None)
|
||||
} else if resp.dragged() {
|
||||
// The edge stays under the pointer where it was grabbed.
|
||||
if resp.drag_started() {
|
||||
ui.data_mut(|d| d.insert_temp(id, shown));
|
||||
}
|
||||
let from: f32 = ui.data(|d| d.get_temp(id)).unwrap_or(shown);
|
||||
let moved = ui.input(|i| Some(i.pointer.latest_pos()?.y - i.pointer.press_origin()?.y)).unwrap_or(0.0);
|
||||
Some(Some(from + moved))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Some(h) = asked
|
||||
&& h != app.ui.pages_height
|
||||
{
|
||||
app.ui.pages_height = h;
|
||||
if app.windows.current == 0 {
|
||||
app.session.prefs.pages_height = h;
|
||||
app.panels_unsaved = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A page rename just asked for by a double click: its field takes the focus when drawn (temp data).
|
||||
|
||||
@@ -331,3 +331,74 @@ fn double_clicking_a_page_renames_it_with_the_focus_in_the_screen_reader_tree()
|
||||
assert_eq!(u.app.ui.rename_page.as_ref().map(|r| r.0), Some(p1));
|
||||
assert!(u.app.ui.last_error.is_none(), "{:?}", u.app.ui.last_error);
|
||||
}
|
||||
|
||||
/// The y of the line between the pages and the layers, above the layers' row at `layer` (found by
|
||||
/// the resize cursor it shows when hovered).
|
||||
fn pages_edge_at(u: &mut Ui, layer: f32) -> f32 {
|
||||
let hits: Vec<f32> = ((layer - 120.0) as i32..layer as i32)
|
||||
.map(|y| y as f32)
|
||||
.filter(|y| u.frame_output(vec![Event::PointerMoved(Pos2::new(150.0, *y))]).platform_output.cursor_icon == egui::CursorIcon::ResizeVertical)
|
||||
.collect();
|
||||
assert!(!hits.is_empty(), "no edge above {layer}");
|
||||
(hits[0] + hits[hits.len() - 1]) / 2.0
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_line_between_pages_and_layers_drags_to_size_the_pages_list() {
|
||||
let mut s = Session::new();
|
||||
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 first = s.doc().unwrap().pages[0];
|
||||
s.execute("page.select", &json!({"id": first})).unwrap();
|
||||
s.execute("select.none", &json!({})).unwrap();
|
||||
let mut u = Ui::new(s);
|
||||
let before = u.row_at(rect).y;
|
||||
let edge = pages_edge_at(&mut u, before);
|
||||
// A drag down by 150 px makes the pages list taller and moves the layers down with it.
|
||||
let at = |dy: f32| Pos2::new(150.0, edge + dy);
|
||||
let button = |p, pressed| Event::PointerButton { pos: p, button: egui::PointerButton::Primary, pressed, modifiers: Modifiers::NONE };
|
||||
u.frame(vec![Event::PointerMoved(at(0.0))]);
|
||||
u.frame(vec![button(at(0.0), true)]);
|
||||
for k in 1..=15 {
|
||||
u.frame(vec![Event::PointerMoved(at(10.0 * k as f32))]);
|
||||
}
|
||||
u.frame(vec![button(at(150.0), false)]);
|
||||
for _ in 0..30 {
|
||||
u.frame(vec![]);
|
||||
}
|
||||
let after = u.row_at(rect).y;
|
||||
assert!((after - before - 150.0).abs() < 3.0, "the layers moved from {before} to {after}");
|
||||
// The height is kept for the next start.
|
||||
let kept = u.app.session.prefs.pages_height.expect("kept in the preferences");
|
||||
let mut next = Session::new();
|
||||
next.prefs = u.app.session.prefs.clone();
|
||||
assert_eq!(crate::FigApp::new(next, Box::new(crate::NoServices)).ui.pages_height, Some(kept));
|
||||
// A double-click puts back the default.
|
||||
let edge = pages_edge_at(&mut u, after);
|
||||
let p = Pos2::new(150.0, edge);
|
||||
u.frame(vec![Event::PointerMoved(p)]);
|
||||
for _ in 0..2 {
|
||||
u.frame(vec![button(p, true)]);
|
||||
u.frame(vec![button(p, false)]);
|
||||
}
|
||||
for _ in 0..30 {
|
||||
u.frame(vec![]);
|
||||
}
|
||||
assert_eq!(u.app.session.prefs.pages_height, None);
|
||||
assert!((u.row_at(rect).y - before).abs() < 3.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_pages_list_keeps_three_rows_and_leaves_the_layers_room() {
|
||||
use crate::layers::{ROW, pages_list_height};
|
||||
// The default: 40% of the panel.
|
||||
assert_eq!(pages_list_height(None, 800.0), 320.0);
|
||||
// Dragged short or tall, it stays within three rows and the room the layers need.
|
||||
assert_eq!(pages_list_height(Some(0.0), 800.0), 3.0 * ROW);
|
||||
assert_eq!(pages_list_height(Some(5000.0), 800.0), 800.0 - 40.0 - 3.0 * ROW);
|
||||
assert_eq!(pages_list_height(Some(f32::NAN), 800.0), 320.0);
|
||||
// A panel too short for both keeps the three rows.
|
||||
assert_eq!(pages_list_height(Some(500.0), 100.0), 3.0 * ROW);
|
||||
}
|
||||
|
||||
@@ -398,6 +398,8 @@ pub struct UiState {
|
||||
pub last_right_tab: RightTab,
|
||||
pub left_tab: LeftTab,
|
||||
pub pages_open: bool,
|
||||
/// The height asked for the pages list (`None`: a share of the panel).
|
||||
pub pages_height: Option<f32>,
|
||||
pub expanded: BTreeSet<NodeId>,
|
||||
/// The layer being renamed in the layers panel.
|
||||
pub rename: Option<layers::Rename>,
|
||||
@@ -528,6 +530,7 @@ impl Default for UiState {
|
||||
last_right_tab: RightTab::Design,
|
||||
left_tab: LeftTab::File,
|
||||
pages_open: true,
|
||||
pages_height: None,
|
||||
expanded: BTreeSet::new(),
|
||||
rename: None,
|
||||
rename_page: None,
|
||||
@@ -646,8 +649,9 @@ impl FigApp {
|
||||
pub fn new(session: Session, services: Box<dyn Services>) -> Self {
|
||||
// The main window's panels open as wide as they were left.
|
||||
let (left_width, right_width) = session.prefs.panel_widths;
|
||||
let pages_height = session.prefs.pages_height;
|
||||
Self {
|
||||
ui: UiState { left_width, right_width, ..UiState::default() },
|
||||
ui: UiState { left_width, right_width, pages_height, ..UiState::default() },
|
||||
session,
|
||||
canvas: canvas::CanvasState::default(),
|
||||
services,
|
||||
|
||||
@@ -208,13 +208,15 @@ pub fn open(app: &mut FigApp, ctx: &Context, index: Option<usize>, path: Option<
|
||||
docs: vec![uid],
|
||||
active: Some(uid),
|
||||
pane: Pane {
|
||||
// A new window opens like the one it comes from: the same tabs and panel widths.
|
||||
// A new window opens like the one it comes from: the same tabs, panel widths and pages
|
||||
// list height.
|
||||
ui: UiState {
|
||||
title_bar: app.ui.title_bar,
|
||||
right_tab: app.ui.right_tab,
|
||||
left_tab: app.ui.left_tab,
|
||||
left_width: app.ui.left_width,
|
||||
right_width: app.ui.right_width,
|
||||
pages_height: app.ui.pages_height,
|
||||
..UiState::default()
|
||||
},
|
||||
..Pane::default()
|
||||
|
||||
Reference in New Issue
Block a user