Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions linux/changelog.d/3927.fixed.1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Changing the font or text size while a data grid was open left its columns at their old widths.
1 change: 1 addition & 0 deletions linux/changelog.d/3927.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
A data grid column sized by its header could cut the header's sort arrow.
125 changes: 106 additions & 19 deletions linux/crates/app/src/ui/data_grid/columns.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ use crate::services::grid::{
CellDisplay, ColumnLayout, GridColumn, LayoutSlot, Placement, default_width_chars, header_text, merge_order,
number_width_chars, sampled_texts, typical_texts, typical_width_chars,
};
use crate::ui::handler_guard::HandlerGuard;

/// Rows sampled for a column's opening width.
const WIDTH_SAMPLE: usize = 200;
Expand All @@ -25,6 +26,21 @@ const CELL_CHROME_PX: i32 = 13;
/// A resize step, in digits of the font the cells draw in, so a step is
/// the same number of characters at any text size.
const STEP_DIGITS: i32 = 2;
/// Adwaita draws a header's title in `.caption-heading`: bold, at 82 % of
/// the grid's font.
const HEADER_SCALE: f64 = 0.82;
/// GTK keeps a sort indicator beside the title of every sortable column,
/// sorted or not: 16 px wide in Adwaita, 6 px from the title.
const SORT_INDICATOR_PX: i32 = 16 + 6;

/// Whether measuring may narrow a column the grid sized itself.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Measure {
/// More rows only widen it, so it holds still as pages arrive.
Widen,
/// A font changed, so the width it had means nothing now.
Again,
}

/// What a column's cells read to draw a row. The column makes one and
/// every cell it builds shares it.
Expand Down Expand Up @@ -66,6 +82,8 @@ pub struct ColumnSet {
/// keep: a better guess from other rows should still reach them.
auto_widths: std::collections::HashMap<usize, i32>,
pages: Pages,
/// The fonts the grid last sized its columns in.
measured_in: Option<String>,
handlers: Vec<(glib::WeakRef<glib::Object>, glib::SignalHandlerId)>,
}

Expand Down Expand Up @@ -234,20 +252,55 @@ impl DataGrid {
/// the first rows, and keeps it while rows scroll by, since an
/// automatic width measures every row GTK builds and shifts.
fn size_unsized_columns(&self) {
let widths = self.opening_widths();
self.size_columns(Measure::Widen);
}

/// The monospace font, the interface font or the text size changed:
/// the columns the grid sized itself take the new measure, and a width
/// the user gave stays.
pub(super) fn fonts_changed(&self) {
self.size_columns(Measure::Again);
row_number_column::fit(self);
}

/// A grid out of any window hears of no font change, and one sized
/// before it had a window measured in no theme's font.
pub(super) fn rooted(&self) {
let measured_in = self.imp().columns.borrow().measured_in.clone();
if measured_in.is_some_and(|fonts| fonts != fonts_now(&self.column_view())) {
self.fonts_changed();
}
}

/// The interface font and the text size reach the grid as GTK system
/// settings; the monospace font is libadwaita's.
pub(super) fn watch_fonts(&self) {
let style = adw::StyleManager::default();
let handler = style.connect_monospace_font_name_notify(glib::clone!(
#[weak(rename_to = grid)]
self,
move |_| grid.fonts_changed()
));
self.imp().font_watch.replace(Some(HandlerGuard::new(&style, handler)));
}

fn size_columns(&self, measure: Measure) {
let widths = self.opening_widths(measure);
self.imp().columns.borrow_mut().measured_in = Some(fonts_now(&self.column_view()));
for (index, view_column, width) in widths {
self.imp().columns.borrow_mut().auto_widths.insert(index, width);
view_column.set_fixed_width(width);
}
}

fn opening_widths(&self) -> Vec<(usize, gtk::ColumnViewColumn, i32)> {
fn opening_widths(&self, measure: Measure) -> Vec<(usize, gtk::ColumnViewColumn, i32)> {
let set = self.imp().columns.borrow();
let sample: Vec<Option<Arc<[Value]>>> = self.store().rows().take(WIDTH_SAMPLE).map(|row| row.cells()).collect();
let by_type = set.pages == Pages::Empty;
let view = self.column_view();
let font = value_font(&view);
let metrics = view.pango_context().metrics(Some(&font), None);
let context = view.create_pango_context();
let font = value_font(&context);
let metrics = context.metrics(Some(&font), None);
let char_px = pango::units_to_double(metrics.approximate_char_width());
let null = editable_null_sentinel();
let mut widths = Vec::new();
Expand All @@ -272,31 +325,37 @@ impl DataGrid {
// no rows a value its type spells out, as a UUID or an enum's
// label, so those are measured as the cell draws them.
let sampled = match (by_type, paint.numbers.get()) {
(_, true) => tabular_width(&view, &font, &"0".repeat(usize::try_from(chars).unwrap_or_default())),
(_, true) => tabular_width(&context, &font, &"0".repeat(usize::try_from(chars).unwrap_or_default())),
(true, false) => 0,
// One line each, so one layout measures them all.
(false, false) => drawn_width(&view, &font, &sampled_texts(column, values.iter().copied()).join("\n")),
(false, false) => drawn_width(
&context,
&font,
&sampled_texts(column, values.iter().copied()).join("\n"),
),
};
let spelled = match by_type || column.display() == CellDisplay::Temporal {
true => typical_texts(column)
.iter()
.map(|typical| tabular_width(&view, &font, typical))
.map(|typical| tabular_width(&context, &font, typical))
.max()
.unwrap_or(0),
false => 0,
};
// The header draws its title smaller than the body font, so the
// body font's width holds it.
let header = view.create_pango_layout(Some(header_text(column))).pixel_size().0;
let indicator = match view_column.sorter() {
Some(_) => SORT_INDICATOR_PX,
None => 0,
};
let header = header_width(&context, header_text(column)) + indicator;
// A NULL the user can replace draws as `<NULL>`, wider than the
// fewest cells a column takes.
let placeholder = match paint.tracked.tracks_changes() && values.iter().any(|value| value.is_null()) {
true => drawn_width(&view, &font, &null),
true => drawn_width(&context, &font, &null),
false => 0,
};
let text = estimate.max(sampled).max(spelled).max(header).max(placeholder);
let width = text + CELL_CHROME_PX;
if auto.is_none_or(|auto| width > auto) {
if measure == Measure::Again || auto.is_none_or(|auto| width > auto) {
widths.push((index, view_column.clone(), width));
}
}
Expand Down Expand Up @@ -493,7 +552,8 @@ impl DataGrid {
return;
};
let view = self.column_view();
let metrics = view.pango_context().metrics(Some(&value_font(&view)), None);
let context = view.create_pango_context();
let metrics = context.metrics(Some(&value_font(&context)), None);
let step = (pango::units_to_double(metrics.approximate_digit_width()) * f64::from(STEP_DIGITS)).ceil() as i32;
let width = column.fixed_width();
let width = match wider {
Expand Down Expand Up @@ -721,27 +781,54 @@ fn same_names(layout: &ColumnLayout, columns: &[GridColumn]) -> bool {

/// The font a cell draws its value in: libadwaita's `monospace` class
/// sets only the family and size of the desktop's monospace font.
fn value_font(view: &gtk::ColumnView) -> pango::FontDescription {
fn value_font(context: &pango::Context) -> pango::FontDescription {
let mut monospace = pango::FontDescription::from_string(&adw::StyleManager::default().monospace_font_name());
monospace.unset_fields(!(pango::FontMask::FAMILY | pango::FontMask::SIZE));
let mut font = view.pango_context().font_description().unwrap_or_default();
let mut font = context.font_description().unwrap_or_default();
font.merge(Some(&monospace), true);
font
}

/// How wide a header draws `title`. The scaled size rounds up, since GTK
/// scales the size before it rounds and the view's font is rounded.
fn header_width(context: &pango::Context, title: &str) -> i32 {
let mut font = context.font_description().unwrap_or_default();
font.set_weight(pango::Weight::Bold);
let size = (f64::from(font.size()) * HEADER_SCALE).ceil();
match font.is_size_absolute() {
true => font.set_absolute_size(size),
false => font.set_size(size as i32),
}
drawn_width(context, &font, title)
}

/// The grid's font, the text size in it, and the monospace font. A
/// widget's own context follows a change only at the next frame, so a
/// fresh one is read.
fn fonts_now(view: &gtk::ColumnView) -> String {
let grid = view
.create_pango_context()
.font_description()
.map(|font| font.to_string())
.unwrap_or_default();
format!("{grid}; {}", adw::StyleManager::default().monospace_font_name())
}

/// How wide `text` is in the tabular figures libadwaita's `numeric`
/// class draws.
fn tabular_width(widget: &impl IsA<gtk::Widget>, font: &pango::FontDescription, text: &str) -> i32 {
let layout = widget.create_pango_layout(Some(text));
fn tabular_width(context: &pango::Context, font: &pango::FontDescription, text: &str) -> i32 {
let layout = pango::Layout::new(context);
layout.set_text(text);
layout.set_font_description(Some(font));
let tabular = pango::AttrList::new();
tabular.insert(pango::AttrFontFeatures::new("tnum"));
layout.set_attributes(Some(&tabular));
layout.pixel_size().0
}

fn drawn_width(widget: &impl IsA<gtk::Widget>, font: &pango::FontDescription, text: &str) -> i32 {
let layout = widget.create_pango_layout(Some(text));
fn drawn_width(context: &pango::Context, font: &pango::FontDescription, text: &str) -> i32 {
let layout = pango::Layout::new(context);
layout.set_text(text);
layout.set_font_description(Some(font));
layout.pixel_size().0
}
14 changes: 14 additions & 0 deletions linux/crates/app/src/ui/data_grid/imp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ use super::row_detail::RowDetailPane;
use super::selection::Target;
use super::selection_summary::SummaryState;
use super::{GridConfig, GridStore};
use crate::ui::handler_guard::HandlerGuard;

/// The grid's selection model, made with the grid and given its rows once
/// the grid knows how it sorts.
Expand Down Expand Up @@ -85,6 +86,7 @@ pub struct DataGrid {
pub editing: Cell<bool>,
/// The window's focus handler, while the grid is in a window.
pub focus_watch: RefCell<Option<(glib::WeakRef<gtk4::Window>, glib::SignalHandlerId)>>,
pub(super) font_watch: RefCell<Option<HandlerGuard>>,
}

#[glib::object_subclass]
Expand Down Expand Up @@ -122,6 +124,7 @@ impl ObjectImpl for DataGrid {
self.find.stop();
self.summary.stop();
self.obj().stop_watching_focus();
drop(self.font_watch.take());
if let Some(menus) = self.menus.get() {
menus.unparent();
}
Expand All @@ -134,12 +137,23 @@ impl WidgetImpl for DataGrid {
fn root(&self) {
self.parent_root();
self.obj().watch_focus();
self.obj().rooted();
}

fn unroot(&self) {
self.obj().stop_watching_focus();
self.parent_unroot();
}

fn system_setting_changed(&self, setting: &gtk4::SystemSetting) {
self.parent_system_setting_changed(setting);
if matches!(
setting,
gtk4::SystemSetting::Dpi | gtk4::SystemSetting::FontName | gtk4::SystemSetting::FontConfig
) {
self.obj().fonts_changed();
}
}
}

impl BinImpl for DataGrid {}
1 change: 1 addition & 0 deletions linux/crates/app/src/ui/data_grid/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ impl DataGrid {
find_bar::install(&grid);
imp.detail_pane.attach(&grid);
grid.set_columns(columns, sorting, rules);
grid.watch_fonts();
grid
}

Expand Down
4 changes: 3 additions & 1 deletion linux/crates/app/src/ui/data_grid/row_number_column.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,9 @@ pub fn fit(grid: &DataGrid) {
};
let largest = grid.row_base() + u64::from(grid.store().len());
let digits = largest.max(1).ilog10() as i32 + 1;
let metrics = grid.column_view().pango_context().metrics(None, None);
// A fresh context, since the view's own follows a font change only at
// the next frame.
let metrics = grid.column_view().create_pango_context().metrics(None, None);
let digit_px = pango::units_to_double(metrics.approximate_digit_width());
column.set_fixed_width((f64::from(digits.max(2)) * digit_px).ceil() as i32 + CHROME_PX);
}
Expand Down
Loading
Loading