mirror of
https://github.com/storytold/gridcraft.git
synced 2026-10-25 11:16:49 -06:00
AutoFit Column Width measures text in the font the grid draws it with (fixes #1316)
This commit is contained in:
@@ -1008,7 +1008,7 @@ pub fn chars_to_points(c: f64) -> f64 {
|
||||
pub use gridcraft_model::points_to_chars;
|
||||
|
||||
/// Approximate text width in points for autofit (no font metrics in the engine: average glyph
|
||||
/// widths for the default font; the UI refines with real measurement when it autofits).
|
||||
/// widths for the default font; a front end with fonts measures instead, see `Session::text_width`).
|
||||
pub fn approx_text_width(text: &str, size: f32, bold: bool) -> f32 {
|
||||
let mut w = 0.0;
|
||||
for c in text.chars() {
|
||||
@@ -1027,6 +1027,7 @@ pub fn approx_text_width(text: &str, size: f32, bold: bool) -> f32 {
|
||||
|
||||
fn autofit_cols(s: &mut Session, p: &Json) -> Result<Json> {
|
||||
let lines = line_targets(s, p, false)?;
|
||||
let measure = s.text_width.clone();
|
||||
edit(s, |cx| {
|
||||
let si = cx.sheet_index();
|
||||
let Some(sh) = cx.wb.sheet(si) else { return Ok(Json::Null) };
|
||||
@@ -1044,9 +1045,11 @@ fn autofit_cols(s: &mut Session, p: &Json) -> Result<Json> {
|
||||
let st = cx.wb.styles.get(sh.style_id(cell));
|
||||
let text = crate::display::cell_text(&cx.wb, sh, cell);
|
||||
for line in text.lines() {
|
||||
max = max.max(
|
||||
approx_text_width(line, st.font.size, st.font.bold) + 6.0 + st.align.indent as f32 * 9.0 + sh.filter_button_space(cell),
|
||||
);
|
||||
let w = match &measure {
|
||||
Some(m) => m(line, &st.font),
|
||||
None => approx_text_width(line, st.font.size, st.font.bold),
|
||||
};
|
||||
max = max.max(w + 6.0 + st.align.indent as f32 * 9.0 + sh.filter_button_space(cell));
|
||||
}
|
||||
}
|
||||
if max > 0.0 {
|
||||
|
||||
@@ -227,6 +227,10 @@ pub enum UiRequest {
|
||||
OpenUrl(String),
|
||||
}
|
||||
|
||||
/// Width in points of one line of text drawn in a cell font, measured by a front end that has
|
||||
/// the font the grid draws with.
|
||||
pub type TextWidth = Arc<dyn Fn(&str, &model::Font) -> f32 + Send + Sync>;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Session {
|
||||
docs: Vec<DocState>,
|
||||
@@ -256,6 +260,9 @@ pub struct Session {
|
||||
/// Review › Notes › Show All Notes: every note is displayed. An application display setting
|
||||
/// (like Excel's comment indicator option), so it's not saved in the file.
|
||||
pub show_all_notes: bool,
|
||||
/// Measures cell text for AutoFit Column Width. Without it (CLI, MCP) AutoFit uses
|
||||
/// [`cmd::format::approx_text_width`].
|
||||
pub text_width: Option<TextWidth>,
|
||||
}
|
||||
|
||||
impl Session {
|
||||
|
||||
@@ -990,6 +990,10 @@ impl SheetApp {
|
||||
|
||||
/// Per-frame logic (control channel, screenshots). Call before `ui`.
|
||||
pub fn logic(&mut self, ctx: &egui::Context) {
|
||||
if self.session.text_width.is_none() {
|
||||
let ctx = ctx.clone();
|
||||
self.session.text_width = Some(std::sync::Arc::new(move |text, font| theme::cell_text_width(&ctx, text, font)));
|
||||
}
|
||||
// A keytip control request lives one frame: the ribbon consumes it during `ui`, and
|
||||
// anything that never reached a control (collapsed ribbon, tab switch) is dropped.
|
||||
self.ui.keytip_invoke = None;
|
||||
|
||||
@@ -192,6 +192,17 @@ pub fn cell_family(name: &str, bold: bool, italic: bool) -> FontFamily {
|
||||
)
|
||||
}
|
||||
|
||||
/// Width in points, at 100% zoom, of one line of cell text drawn as the grid draws it: in the
|
||||
/// face [`cell_family`] picks, which on a machine without the named font is a wider or narrower
|
||||
/// substitute (#1316). A family the active font set doesn't have yet (bound fonts apply from the
|
||||
/// next frame) falls back to the engine's estimate rather than letting egui panic on it.
|
||||
pub fn cell_text_width(ctx: &egui::Context, text: &str, font: &gridcraft_engine::model::Font) -> f32 {
|
||||
let family = cell_family(&font.name, font.bold, font.italic);
|
||||
let id = FontId::new(font.size * 96.0 / 72.0, family);
|
||||
ctx.fonts_mut(|f| f.families().contains(&id.family).then(|| f.layout_no_wrap(text.to_owned(), id, Color32::PLACEHOLDER).size().x))
|
||||
.unwrap_or_else(|| gridcraft_engine::cmd::format::approx_text_width(text, font.size, font.bold))
|
||||
}
|
||||
|
||||
/// Whether epaint can open face `index` of this font file and it maps characters to glyphs.
|
||||
/// epaint panics (in `Context::set_fonts`, i.e. on startup) on a face it can't open, so every font
|
||||
/// file we did not ship is checked first with the same parser and the same call epaint makes
|
||||
|
||||
@@ -191,3 +191,30 @@ fn table_filter_autofit_works_through_command_and_header_double_click() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn header_double_click_fits_the_text_as_the_grid_draws_it() {
|
||||
// Issue #1316: `@` and `%` are far wider in every real font than the engine's average-glyph
|
||||
// estimate, so AutoFit has to measure with the font the grid draws the cell in.
|
||||
let text = "sarmstrong@example.com %%% @@@";
|
||||
let mut h = harness("A1");
|
||||
h.state_mut().session.execute("range.setValues", json!({"range":"A1", "values":[[text]]})).unwrap();
|
||||
h.run_steps(3);
|
||||
let (pos, _) = edge(&h, false, 0);
|
||||
button(&mut h, pos, true);
|
||||
button(&mut h, pos, false);
|
||||
h.step();
|
||||
button(&mut h, pos, true);
|
||||
button(&mut h, pos, false);
|
||||
h.run_steps(2);
|
||||
|
||||
let app = h.state();
|
||||
let sheet = app.session.active().unwrap().wb.active().unwrap();
|
||||
let font = &app.session.active().unwrap().wb.styles.get(sheet.style_id(gridcraft_engine::core::CellRef::new(0, 0))).font;
|
||||
let id = egui::FontId::new(font.size * 96.0 / 72.0, gridcraft_ui_egui::theme::cell_family(&font.name, font.bold, font.italic));
|
||||
let drawn = h.ctx.fonts_mut(|f| f.layout_no_wrap(text.to_string(), id, egui::Color32::BLACK)).size().x;
|
||||
let estimate = gridcraft_engine::cmd::format::approx_text_width(text, font.size, font.bold);
|
||||
assert!(drawn > estimate + 6.0, "fixture: the drawn text ({drawn}) must be wider than the estimate ({estimate}) plus padding");
|
||||
// Two 2.5 px paddings and the gridline, as the grid lays out the cell.
|
||||
assert!(sheet.col_width(0) >= drawn + 6.0, "column {} is narrower than the drawn text {drawn} + padding", sheet.col_width(0));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user