mirror of
https://github.com/storytold/effectcraft.git
synced 2026-11-06 18:41:26 +00:00
Project panel: the Name column fits long names and resizes (#3094)
* Project panel: the Name column fits long names and resizes The Name column was 40 % of the panel and at most 260 points wide, with no way to resize it, so long item names (compositions of an imported After Effects project) ended in "..." even in a wide panel. It now fits the longest listed name, never narrower than before and at most the panel's width less the Label column (the optional columns keep scrolling under it). Dragging the column's edge in the header resizes it, a double-click on the edge fits the names again. The width is kept in UiState::project_name_w (None = fit the names); the edge has the automation id project.nameColumn.edge. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Project panel: the Name column leaves room for the optional columns; no name fit for a manual width Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
43a47a5d6a
commit
bb920d68ec
3 files changed
+125
-3
No files matched your search
@@ -515,8 +515,39 @@ impl Columns {
|
||||
}
|
||||
}
|
||||
|
||||
fn layout_columns(app: &mut EffectcraftApp, rect: Rect) -> Columns {
|
||||
let name_w = (rect.width() * 0.40).clamp(120.0, 260.0);
|
||||
/// Narrowest Name column (points).
|
||||
const MIN_NAME_W: f32 = 120.0;
|
||||
|
||||
/// The Name column width that shows every listed name in full: indent, twirl and icon, the
|
||||
/// name, and room for a problem marker.
|
||||
fn name_fit(app: &EffectcraftApp, p: &egui::Painter, rows: &[(ItemId, usize)]) -> f32 {
|
||||
rows.iter()
|
||||
.filter_map(|(id, depth)| {
|
||||
let it = app.session.project.item(*id)?;
|
||||
let text = p.layout_no_wrap(it.name.clone(), Tokens::ui(12.0), Color32::WHITE).size().x;
|
||||
Some(8.0 + 14.0 * *depth as f32 + 30.0 + text + 24.0)
|
||||
})
|
||||
.fold(0.0, f32::max)
|
||||
}
|
||||
|
||||
/// The part of the panel the optional columns keep next to a widest Name column (points): room
|
||||
/// for some of their content and a horizontal scroll bar track that can be dragged.
|
||||
const MIN_OPTIONAL_VIEW: f32 = 40.0;
|
||||
|
||||
/// The widest the Name column gets in a panel `width` points wide: the width less the Label
|
||||
/// column (40 points), and less [`MIN_OPTIONAL_VIEW`] while optional columns are shown.
|
||||
fn name_max_w(app: &EffectcraftApp, width: f32) -> f32 {
|
||||
let optional = if app.ui.project_columns.iter().any(|k| COLUMNS.iter().any(|c| c.0 == k)) { MIN_OPTIONAL_VIEW } else { 0.0 };
|
||||
(width - 40.0 - optional).max(MIN_NAME_W)
|
||||
}
|
||||
|
||||
/// Column positions. The Name column is as wide as the user dragged it, else wide enough for
|
||||
/// the names listed (`fit`), as After Effects shows full names; at most [`name_max_w`] (the
|
||||
/// optional columns scroll under it).
|
||||
fn layout_columns(app: &mut EffectcraftApp, rect: Rect, fit: f32) -> Columns {
|
||||
let max_w = name_max_w(app, rect.width());
|
||||
let auto = (rect.width() * 0.40).clamp(MIN_NAME_W, 260.0).max(fit);
|
||||
let name_w = app.ui.project_name_w.filter(|w| w.is_finite()).unwrap_or(auto).clamp(MIN_NAME_W, max_w);
|
||||
let label_x = rect.min.x + name_w + 14.0;
|
||||
let mut cols: Vec<(&'static str, &'static str, f32, f32)> = vec![("name", "Name", rect.min.x + 26.0, name_w - 26.0), ("label", "", label_x - 8.0, 18.0)];
|
||||
// Optional columns wider than the panel scroll horizontally under the frozen Name and Label
|
||||
@@ -590,6 +621,21 @@ fn draw_header(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui::Painter, h
|
||||
}
|
||||
let hresp = ui.interact(Rect::from_min_max(pos2(end_x, hdr.min.y), hdr.max), egui::Id::unique("proj-hdr-rest"), Sense::CLICK);
|
||||
column_menu(app, &hresp);
|
||||
// The Name column's right edge: drag to resize it, double-click to fit the names again.
|
||||
let edge = Rect::from_center_size(pos2(hdr.min.x + cl.name_w + 5.0, hdr.center().y), vec2(8.0, hdr.height()));
|
||||
let eresp = ui.interact(edge, egui::Id::unique("proj-name-edge"), Sense::click_and_drag());
|
||||
app.auto.add("project.nameColumn.edge", edge, "Drag to resize the Name column; double-click to fit the names");
|
||||
if eresp.hovered() || eresp.dragged() {
|
||||
ui.ctx().set_cursor_icon(egui::CursorIcon::ResizeColumn);
|
||||
hp.line_segment([pos2(edge.center().x, hdr.min.y + 3.0), pos2(edge.center().x, hdr.max.y - 3.0)], Stroke::new(1.0, t.text_dim));
|
||||
}
|
||||
if eresp.double_clicked() {
|
||||
app.ui.project_name_w = None;
|
||||
} else if eresp.dragged() {
|
||||
let max_w = name_max_w(app, hdr.width());
|
||||
app.ui.project_name_w = Some((app.ui.project_name_w.unwrap_or(cl.name_w).min(max_w) + eresp.drag_delta().x).clamp(MIN_NAME_W, max_w));
|
||||
}
|
||||
eresp.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, true, crate::i18n::tr("Resize the Name column")));
|
||||
app.auto.add("project.columns", hdr, "Columns (right-click to show or hide)");
|
||||
}
|
||||
|
||||
@@ -1206,7 +1252,9 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
let gutter_id = egui::Id::unique("project-gutter");
|
||||
let gutter = ctx.data(|d| d.get_temp::<bool>(gutter_id)).unwrap_or(false);
|
||||
let content = if gutter { Rect::from_min_max(rect.min, pos2((rect.max.x - widgets::SCROLL_GUTTER).max(rect.min.x), rect.max.y)) } else { rect };
|
||||
let cl = layout_columns(app, content);
|
||||
// Only an automatic width (or an invalid saved one) measures the names.
|
||||
let fit = if app.ui.project_name_w.is_some_and(f32::is_finite) { 0.0 } else { name_fit(app, &p, &visible_rows(app)) };
|
||||
let cl = layout_columns(app, content, fit);
|
||||
let (name_w, label_x, opt_x0, overflow, hscroll) = (cl.name_w, cl.label_x, cl.opt_x0, cl.overflow, cl.hscroll);
|
||||
draw_header(app, ui, &p, hdr, &cl);
|
||||
// Rows.
|
||||
@@ -1451,6 +1499,53 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// Long names show in full: the Name column fits them (it stopped at 260 points with "…")
|
||||
/// up to the panel's width less the Label column and, while optional columns show, room for
|
||||
/// them; a width the user dragged wins.
|
||||
#[test]
|
||||
fn the_name_column_fits_the_names_up_to_the_panel_width() {
|
||||
let mut app = EffectcraftApp::new(effectcraft_engine::Session::default());
|
||||
let rect = Rect::from_min_size(pos2(0.0, 0.0), vec2(700.0, 400.0));
|
||||
assert_eq!(layout_columns(&mut app, rect, 0.0).name_w, 260.0);
|
||||
assert_eq!(layout_columns(&mut app, rect, 400.0).name_w, 400.0);
|
||||
// The optional columns keep 40 points more: some content and a usable scroll bar track.
|
||||
assert!(!app.ui.project_columns.is_empty());
|
||||
let cl = layout_columns(&mut app, rect, 2000.0);
|
||||
assert_eq!(cl.name_w, 620.0);
|
||||
assert!(rect.max.x - cl.opt_x0 >= 50.0, "the optional columns keep {} points", rect.max.x - cl.opt_x0);
|
||||
let narrow = Rect::from_min_size(pos2(0.0, 0.0), vec2(100.0, 400.0));
|
||||
assert_eq!(layout_columns(&mut app, narrow, 400.0).name_w, MIN_NAME_W);
|
||||
app.ui.project_name_w = Some(150.0);
|
||||
assert_eq!(layout_columns(&mut app, rect, 400.0).name_w, 150.0);
|
||||
app.ui.project_name_w = Some(f32::NAN);
|
||||
assert_eq!(layout_columns(&mut app, rect, 400.0).name_w, 400.0);
|
||||
app.ui.project_name_w = Some(5000.0);
|
||||
assert_eq!(layout_columns(&mut app, rect, 0.0).name_w, 620.0);
|
||||
// Without optional columns the Name column may take all but the Label column.
|
||||
app.ui.project_columns.clear();
|
||||
assert_eq!(layout_columns(&mut app, rect, 0.0).name_w, 660.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn name_fit_measures_the_longest_listed_name_with_its_indent() {
|
||||
let mut s = effectcraft_engine::Session::default();
|
||||
let p = std::sync::Arc::make_mut(&mut s.project);
|
||||
let folder = p.add_item("F", Label::Aqua, None, ItemKind::Folder);
|
||||
let long = p.add_item("A rather long composition name for the list", Label::Aqua, Some(folder), ItemKind::Folder);
|
||||
let app = EffectcraftApp::new(s);
|
||||
let ctx = egui::Context::default();
|
||||
let mut fit = (0.0, 0.0);
|
||||
ctx.run_ui(Default::default(), |ui| {
|
||||
let painter = ui.painter();
|
||||
let text = painter.layout_no_wrap("A rather long composition name for the list".into(), Tokens::ui(12.0), Color32::WHITE).size().x;
|
||||
fit = (name_fit(&app, painter, &[(folder, 0), (long, 1)]), 8.0 + 14.0 + 30.0 + text + 24.0);
|
||||
})
|
||||
.textures_delta
|
||||
.clear();
|
||||
assert_eq!(fit.0, fit.1);
|
||||
assert!(fit.0 > 260.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn size_keys_are_sampled_once_with_stable_ties() {
|
||||
let mut project = Project::default();
|
||||
|
||||
@@ -684,6 +684,10 @@ pub struct UiState {
|
||||
/// Horizontal scroll of the Project panel's optional columns (Name and Label stay put).
|
||||
#[serde(default)]
|
||||
pub project_hscroll: f32,
|
||||
/// Width of the Project panel's Name column set by dragging its edge in the header (points);
|
||||
/// `None` fits the longest name shown (double-clicking the edge goes back to that).
|
||||
#[serde(default)]
|
||||
pub project_name_w: Option<f32>,
|
||||
/// Vertical scroll of the Project panel's item list (points); only visible rows are drawn.
|
||||
#[serde(default)]
|
||||
pub project_scroll: f32,
|
||||
@@ -785,6 +789,7 @@ impl Default for UiState {
|
||||
project_sort: default_project_sort(),
|
||||
project_sort_desc: false,
|
||||
project_hscroll: 0.0,
|
||||
project_name_w: None,
|
||||
project_scroll: 0.0,
|
||||
project_columns: default_project_columns(),
|
||||
fx_closed: BTreeSet::new(),
|
||||
|
||||
@@ -299,3 +299,25 @@ fn project_panel_columns_scroll_horizontally() {
|
||||
assert!(rect(&h, "project.sort.comment").min.x < x0 - 20.0);
|
||||
assert_eq!(rect(&h, "project.sort.name").min.x, name0, "Name stays frozen");
|
||||
}
|
||||
|
||||
/// The Project panel's Name column edge drags to resize the column, and a double-click on the
|
||||
/// edge fits the names again.
|
||||
#[test]
|
||||
fn project_name_column_edge_resizes_and_fits_again() {
|
||||
let mut h = harness(1400.0);
|
||||
h.state_mut().ui.start_screen = false;
|
||||
h.run_steps(3);
|
||||
let before = rect(&h, "project.nameColumn.edge").center();
|
||||
drag(&mut h, before, before + vec2(80.0, 0.0));
|
||||
assert!(h.state().ui.project_name_w.is_some());
|
||||
let edge = rect(&h, "project.nameColumn.edge").center();
|
||||
assert!((edge.x - (before.x + 80.0)).abs() < 2.0, "{edge:?} after dragging from {before:?}");
|
||||
h.input_mut().events.push(Event::PointerMoved(edge));
|
||||
for _ in 0..2 {
|
||||
h.input_mut().events.push(Event::PointerButton { pos: edge, button: egui::PointerButton::Primary, pressed: true, modifiers: Default::default() });
|
||||
h.input_mut().events.push(Event::PointerButton { pos: edge, button: egui::PointerButton::Primary, pressed: false, modifiers: Default::default() });
|
||||
}
|
||||
h.run_steps(3);
|
||||
assert_eq!(h.state().ui.project_name_w, None);
|
||||
assert!((rect(&h, "project.nameColumn.edge").center().x - before.x).abs() < 1.0);
|
||||
}
|
||||
Reference in new issue
Block a user