Merge pull request #5207 from dotCooCoo/fix/tabs-above-min-width

Tabs: Above Text keeps the ruler at the zoom when the panel comes out wider
This commit is contained in:
BFlat
2026-11-02 14:52:50 +07:00
committed by GitHub
3 changed files with 113 additions and 4 deletions
+3 -1
View File
@@ -39,6 +39,8 @@ pub(crate) const COLUMN_MIN: f32 = 220.0;
pub(crate) const COLUMN_MAX: f32 = 720.0;
/// Width of a column collapsed to icons.
pub(crate) const ICON_WIDTH: f32 = 38.0;
/// The narrowest a panel other than the canvas is drawn, docked or floating.
pub(crate) const PANEL_MIN_WIDTH: f32 = 220.0;
/// Docking state that lives for the session only.
#[derive(Default)]
@@ -1298,7 +1300,7 @@ impl craft_ui::docking::DockContent<String> for PanelContent<'_> {
}
fn limits(&self, id: &String) -> craft_ui::docking::PanelLimits {
let min = if id == CANVAS { egui::vec2(200.0, 120.0) } else { egui::vec2(220.0, 80.0) };
let min = if id == CANVAS { egui::vec2(200.0, 120.0) } else { egui::vec2(PANEL_MIN_WIDTH, 80.0) };
craft_ui::docking::PanelLimits { min, ..Default::default() }
}
+40 -3
View File
@@ -41,6 +41,9 @@ fn align_id(a: TabAlign) -> &'static str {
/// The panel body's margin and the ruler's own inset: the ruler's 0 is this far right of the panel.
const RULER_LEFT: f32 = 10.0 + 4.0;
/// The most ticks the ruler draws.
const MAX_TICKS: usize = 10_000;
/// The largest stop position `text.tabs.set` takes, in points.
const MAX_STOP: f64 = 100_000.0;
@@ -248,6 +251,27 @@ fn current_para(app: &VectorcraftApp) -> Option<(Node, ParaStyle, f64)> {
Some((n, para, span.max(72.0)))
}
/// [`above_text`], then note the active document and text `n`. A floating panel is
/// at least [`crate::panel_docking::PANEL_MIN_WIDTH`] wide, so for narrow text at a low zoom the
/// panel comes out wider than asked; the ruler then draws text `n` at [`screen_scale`] and runs
/// on past the text's width.
fn place_above_text(app: &mut VectorcraftApp, ctx: &egui::Context, n: &Node, span: f64) {
if above_text(app, ctx, n, span).is_some() {
set_pstate(ctx, "tabs-above", app.session.active().map(|doc| (doc.uid, n.id)));
}
}
/// Screen points per point of text `n` at the canvas zoom, as [`above_text`] sizes the ruler:
/// across the frame for area type, the zoom for point type.
fn screen_scale(app: &VectorcraftApp, n: &Node, span: f64) -> Option<f64> {
let xf = crate::canvas::Xf::new(app.canvas_rect?, app.view()?);
let per = match &n.kind {
NodeKind::Text(t) if matches!(t.kind, TextKind::Area { .. }) => f64::from(xf.rect_to_screen(n.geometric_bounds()?).width()) / span,
_ => xf.zoom,
};
(per.is_finite() && per > 0.0).then_some(per)
}
/// Position Panel Above Text: float the panel just above text `n`, as wide as its ruler needs to
/// put 0 on the text's left edge and its marks at the canvas zoom, so each stop sits over the
/// column it sets (#729). `span`: the ruler's length in the text's points.
@@ -383,7 +407,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
}
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
if ui.button(tl!("Above Text")).on_hover_text(tl!("Position Panel Above Text")).clicked() {
above_text(app, ui.ctx(), &node, span);
place_above_text(app, ui.ctx(), &node, span);
}
});
});
@@ -430,7 +454,20 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
let p = ui.painter_at(rect.expand(2.0));
p.rect_filled(rect, 2.0, t.input);
p.rect_stroke(rect, 2.0, Stroke::new(1.0, t.input_border), egui::StrokeKind::Inside);
let scale = (w as f64 - 8.0) / span;
// After Position Panel Above Text on this text, while the panel floats no wider than the
// narrowest floating window, a ruler wider than the text is on screen draws the text at its
// screen scale and runs on past its width. Any other ruler fits the span to its width.
let above: Option<(u64, vectorcraft_doc::NodeId)> = pstate(ui.ctx(), "tabs-above");
let narrowest = || {
let floating = app.ui.docking.as_ref().and_then(|l| l.floating.iter().find(|f| f.contains(&"tabs".to_string())));
floating.is_some_and(|f| f.rect[2] <= crate::panel_docking::PANEL_MIN_WIDTH + 0.5)
};
let scale = above
.filter(|at| Some(*at) == app.session.active().map(|doc| (doc.uid, node.id)) && narrowest())
.and_then(|_| screen_scale(app, &node, span))
.filter(|per| span * per + 8.0 < w as f64 - 0.5)
.unwrap_or((w as f64 - 8.0) / span);
let length = span.max((w as f64 - 8.0) / scale);
let to_x = |pt: f64| rect.left() + 4.0 + (pt * scale) as f32;
let to_pt = |x: f32| (((x - rect.left() - 4.0) as f64) / scale).max(0.0);
// The ruler is numbered in the General unit, with the canvas rulers' label step at its scale
@@ -438,7 +475,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
let unit = app.session.general_unit();
let step = unit.ruler_step(scale);
let minor = step / 10.0;
for k in 0..=(unit.from_pt(span) / minor + 1e-6).floor() as usize {
for k in 0..=((unit.from_pt(length) / minor + 1e-6).floor() as usize).min(MAX_TICKS) {
let v = k as f64 * minor;
let x = to_x(unit.to_pt(v));
let len = match k % 10 {
+70
View File
@@ -984,6 +984,76 @@ fn delete_tab_after_the_last_stop_deletes_the_stop_the_ruler_shows_selected() {
assert!(current(&app).unwrap().1.is_empty());
}
/// Position Panel Above Text on text narrower than a floating panel can be at the zoom: the panel
/// comes out wider than asked, and its ruler draws the text at the zoom, running on past the text,
/// so its marks stay over the text. A ruler wider than the narrowest panel, for other text or in
/// another document fits the span to its width as before.
#[test]
fn above_text_keeps_the_zoom_on_a_ruler_wider_than_asked() {
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.session.execute("file.new", &json!({"width": 600, "height": 400})).unwrap();
app.session.execute("text.create", &json!({"x": 40, "y": 200, "text": "a b", "area": {"width": 100, "height": 100}})).unwrap();
app.canvas_rect = Some(egui::Rect::from_min_size(egui::pos2(50.0, 40.0), vec2(1000.0, 700.0)));
if let Some(v) = app.view_mut() {
v.zoom = 0.5;
}
let ctx = egui::Context::default();
let (node, _, span) = current(&app).unwrap();
place_above_text(&mut app, &ctx, &node, span);
// The panel asked for: a 50 pt ruler, less than the narrowest panel's.
let asked = app.ui.docking.clone().unwrap().floating.iter().find(|w| w.contains(&"tabs".to_string())).unwrap().rect[2];
assert!((asked - RULER_LEFT * 2.0 - 50.0).abs() < 0.01, "{asked}");
// Screen points per point of text on a ruler `width` wide, from its first two labels.
let scale = |app: &mut VectorcraftApp, width: f32| {
let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, vec2(width, 400.0));
let mut out = ctx.run_ui(egui::RawInput { screen_rect: Some(screen), ..Default::default() }, |ui| show(app, ui));
out.textures_delta.clear();
let labels: Vec<(f64, f32)> = out
.shapes
.iter()
.filter_map(|c| match &c.shape {
egui::Shape::Text(t) if t.galley.job.sections.first().is_some_and(|s| s.format.font_id.size == 9.0) => {
Some((t.galley.text().parse::<f64>().ok()?, t.pos.x))
}
_ => None,
})
.collect();
let [(0.0, a), (next, b), ..] = labels[..] else { panic!("ruler labels {labels:?}") };
(b - a) as f64 / next
};
let narrowest = crate::panel_docking::PANEL_MIN_WIDTH;
let fitted = (f64::from(narrowest) - 8.0) / span;
let at_zoom = scale(&mut app, narrowest);
assert!((at_zoom - 0.5).abs() < 1e-3, "{at_zoom}");
let other =
app.session.execute("text.create", &json!({"x": 300, "y": 200, "text": "c", "area": {"width": 100, "height": 100}})).unwrap()["id"].clone();
app.session.execute("select.set", &json!({"ids": [other]})).unwrap();
assert!((scale(&mut app, narrowest) - fitted).abs() < 1e-3, "other text fits its span to the ruler");
app.session.execute("select.set", &json!({"ids": [node.id.0]})).unwrap();
assert!((scale(&mut app, narrowest) - 0.5).abs() < 1e-3);
// At another zoom the ruler follows it.
if let Some(v) = app.view_mut() {
v.zoom = 2.0;
}
assert!((scale(&mut app, narrowest) - 2.0).abs() < 1e-3, "another zoom");
if let Some(v) = app.view_mut() {
v.zoom = 0.5;
}
app.session.execute("file.new", &json!({"width": 600, "height": 400})).unwrap();
app.session.execute("text.create", &json!({"x": 40, "y": 200, "text": "d", "area": {"width": 100, "height": 100}})).unwrap();
if let Some(v) = app.view_mut() {
v.zoom = 0.5;
}
assert!((scale(&mut app, narrowest) - fitted).abs() < 1e-3, "another document");
assert!(app.session.set_active(0));
assert!((scale(&mut app, narrowest) - 0.5).abs() < 1e-3);
// The panel made wider than the narrowest floating window fits the span to its ruler again.
let docking = app.ui.docking.as_mut().unwrap();
docking.floating.iter_mut().find(|w| w.contains(&"tabs".to_string())).unwrap().rect[2] = 400.0;
let wide = scale(&mut app, 400.0);
assert!((wide - (400.0 - 8.0) / span).abs() < 1e-3, "{wide}");
}
/// Position Panel Above Text floats the panel over the text, its ruler's 0 on the frame's left
/// edge and a stop's mark over the column it sets, at any zoom (#729).
#[test]