1
0
mirror of https://github.com/emilk/egui.git synced 2026-06-26 22:53:14 -04:00

Stop wrapping FontsImpl in an Arc<Mutex<_>>

We never need to clone it, and none of its methods took `self` as
mutable, meaning it wasn't making use of the semantic difference between
the two.

This API is about to get reworked, and simplifying it is a first step.
This commit is contained in:
valadaptive
2025-07-03 18:21:08 -04:00
parent aedd43c88f
commit bbf9ac4d4b
23 changed files with 106 additions and 105 deletions

View File

@@ -60,7 +60,7 @@ pub trait AtomExt<'a> {
{
let font_selection = FontSelection::default();
let font_id = font_selection.resolve(ui.style());
let height = ui.fonts(|f| f.row_height(&font_id));
let height = ui.fonts_mut(|f| f.row_height(&font_id));
self.atom_max_height(height)
}
}

View File

@@ -476,7 +476,7 @@ impl Window<'_> {
let (title_bar_height_with_margin, title_content_spacing) = if with_title_bar {
let style = ctx.style();
let title_bar_inner_height = ctx
.fonts(|fonts| title.font_height(fonts, &style))
.fonts_mut(|fonts| title.font_height(fonts, &style))
.at_least(style.spacing.interact_size.y);
let title_bar_inner_height = title_bar_inner_height + window_frame.inner_margin.sum().y;
let half_height = (title_bar_inner_height / 2.0).round() as _;

View File

@@ -641,7 +641,7 @@ impl ContextImpl {
// Preload the most common characters for the most common fonts.
// This is not very important to do, but may save a few GPU operations.
for font_id in self.memory.options.style().text_styles.values() {
fonts.lock().fonts.font(font_id).preload_common_characters();
fonts.fonts.font(font_id).preload_common_characters();
}
}
}
@@ -1060,6 +1060,22 @@ impl Context {
})
}
/// Read-write access to [`Fonts`].
///
/// Not valid until first call to [`Context::run()`].
/// That's because since we don't know the proper `pixels_per_point` until then.
#[inline]
pub fn fonts_mut<R>(&self, reader: impl FnOnce(&mut Fonts) -> R) -> R {
self.write(move |ctx| {
let pixels_per_point = ctx.pixels_per_point();
reader(
ctx.fonts
.get_mut(&pixels_per_point.into())
.expect("No fonts available until first call to Context::run()"),
)
})
}
/// Read-only access to [`Options`].
#[inline]
pub fn options<R>(&self, reader: impl FnOnce(&Options) -> R) -> R {
@@ -1568,9 +1584,8 @@ impl Context {
} = ModifierNames::SYMBOLS;
let font_id = TextStyle::Body.resolve(&self.style());
self.fonts(|f| {
let mut lock = f.lock();
let font = lock.fonts.font(&font_id);
self.fonts_mut(|f| {
let font = f.fonts.font(&font_id);
font.has_glyphs(alt)
&& font.has_glyphs(ctrl)
&& font.has_glyphs(shift)
@@ -1927,7 +1942,7 @@ impl Context {
self.read(|ctx| {
if let Some(current_fonts) = ctx.fonts.get(&pixels_per_point.into()) {
// NOTE: this comparison is expensive since it checks TTF data for equality
if current_fonts.lock().fonts.definitions() == &font_definitions {
if current_fonts.fonts.definitions() == &font_definitions {
update_fonts = false; // no need to update
}
}
@@ -1955,7 +1970,6 @@ impl Context {
self.read(|ctx| {
if let Some(current_fonts) = ctx.fonts.get(&pixels_per_point.into()) {
if current_fonts
.lock()
.fonts
.definitions()
.font_data

View File

@@ -98,7 +98,7 @@ impl State {
{
// Paint location to left of `pos`:
let location_galley =
ctx.fonts(|f| f.layout(location, font_id.clone(), color, f32::INFINITY));
ctx.fonts_mut(|f| f.layout(location, font_id.clone(), color, f32::INFINITY));
let location_rect =
Align2::RIGHT_TOP.anchor_size(pos - 4.0 * Vec2::X, location_galley.size());
painter.galley(location_rect.min, location_galley, color);

View File

@@ -149,6 +149,14 @@ impl Painter {
self.ctx.fonts(reader)
}
/// Read-write access to the shared [`Fonts`].
///
/// See [`Context`] documentation for how locks work.
#[inline]
pub fn fonts_mut<R>(&self, reader: impl FnOnce(&mut Fonts) -> R) -> R {
self.ctx.fonts_mut(reader)
}
/// Where we paint
#[inline]
pub fn layer_id(&self) -> LayerId {
@@ -525,7 +533,7 @@ impl Painter {
color: crate::Color32,
wrap_width: f32,
) -> Arc<Galley> {
self.fonts(|f| f.layout(text, font_id, color, wrap_width))
self.fonts_mut(|f| f.layout(text, font_id, color, wrap_width))
}
/// Will line break at `\n`.
@@ -539,7 +547,7 @@ impl Painter {
font_id: FontId,
color: crate::Color32,
) -> Arc<Galley> {
self.fonts(|f| f.layout(text, font_id, color, f32::INFINITY))
self.fonts_mut(|f| f.layout(text, font_id, color, f32::INFINITY))
}
/// Lay out this text layut job in a galley.
@@ -548,7 +556,7 @@ impl Painter {
#[inline]
#[must_use]
pub fn layout_job(&self, layout_job: LayoutJob) -> Arc<Galley> {
self.fonts(|f| f.layout_job(layout_job))
self.fonts_mut(|f| f.layout_job(layout_job))
}
/// Paint text that has already been laid out in a [`Galley`].

View File

@@ -735,7 +735,7 @@ impl Ui {
///
/// Returns a value rounded to [`emath::GUI_ROUNDING`].
pub fn text_style_height(&self, style: &TextStyle) -> f32 {
self.fonts(|f| f.row_height(&style.resolve(self.style())))
self.fonts_mut(|f| f.row_height(&style.resolve(self.style())))
}
/// Screen-space rectangle for clipping what we paint in this ui.
@@ -852,6 +852,12 @@ impl Ui {
pub fn fonts<R>(&self, reader: impl FnOnce(&Fonts) -> R) -> R {
self.ctx().fonts(reader)
}
/// Read-write access to [`Fonts`].
#[inline]
pub fn fonts_mut<R>(&self, reader: impl FnOnce(&mut Fonts) -> R) -> R {
self.ctx().fonts_mut(reader)
}
}
// ------------------------------------------------------------------------

View File

@@ -307,7 +307,7 @@ impl RichText {
/// Read the font height of the selected text style.
///
/// Returns a value rounded to [`emath::GUI_ROUNDING`].
pub fn font_height(&self, fonts: &epaint::Fonts, style: &Style) -> f32 {
pub fn font_height(&self, fonts: &mut epaint::Fonts, style: &Style) -> f32 {
let mut font_id = self.text_style.as_ref().map_or_else(
|| FontSelection::Default.resolve(style),
|text_style| text_style.resolve(style),
@@ -676,7 +676,7 @@ impl WidgetText {
}
/// Returns a value rounded to [`emath::GUI_ROUNDING`].
pub(crate) fn font_height(&self, fonts: &epaint::Fonts, style: &Style) -> f32 {
pub(crate) fn font_height(&self, fonts: &mut epaint::Fonts, style: &Style) -> f32 {
match self {
Self::Text(_) => fonts.row_height(&FontSelection::Default.resolve(style)),
Self::RichText(text) => text.font_height(fonts, style),
@@ -762,7 +762,7 @@ impl WidgetText {
},
);
layout_job.wrap = text_wrapping;
ctx.fonts(|f| f.layout_job(layout_job))
ctx.fonts_mut(|f| f.layout_job(layout_job))
}
Self::RichText(text) => {
let mut layout_job = Arc::unwrap_or_clone(text).into_layout_job(
@@ -771,12 +771,12 @@ impl WidgetText {
default_valign,
);
layout_job.wrap = text_wrapping;
ctx.fonts(|f| f.layout_job(layout_job))
ctx.fonts_mut(|f| f.layout_job(layout_job))
}
Self::LayoutJob(job) => {
let mut job = Arc::unwrap_or_clone(job);
job.wrap = text_wrapping;
ctx.fonts(|f| f.layout_job(job))
ctx.fonts_mut(|f| f.layout_job(job))
}
Self::Galley(galley) => galley,
}

View File

@@ -211,7 +211,7 @@ impl Label {
if let Some(first_section) = layout_job.sections.first_mut() {
first_section.leading_space = first_row_indentation;
}
let galley = ui.fonts(|fonts| fonts.layout_job(layout_job));
let galley = ui.fonts_mut(|fonts| fonts.layout_job(layout_job));
let pos = pos2(ui.max_rect().left(), ui.cursor().top());
assert!(!galley.rows.is_empty(), "Galleys are never empty");
@@ -252,7 +252,7 @@ impl Label {
layout_job.justify = ui.layout().horizontal_justify();
}
let galley = ui.fonts(|fonts| fonts.layout_job(layout_job));
let galley = ui.fonts_mut(|fonts| fonts.layout_job(layout_job));
let (rect, mut response) = ui.allocate_exact_size(galley.size(), sense);
response.intrinsic_size = Some(galley.intrinsic_size());
let galley_pos = match galley.job.halign {

View File

@@ -504,7 +504,7 @@ impl TextEdit<'_> {
let hint_text_str = hint_text.text().to_owned();
let font_id = font_selection.resolve(ui.style());
let row_height = ui.fonts(|f| f.row_height(&font_id));
let row_height = ui.fonts_mut(|f| f.row_height(&font_id));
const MIN_WIDTH: f32 = 24.0; // Never make a [`TextEdit`] more narrow than this.
let available_width = (ui.available_width() - margin.sum().x).at_least(MIN_WIDTH);
let desired_width = desired_width.unwrap_or_else(|| ui.spacing().text_edit_width);
@@ -522,7 +522,7 @@ impl TextEdit<'_> {
} else {
LayoutJob::simple_singleline(text, font_id_clone.clone(), text_color)
};
ui.fonts(|f| f.layout_job(layout_job))
ui.fonts_mut(|f| f.layout_job(layout_job))
};
let layouter = layouter.unwrap_or(&mut default_layouter);

View File

@@ -238,7 +238,7 @@ impl ColoredText {
pub fn ui(&self, ui: &mut egui::Ui) {
let mut job = self.0.clone();
job.wrap.max_width = ui.available_width();
let galley = ui.fonts(|f| f.layout_job(job));
let galley = ui.fonts_mut(|f| f.layout_job(job));
ui.add(egui::Label::new(galley).selectable(true));
}
}

View File

@@ -90,7 +90,7 @@ impl FrameHistory {
));
let cpu_usage = to_screen.inverse().transform_pos(pointer_pos).y;
let text = format!("{:.1} ms", 1e3 * cpu_usage);
shapes.push(ui.fonts(|f| {
shapes.push(ui.fonts_mut(|f| {
Shape::text(
f,
pos2(rect.left(), y),

View File

@@ -165,14 +165,13 @@ pub fn criterion_benchmark(c: &mut Criterion) {
let wrap_width = 512.0;
let font_id = egui::FontId::default();
let text_color = egui::Color32::WHITE;
let fonts = egui::epaint::text::Fonts::new(
let mut fonts = egui::epaint::text::Fonts::new(
pixels_per_point,
max_texture_side,
egui::epaint::AlphaFromCoverage::default(),
egui::FontDefinitions::default(),
);
{
let mut locked_fonts = fonts.lock();
c.bench_function("text_layout_uncached", |b| {
b.iter(|| {
use egui::epaint::text::{LayoutJob, layout};
@@ -183,7 +182,7 @@ pub fn criterion_benchmark(c: &mut Criterion) {
text_color,
wrap_width,
);
layout(&mut locked_fonts.fonts, job.into())
layout(&mut fonts.fonts, job.into())
});
});
}

View File

@@ -85,7 +85,7 @@ impl crate::View for CodeEditor {
language,
);
layout_job.wrap.max_width = wrap_width;
ui.fonts(|f| f.layout_job(layout_job))
ui.fonts_mut(|f| f.layout_job(layout_job))
};
egui::ScrollArea::vertical().show(ui, |ui| {

View File

@@ -85,7 +85,7 @@ impl CodeExample {
ui.horizontal(|ui| {
let font_id = egui::TextStyle::Monospace.resolve(ui.style());
let indentation = 2.0 * 4.0 * ui.fonts(|f| f.glyph_width(&font_id, ' '));
let indentation = 2.0 * 4.0 * ui.fonts_mut(|f| f.glyph_width(&font_id, ' '));
ui.add_space(indentation);
egui::Grid::new("code_samples")

View File

@@ -141,9 +141,8 @@ fn char_info_ui(ui: &mut egui::Ui, chr: char, glyph_info: &GlyphInfo, font_id: e
}
fn available_characters(ui: &egui::Ui, family: egui::FontFamily) -> BTreeMap<char, GlyphInfo> {
ui.fonts(|f| {
f.lock()
.fonts
ui.fonts_mut(|f| {
f.fonts
.font(&egui::FontId::new(10.0, family)) // size is arbitrary for getting the characters
.characters()
.iter()

View File

@@ -213,7 +213,7 @@ fn label_ui(ui: &mut egui::Ui) {
ui.horizontal_wrapped(|ui| {
// Trick so we don't have to add spaces in the text below:
let width = ui.fonts(|f|f.glyph_width(&TextStyle::Body.resolve(ui.style()), ' '));
let width = ui.fonts_mut(|f|f.glyph_width(&TextStyle::Body.resolve(ui.style()), ' '));
ui.spacing_mut().item_spacing.x = width;
ui.label(RichText::new("Text can have").color(Color32::from_rgb(110, 255, 110)));
@@ -792,7 +792,7 @@ impl TextRotation {
let start_pos = self.size / 2.0;
let s = ui.ctx().fonts(|f| {
let s = ui.ctx().fonts_mut(|f| {
let mut t = egui::Shape::text(
f,
rect.min + start_pos,

View File

@@ -191,7 +191,7 @@ fn huge_content_painter(ui: &mut egui::Ui) {
ui.add_space(4.0);
let font_id = TextStyle::Body.resolve(ui.style());
let row_height = ui.fonts(|f| f.row_height(&font_id)) + ui.spacing().item_spacing.y;
let row_height = ui.fonts_mut(|f| f.row_height(&font_id)) + ui.spacing().item_spacing.y;
let num_rows = 10_000;
ScrollArea::vertical()

View File

@@ -83,7 +83,7 @@ impl EasyMarkEditor {
let mut layouter = |ui: &egui::Ui, easymark: &dyn TextBuffer, wrap_width: f32| {
let mut layout_job = highlighter.highlight(ui.style(), easymark.as_str());
layout_job.wrap.max_width = wrap_width;
ui.fonts(|f| f.layout_job(layout_job))
ui.fonts_mut(|f| f.layout_job(layout_job))
};
ui.add(

View File

@@ -162,7 +162,7 @@ fn bullet_point(ui: &mut Ui, width: f32) -> Response {
fn numbered_point(ui: &mut Ui, width: f32, number: &str) -> Response {
let font_id = TextStyle::Body.resolve(ui.style());
let row_height = ui.fonts(|f| f.row_height(&font_id));
let row_height = ui.fonts_mut(|f| f.row_height(&font_id));
let (rect, response) = ui.allocate_exact_size(vec2(width, row_height), Sense::hover());
let text = format!("{number}.");
let text_color = ui.visuals().strong_text_color();

View File

@@ -299,7 +299,7 @@ impl Shape {
#[expect(clippy::needless_pass_by_value)]
pub fn text(
fonts: &Fonts,
fonts: &mut Fonts,
pos: Pos2,
anchor: Align2,
text: impl ToString,

View File

@@ -181,7 +181,7 @@ mod tests {
#[test]
fn text_bounding_box_under_rotation() {
let fonts = Fonts::new(
let mut fonts = Fonts::new(
1.0,
1024,
AlphaFromCoverage::default(),
@@ -190,7 +190,7 @@ mod tests {
let font = FontId::monospace(12.0);
let mut t = crate::Shape::text(
&fonts,
&mut fonts,
Pos2::ZERO,
emath::Align2::CENTER_CENTER,
"testing123",

View File

@@ -2,7 +2,7 @@ use std::{collections::BTreeMap, sync::Arc};
use crate::{
AlphaFromCoverage, TextureAtlas,
mutex::{Mutex, MutexGuard},
mutex::Mutex,
text::{
Galley, LayoutJob, LayoutSection,
font::{Font, FontImpl},
@@ -418,8 +418,10 @@ impl FontDefinitions {
/// If you are using `egui`, use `egui::Context::set_fonts` and `egui::Context::fonts`.
///
/// You need to call [`Self::begin_pass`] and [`Self::font_image_delta`] once every frame.
#[derive(Clone)]
pub struct Fonts(Arc<Mutex<FontsAndCache>>);
pub struct Fonts {
pub fonts: FontsImpl,
galley_cache: GalleyCache,
}
impl Fonts {
/// Create a new [`Fonts`] for text layout.
@@ -433,7 +435,7 @@ impl Fonts {
text_alpha_from_coverage: AlphaFromCoverage,
definitions: FontDefinitions,
) -> Self {
let fonts_and_cache = FontsAndCache {
Self {
fonts: FontsImpl::new(
pixels_per_point,
max_texture_side,
@@ -441,8 +443,7 @@ impl Fonts {
definitions,
),
galley_cache: Default::default(),
};
Self(Arc::new(Mutex::new(fonts_and_cache)))
}
}
/// Call at the start of each frame with the latest known
@@ -453,27 +454,25 @@ impl Fonts {
/// This function will react to changes in `pixels_per_point`, `max_texture_side`, and `text_alpha_from_coverage`,
/// as well as notice when the font atlas is getting full, and handle that.
pub fn begin_pass(
&self,
&mut self,
pixels_per_point: f32,
max_texture_side: usize,
text_alpha_from_coverage: AlphaFromCoverage,
) {
let mut fonts_and_cache = self.0.lock();
let pixels_per_point_changed = fonts_and_cache.fonts.pixels_per_point != pixels_per_point;
let max_texture_side_changed = fonts_and_cache.fonts.max_texture_side != max_texture_side;
let pixels_per_point_changed = self.fonts.pixels_per_point != pixels_per_point;
let max_texture_side_changed = self.fonts.max_texture_side != max_texture_side;
let text_alpha_from_coverage_changed =
fonts_and_cache.fonts.atlas.lock().text_alpha_from_coverage != text_alpha_from_coverage;
let font_atlas_almost_full = fonts_and_cache.fonts.atlas.lock().fill_ratio() > 0.8;
self.fonts.atlas.lock().text_alpha_from_coverage != text_alpha_from_coverage;
let font_atlas_almost_full = self.fonts.atlas.lock().fill_ratio() > 0.8;
let needs_recreate = pixels_per_point_changed
|| max_texture_side_changed
|| text_alpha_from_coverage_changed
|| font_atlas_almost_full;
if needs_recreate {
let definitions = fonts_and_cache.fonts.definitions.clone();
let definitions = self.fonts.definitions.clone();
*fonts_and_cache = FontsAndCache {
*self = Self {
fonts: FontsImpl::new(
pixels_per_point,
max_texture_side,
@@ -484,83 +483,70 @@ impl Fonts {
};
}
fonts_and_cache.galley_cache.flush_cache();
self.galley_cache.flush_cache();
}
/// Call at the end of each frame (before painting) to get the change to the font texture since last call.
pub fn font_image_delta(&self) -> Option<crate::ImageDelta> {
self.lock().fonts.atlas.lock().take_delta()
}
/// Access the underlying [`FontsAndCache`].
#[doc(hidden)]
#[inline]
pub fn lock(&self) -> MutexGuard<'_, FontsAndCache> {
self.0.lock()
self.fonts.atlas.lock().take_delta()
}
#[inline]
pub fn pixels_per_point(&self) -> f32 {
self.lock().fonts.pixels_per_point
self.fonts.pixels_per_point
}
#[inline]
pub fn max_texture_side(&self) -> usize {
self.lock().fonts.max_texture_side
self.fonts.max_texture_side
}
/// The font atlas.
/// Pass this to [`crate::Tessellator`].
pub fn texture_atlas(&self) -> Arc<Mutex<TextureAtlas>> {
self.lock().fonts.atlas.clone()
self.fonts.atlas.clone()
}
/// The full font atlas image.
#[inline]
pub fn image(&self) -> crate::ColorImage {
self.lock().fonts.atlas.lock().image().clone()
self.fonts.atlas.lock().image().clone()
}
/// Current size of the font image.
/// Pass this to [`crate::Tessellator`].
pub fn font_image_size(&self) -> [usize; 2] {
self.lock().fonts.atlas.lock().size()
self.fonts.atlas.lock().size()
}
/// Width of this character in points.
#[inline]
pub fn glyph_width(&self, font_id: &FontId, c: char) -> f32 {
self.lock().fonts.glyph_width(font_id, c)
pub fn glyph_width(&mut self, font_id: &FontId, c: char) -> f32 {
self.fonts.glyph_width(font_id, c)
}
/// Can we display this glyph?
#[inline]
pub fn has_glyph(&self, font_id: &FontId, c: char) -> bool {
self.lock().fonts.has_glyph(font_id, c)
pub fn has_glyph(&mut self, font_id: &FontId, c: char) -> bool {
self.fonts.has_glyph(font_id, c)
}
/// Can we display all the glyphs in this text?
pub fn has_glyphs(&self, font_id: &FontId, s: &str) -> bool {
self.lock().fonts.has_glyphs(font_id, s)
pub fn has_glyphs(&mut self, font_id: &FontId, s: &str) -> bool {
self.fonts.has_glyphs(font_id, s)
}
/// Height of one row of text in points.
///
/// Returns a value rounded to [`emath::GUI_ROUNDING`].
#[inline]
pub fn row_height(&self, font_id: &FontId) -> f32 {
self.lock().fonts.row_height(font_id)
pub fn row_height(&mut self, font_id: &FontId) -> f32 {
self.fonts.row_height(font_id)
}
/// List of all known font families.
pub fn families(&self) -> Vec<FontFamily> {
self.lock()
.fonts
.definitions
.families
.keys()
.cloned()
.collect()
self.fonts.definitions.families.keys().cloned().collect()
}
/// Layout some text.
@@ -571,12 +557,14 @@ impl Fonts {
///
/// The implementation uses memoization so repeated calls are cheap.
#[inline]
pub fn layout_job(&self, job: LayoutJob) -> Arc<Galley> {
self.lock().layout_job(job)
pub fn layout_job(&mut self, job: LayoutJob) -> Arc<Galley> {
let allow_split_paragraphs = true; // Optimization for editing text with many paragraphs.
self.galley_cache
.layout(&mut self.fonts, job, allow_split_paragraphs)
}
pub fn num_galleys_in_cache(&self) -> usize {
self.lock().galley_cache.num_galleys_in_cache()
self.galley_cache.num_galleys_in_cache()
}
/// How full is the font atlas?
@@ -584,14 +572,14 @@ impl Fonts {
/// This increases as new fonts and/or glyphs are used,
/// but can also decrease in a call to [`Self::begin_pass`].
pub fn font_atlas_fill_ratio(&self) -> f32 {
self.lock().fonts.atlas.lock().fill_ratio()
self.fonts.atlas.lock().fill_ratio()
}
/// Will wrap text at the given width and line break at `\n`.
///
/// The implementation uses memoization so repeated calls are cheap.
pub fn layout(
&self,
&mut self,
text: String,
font_id: FontId,
color: crate::Color32,
@@ -605,7 +593,7 @@ impl Fonts {
///
/// The implementation uses memoization so repeated calls are cheap.
pub fn layout_no_wrap(
&self,
&mut self,
text: String,
font_id: FontId,
color: crate::Color32,
@@ -618,7 +606,7 @@ impl Fonts {
///
/// The implementation uses memoization so repeated calls are cheap.
pub fn layout_delayed_color(
&self,
&mut self,
text: String,
font_id: FontId,
wrap_width: f32,
@@ -629,19 +617,6 @@ impl Fonts {
// ----------------------------------------------------------------------------
pub struct FontsAndCache {
pub fonts: FontsImpl,
galley_cache: GalleyCache,
}
impl FontsAndCache {
fn layout_job(&mut self, job: LayoutJob) -> Arc<Galley> {
let allow_split_paragraphs = true; // Optimization for editing text with many paragraphs.
self.galley_cache
.layout(&mut self.fonts, job, allow_split_paragraphs)
}
}
// ----------------------------------------------------------------------------
/// The collection of fonts used by `epaint`.

View File

@@ -184,7 +184,7 @@ impl LayoutJob {
/// The height of the tallest font used in the job.
///
/// Returns a value rounded to [`emath::GUI_ROUNDING`].
pub fn font_height(&self, fonts: &crate::Fonts) -> f32 {
pub fn font_height(&self, fonts: &mut crate::Fonts) -> f32 {
let mut max_height = 0.0_f32;
for section in &self.sections {
max_height = max_height.max(fonts.row_height(&section.format.font_id));