diff --git a/CHANGELOG.md b/CHANGELOG.md index 0867e3e..cd32bd3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,6 +53,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `ColorExt::into_source_and_lightness` has been renamed to `ColorExt::into_hsl`, and its return type is now `Hsl` instead of the individual components. +- `Window::font_data_to_load` has been renamed to `fonts`, and now has the + `FontCollection` type. +- Several font-related functions have been moved from `GraphicsContext` to + `WidgetContext`: + + - `GraphicsContext::set_font_family()` + - `GraphicsContext::find_available_font_family()` + - `GraphicsContext::set_available_font_family()` ### Fixed @@ -218,6 +226,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `List` is a new widget that creates lists similar to HTML's `ol` and `ul` tags. - `Dynamic::try_lock()` is a panic-free version of `Dynamic::lock()`. +- `FontCollection` is a new type that can be used to load fonts at app/window + startup or at runtime. +- `Cushy::fonts()`returns a `FontCollection` that is loaded into all windows. +- `WidgetContext::loaded_font_faces()` returns a list of fonts that were loaded + for a given `LoadedFont`. +- `Graphics::font_system()` returns a reference to the underlying Cosmic Text + `FontSystem`. [plotters]: https://github.com/plotters-rs/plotters diff --git a/Cargo.lock b/Cargo.lock index 563c8d5..192e546 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -321,9 +321,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.89" +version = "1.0.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0ba8f7aaa012f30d5b2861462f6708eccd49c3c39863fe083a308035f63d723" +checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5" dependencies = [ "jobserver", "libc", @@ -1123,7 +1123,7 @@ checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" [[package]] name = "kludgine" version = "0.7.0" -source = "git+https://github.com/khonsulabs/kludgine#eff686f12918a52321a63c3cb2abbbd68be45068" +source = "git+https://github.com/khonsulabs/kludgine#f2bc0d6ea9ebe2709ab208723fc7a072cf87164f" dependencies = [ "ahash", "alot", diff --git a/Cargo.toml b/Cargo.toml index cee429d..e0aca89 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,7 +48,7 @@ nominals = "0.3.0" # [patch.crates-io] -# kludgine = { path = "../kludgine" } +# cosmic-text = { path = "../cosmic-text" } # appit = { path = "../appit" } # figures = { path = "../figures" } # alot = { git = "https://github.com/khonsulabs/alot" } diff --git a/examples/dynamic-fonts.rs b/examples/dynamic-fonts.rs new file mode 100644 index 0000000..1116a17 --- /dev/null +++ b/examples/dynamic-fonts.rs @@ -0,0 +1,56 @@ +use cushy::fonts::FontCollection; +use cushy::styles::components::{FontFamily, FontWeight, LineHeight, TextSize}; +use cushy::styles::{Component, DynamicComponent, FamilyOwned, FontFamilyList}; +use cushy::value::{Dynamic, Source}; +use cushy::widget::MakeWidget; +use cushy::widgets::input::InputValue; +use cushy::Run; +use figures::units::Px; + +fn main() -> cushy::Result<()> { + let file_path = Dynamic::::default(); + let fonts = FontCollection::default(); + let font_data = file_path.map_each(|path| std::fs::read(path).map_err(|err| err.to_string())); + let loaded_font = font_data.map_each({ + let fonts = fonts.clone(); + move |result| { + result + .as_ref() + .ok() + .map(|data| fonts.push_unloadable(data.to_vec())) + } + }); + let primary_family_name = DynamicComponent::new({ + let loaded_font = loaded_font.clone(); + move |context| { + let font = loaded_font.get_tracking_invalidate(context)?; + + let face = context.loaded_font_faces(&font).first()?; + Some(Component::custom(FontFamilyList::from(vec![ + FamilyOwned::Name(face.families[0].0.clone()), + ]))) + } + }); + let family_weight = DynamicComponent::new(move |context| { + let font = loaded_font.get_tracking_invalidate(context)?; + + let face = context.loaded_font_faces(&font).first()?; + + Some(Component::FontWeight(face.weight)) + }); + + let mut window = file_path + .into_input() + .validation(font_data.clone()) + .and( + "The quick brown fox jumps over the lazy dog." + .with(&TextSize, Px::new(36)) + .with(&LineHeight, Px::new(36)) + .with_dynamic(&FontFamily, primary_family_name) + .with_dynamic(&FontWeight, family_weight), + ) + .into_rows() + .into_window(); + window.fonts = fonts; + window.run() +} diff --git a/src/app.rs b/src/app.rs index 2b59727..c383d0f 100644 --- a/src/app.rs +++ b/src/app.rs @@ -3,6 +3,7 @@ use std::sync::{Arc, Mutex, MutexGuard}; use arboard::Clipboard; use kludgine::app::{AppEvent, AsApplication}; +use crate::fonts::FontCollection; use crate::utils::IgnorePoison; use crate::window::sealed::WindowCommand; use crate::window::WindowHandle; @@ -46,6 +47,7 @@ impl AsApplication> for PendingApp { #[derive(Clone)] pub struct Cushy { pub(crate) clipboard: Option>>, + pub(crate) fonts: FontCollection, } impl Cushy { @@ -54,6 +56,7 @@ impl Cushy { clipboard: Clipboard::new() .ok() .map(|clipboard| Arc::new(Mutex::new(clipboard))), + fonts: FontCollection::default(), } } @@ -65,6 +68,12 @@ impl Cushy { .as_ref() .map(|mutex| mutex.lock().ignore_poison()) } + + /// Returns the font collection that will be loaded in all Cushy windows. + #[must_use] + pub fn fonts(&self) -> &FontCollection { + &self.fonts + } } /// A type that is a Cushy application. diff --git a/src/context.rs b/src/context.rs index c0ce13a..6af6dbe 100644 --- a/src/context.rs +++ b/src/context.rs @@ -6,16 +6,18 @@ use figures::units::{Lp, Px, UPx}; use figures::{IntoSigned, Point, Px2D, Rect, Round, ScreenScale, Size, Zero}; use kludgine::app::winit::event::{Ime, MouseButton, MouseScrollDelta, TouchPhase}; use kludgine::app::winit::window::CursorIcon; +use kludgine::cosmic_text::FamilyOwned; use kludgine::shapes::{Shape, StrokeOptions}; use kludgine::{Color, Kludgine, KludgineId}; use crate::animation::ZeroToOne; -use crate::graphics::Graphics; +use crate::fonts::{LoadedFont, LoadedFontFace}; +use crate::graphics::{FontState, Graphics}; use crate::styles::components::{ CornerRadius, FontFamily, FontStyle, FontWeight, HighlightColor, LayoutOrder, LineHeight, Opacity, TextSize, WidgetBackground, }; -use crate::styles::{ComponentDefinition, Styles, Theme, ThemePair}; +use crate::styles::{ComponentDefinition, FontFamilyList, Styles, Theme, ThemePair}; use crate::tree::Tree; use crate::value::{IntoValue, Source, Value}; use crate::widget::{EventHandling, MountedWidget, RootBehavior, WidgetId, WidgetInstance}; @@ -563,6 +565,36 @@ impl<'context, 'clip, 'gfx, 'pass> GraphicsContext<'context, 'clip, 'gfx, 'pass> }) } + /// Sets the current font family. + pub fn set_font_family(&mut self, family: FamilyOwned) { + self.font_state.current_font_family = None; + self.gfx.set_font_family(family); + } + + /// Returns the currently set font family list. + pub fn current_family_list(&mut self) -> FontFamilyList { + self.font_state + .current_font_family + .clone() + .unwrap_or_else(|| FontFamilyList::from(vec![FamilyOwned::new(self.gfx.font_family())])) + } + + /// Returns the first font family in `list` that is currently in the font + /// system, or None if no font families match. + pub fn find_available_font_family(&mut self, list: &FontFamilyList) -> Option { + self.font_state.find_available_font_family(list) + } + + /// Sets the font family to the first family in `list`. + pub fn set_available_font_family(&mut self, list: &FontFamilyList) { + if self.font_state.current_font_family.as_ref() != Some(list) { + if let Some(family) = self.find_available_font_family(list) { + self.set_font_family(family); + } + self.font_state.current_font_family = Some(list.clone()); + } + } + /// Updates `self` to have `opacity`. /// /// This setting will be mixed with the current opacity value. @@ -636,8 +668,7 @@ impl<'context, 'clip, 'gfx, 'pass> GraphicsContext<'context, 'clip, 'gfx, 'pass> /// Applies the current style settings for font family, text size, font /// style, and font weight. pub fn apply_current_font_settings(&mut self) { - self.gfx - .set_available_font_family(&self.widget.get(&FontFamily)); + self.set_available_font_family(&self.widget.get(&FontFamily)); self.gfx.set_font_size(self.widget.get(&TextSize)); self.gfx.set_line_height(self.widget.get(&LineHeight)); self.gfx.set_font_style(self.widget.get(&FontStyle)); @@ -856,6 +887,7 @@ pub struct WidgetContext<'context> { theme: Cow<'context, ThemePair>, cursor: &'context mut CursorState, pending_state: PendingState<'context>, + font_state: &'context mut FontState, effective_styles: Styles, cache: WidgetCacheKey, } @@ -865,6 +897,7 @@ impl<'context> WidgetContext<'context> { current_node: MountedWidget, theme: &'context ThemePair, window: &'context mut dyn PlatformWindow, + font_state: &'context mut FontState, theme_mode: ThemeMode, cursor: &'context mut CursorState, ) -> Self { @@ -891,6 +924,7 @@ impl<'context> WidgetContext<'context> { }, cursor, current_node, + font_state, theme: Cow::Borrowed(theme), window, } @@ -902,6 +936,7 @@ impl<'context> WidgetContext<'context> { tree: self.tree.clone(), current_node: self.current_node.clone(), window: &mut *self.window, + font_state: &mut *self.font_state, theme: Cow::Borrowed(self.theme.as_ref()), pending_state: self.pending_state.borrowed(), cache: self.cache, @@ -940,6 +975,7 @@ impl<'context> WidgetContext<'context> { }, current_node, tree: self.tree.clone(), + font_state: &mut *self.font_state, window: &mut *self.window, theme, pending_state: self.pending_state.borrowed(), @@ -1197,6 +1233,16 @@ impl<'context> WidgetContext<'context> { pub fn cache_key(&self) -> WidgetCacheKey { self.cache } + + /// Returns a list of faces that were loaded from `font`, or an empty slice + /// if no faces were loaded. + #[must_use] + pub fn loaded_font_faces(&self, font: &LoadedFont) -> &[LoadedFontFace] { + self.font_state + .loaded_fonts + .get(&font.id()) + .map_or(&[], |ids| &ids.faces) + } } impl Drop for EventContext<'_> { diff --git a/src/fonts.rs b/src/fonts.rs new file mode 100644 index 0000000..5a560cc --- /dev/null +++ b/src/fonts.rs @@ -0,0 +1,129 @@ +//! Types for loading fonts to use in Cushy. +use std::sync::Arc; + +use alot::{LotId, Lots}; +use kludgine::cosmic_text::fontdb::{self, Language}; +use kludgine::cosmic_text::{Stretch, Style, Weight}; + +use crate::value::Dynamic; + +/// A collection of fonts that can be loaded into Cushy. +#[derive(Clone, Default, PartialEq)] +pub struct FontCollection(pub(crate) Dynamic); + +impl FontCollection { + /// Pushes a font that will be unloaded when the last clone of the + /// [`LoadedFont`] is dropped. + #[must_use] + pub fn push_unloadable(&self, font_data: Vec) -> LoadedFont { + LoadedFont(Arc::new(LoadedFontHandle { + collection: self.clone(), + id: self.push_inner(font_data), + })) + } + + /// Adds `font_data` to this collection and returns self. + #[must_use] + pub fn with(self, font_data: Vec) -> Self { + self.push(font_data); + self + } + + /// Pushes `font_data` into this collection. + pub fn push(&self, font_data: Vec) { + self.push_inner(font_data); + } + + fn push_inner(&self, font_data: Vec) -> LotId { + self.0.lock().fonts.push(Arc::new(font_data)) + } +} + +pub(crate) struct FontIter<'a> { + collection: *const (), + iter: alot::unordered::EntryIter<'a, Arc>>, +} + +impl<'a> Iterator for FontIter<'a> { + type Item = (LoadedFontId, &'a Arc>); + + fn next(&mut self) -> Option { + self.iter.next().map(|(id, data)| { + ( + LoadedFontId { + collection: self.collection, + id, + }, + data, + ) + }) + } +} + +#[derive(Default)] +pub(crate) struct FontCollectionData { + fonts: Lots>>, +} + +impl FontCollectionData { + pub(crate) fn fonts(&self, collection: &FontCollection) -> FontIter<'_> { + FontIter { + collection: collection.0.as_ptr(), + iter: self.fonts.entries(), + } + } +} + +#[derive(PartialEq)] +struct LoadedFontHandle { + collection: FontCollection, + id: LotId, +} + +impl Drop for LoadedFontHandle { + fn drop(&mut self) { + let mut collection = self.collection.0.lock(); + collection.fonts.remove(self.id); + } +} + +/// A handle to font data that has been loaded. +/// +/// This type can be cloned and uses reference counting to track when to release +/// the underlying font data. +/// +/// Font data is not parsed until it is loaded into a running Cushy window. To +/// find information about this handle, use +/// [`WidgetContext::loaded_font_faces()`](crate::context::WidgetContext::loaded_font_faces). +#[derive(PartialEq, Clone)] +pub struct LoadedFont(Arc); + +impl LoadedFont { + pub(crate) fn id(&self) -> LoadedFontId { + LoadedFontId { + collection: self.0.collection.0.as_ptr(), + id: self.0.id, + } + } +} +#[derive(Eq, PartialEq, Ord, PartialOrd, Clone, Copy)] +pub(crate) struct LoadedFontId { + pub(crate) collection: *const (), + pub(crate) id: LotId, +} + +/// Information about a [`LoadedFont`]. +#[derive(Debug)] +pub struct LoadedFontFace { + /// The font database ID for this face. + pub id: fontdb::ID, + /// The names of the families contained in this face, and the corresponding + /// language of the name. + pub families: Vec<(String, Language)>, + /// The weight of the font face. + pub weight: Weight, + /// The style of the font face. + pub style: Style, + /// The stretch of the font face. + pub stretch: Stretch, +} diff --git a/src/graphics.rs b/src/graphics.rs index 907736c..f97a167 100644 --- a/src/graphics.rs +++ b/src/graphics.rs @@ -1,11 +1,11 @@ use std::ops::{Deref, DerefMut}; -use ahash::AHashSet; use figures::units::{Px, UPx}; use figures::{ self, Fraction, IntoSigned, IntoUnsigned, Point, Rect, ScreenScale, ScreenUnit, Size, Zero, }; -use kludgine::cosmic_text::FamilyOwned; +use kempt::{map, Map}; +use kludgine::cosmic_text::{fontdb, FamilyOwned, FontSystem}; use kludgine::drawing::Renderer; use kludgine::shapes::Shape; use kludgine::text::{MeasuredText, Text, TextOrigin}; @@ -14,13 +14,14 @@ use kludgine::{ }; use crate::animation::ZeroToOne; +use crate::fonts::{FontCollection, LoadedFontFace, LoadedFontId}; use crate::styles::FontFamilyList; +use crate::value::{DynamicRead, Generation, Source}; /// A 2d graphics context pub struct Graphics<'clip, 'gfx, 'pass> { renderer: RenderContext<'clip, 'gfx, 'pass>, region: Rect, - font_state: &'clip mut FontState, pub(crate) opacity: ZeroToOne, } @@ -32,11 +33,10 @@ enum RenderContext<'clip, 'gfx, 'pass> { impl<'clip, 'gfx, 'pass> Graphics<'clip, 'gfx, 'pass> { /// Returns a new graphics context for the given [`Renderer`]. #[must_use] - pub fn new(renderer: Renderer<'gfx, 'pass>, font_state: &'clip mut FontState) -> Self { + pub fn new(renderer: Renderer<'gfx, 'pass>) -> Self { Self { region: renderer.clip_rect().into_signed(), renderer: RenderContext::Renderer(renderer), - font_state, opacity: ZeroToOne::ONE, } } @@ -66,28 +66,6 @@ impl<'clip, 'gfx, 'pass> Graphics<'clip, 'gfx, 'pass> { ) } - /// Sets the current font family. - pub fn set_font_family(&mut self, family: cosmic_text::FamilyOwned) { - self.font_state.current_font_family = None; - self.renderer.set_font_family(family); - } - - /// Returns the first font family in `list` that is currently in the font - /// system, or None if no font families match. - pub fn find_available_font_family(&mut self, list: &FontFamilyList) -> Option { - self.font_state.find_available_font_family(list) - } - - /// Sets the font family to the first family in `list`. - pub fn set_available_font_family(&mut self, list: &FontFamilyList) { - if self.font_state.current_font_family.as_ref() != Some(list) { - if let Some(family) = self.find_available_font_family(list) { - self.set_font_family(family); - } - self.font_state.current_font_family = Some(list.clone()); - } - } - /// Returns the underlying renderer. /// /// Note: Kludgine graphics contexts only support clipping. This type adds @@ -120,7 +98,6 @@ impl<'clip, 'gfx, 'pass> Graphics<'clip, 'gfx, 'pass> { Graphics { renderer: RenderContext::Clipped(self.renderer.clipped_to(new_clip)), region, - font_state: &mut *self.font_state, opacity: self.opacity, } } @@ -310,6 +287,11 @@ impl<'clip, 'gfx, 'pass> Graphics<'clip, 'gfx, 'pass> { self.renderer.draw_measured_text(text, origin); } + /// Returns a reference to the font system used to render. + pub fn font_system(&mut self) -> &mut FontSystem { + self.renderer.font_system() + } + /// Returns this renderer as a /// [`DrawingArea`](plotters::drawing::DrawingArea) compatible with the /// [plotters](https://github.com/plotters-rs/plotters) crate. @@ -358,26 +340,163 @@ impl<'gfx, 'pass> DerefMut for RenderContext<'_, 'gfx, 'pass> { } } +pub(crate) struct LoadedFontIds { + generation: usize, + pub(crate) faces: Vec, +} + pub struct FontState { - fonts: AHashSet, - current_font_family: Option, + app_fonts: FontCollection, + app_font_generation: Generation, + window_fonts: FontCollection, + window_font_generation: Generation, + pub(crate) loaded_fonts: Map, + font_generation: usize, + fonts: Map, + pub(crate) current_font_family: Option, } impl FontState { - pub fn new(db: &cosmic_text::fontdb::Database) -> Self { - let faces = db.faces(); - let mut fonts = AHashSet::with_capacity(faces.size_hint().0); - for (family, _) in faces.filter_map(|f| f.families.first()) { - fonts.insert(family.clone()); - } - Self { + pub fn new( + db: &mut cosmic_text::fontdb::Database, + window_fonts: FontCollection, + app_fonts: FontCollection, + ) -> Self { + let mut fonts = Map::new(); + Self::gather_available_family_names(&mut fonts, 0, db); + let mut state = Self { fonts, current_font_family: None, + window_font_generation: window_fonts.0.generation(), + window_fonts, + app_font_generation: app_fonts.0.generation(), + app_fonts, + font_generation: 0, + loaded_fonts: Map::new(), + }; + + state.update_fonts(db); + + state + } + + fn gather_available_family_names( + families: &mut Map, + generation: usize, + db: &cosmic_text::fontdb::Database, + ) { + for (family, _) in db.faces().filter_map(|f| f.families.first()) { + families + .entry(family) + .and_modify(|gen| *gen = generation) + .or_insert(generation); + } + + let mut i = 0; + while i < families.len() { + if families.field(i).expect("length checked").value == generation { + i += 1; + } else { + families.remove_by_index(i); + } } } - pub fn next_frame(&mut self) { + pub fn update_fonts(&mut self, db: &mut cosmic_text::fontdb::Database) -> bool { + let new_app_generation = self.app_fonts.0.generation(); + let app_fonts_changed = if self.app_font_generation == new_app_generation { + false + } else { + self.app_font_generation = new_app_generation; + true + }; + let new_window_generation = self.window_fonts.0.generation(); + let window_fonts_changed = if self.window_font_generation == new_window_generation { + false + } else { + self.window_font_generation = new_window_generation; + true + }; + + let changed = app_fonts_changed || window_fonts_changed; + if changed { + self.font_generation += 1; + + if app_fonts_changed { + Self::synchronize_font_list( + &mut self.loaded_fonts, + self.font_generation, + &self.app_fonts, + db, + ); + } + if window_fonts_changed { + Self::synchronize_font_list( + &mut self.loaded_fonts, + self.font_generation, + &self.window_fonts, + db, + ); + } + + // Remove all fonts that didn't have their generation touched. + let mut i = 0; + while i < self.loaded_fonts.len() { + let field = self.loaded_fonts.field(i).expect("length checked"); + let check_if_changed = (app_fonts_changed + && self.app_fonts.0.as_ptr() == field.key().collection) + || (window_fonts_changed + && self.window_fonts.0.as_ptr() == field.key().collection); + if !check_if_changed || field.value.generation == self.font_generation { + i += 1; + } else { + for face in self.loaded_fonts.remove_by_index(i).value.faces { + db.remove_face(face.id); + } + } + } + + Self::gather_available_family_names(&mut self.fonts, self.font_generation, db); + } + + changed + } + + fn synchronize_font_list( + loaded_fonts: &mut Map, + generation: usize, + collection: &FontCollection, + db: &mut cosmic_text::fontdb::Database, + ) { + for (font_id, data) in collection.0.read().fonts(collection) { + match loaded_fonts.entry(font_id) { + map::Entry::Occupied(mut entry) => { + entry.generation = generation; + } + map::Entry::Vacant(entry) => { + let faces = db + .load_font_source(fontdb::Source::Binary(data.clone())) + .into_iter() + .filter_map(|id| { + db.face(id).map(|face| LoadedFontFace { + id, + families: face.families.clone(), + weight: face.weight, + style: face.style, + stretch: face.stretch, + }) + }) + .collect(); + entry.insert(LoadedFontIds { generation, faces }); + } + } + } + } + + #[must_use] + pub fn next_frame(&mut self, db: &mut cosmic_text::fontdb::Database) -> bool { self.current_font_family = None; + self.update_fonts(db) } pub fn find_available_font_family(&self, list: &FontFamilyList) -> Option { diff --git a/src/lib.rs b/src/lib.rs index 8adb1f8..91ee6b7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,6 +16,7 @@ mod names; pub mod styles; mod app; pub mod debug; +pub mod fonts; mod tick; mod tree; pub mod value; diff --git a/src/value.rs b/src/value.rs index 29313d7..3ab8f47 100644 --- a/src/value.rs +++ b/src/value.rs @@ -892,6 +892,10 @@ impl Dynamic { })) } + pub(crate) fn as_ptr(&self) -> *const () { + Arc::as_ptr(&self.0).cast() + } + /// Returns a weak reference to this dynamic. /// /// This is powered by [`Arc`]/[`Weak`] and follows the same semantics for diff --git a/src/widgets/label.rs b/src/widgets/label.rs index e993e6f..8d2c226 100644 --- a/src/widgets/label.rs +++ b/src/widgets/label.rs @@ -10,6 +10,7 @@ use kludgine::{CanRenderTo, Color, DrawableExt}; use super::input::CowString; use crate::context::{GraphicsContext, LayoutContext, Trackable, WidgetContext}; use crate::styles::components::TextColor; +use crate::styles::FontFamilyList; use crate::value::{Dynamic, Generation, IntoReadOnly, ReadOnly, Value}; use crate::widget::{Widget, WidgetInstance}; use crate::window::WindowLocal; @@ -21,7 +22,7 @@ pub struct Label { /// The contents of the label. pub display: ReadOnly, displayed: String, - prepared_text: WindowLocal<(MeasuredText, Option, Px, Color)>, + prepared_text: WindowLocal, } impl Label @@ -44,14 +45,16 @@ where width: Px, ) -> &MeasuredText { let check_generation = self.display.generation(); + context.apply_current_font_settings(); + let current_families = context.current_family_list(); match self.prepared_text.get(context) { - Some((prepared, prepared_generation, prepared_width, prepared_color)) - if prepared.can_render_to(&context.gfx) - && *prepared_generation == check_generation - && *prepared_color == color - && *prepared_width == width => {} + Some(cache) + if cache.text.can_render_to(&context.gfx) + && cache.generation == check_generation + && cache.color == color + && cache.width == width + && cache.families == current_families => {} _ => { - context.apply_current_font_settings(); let measured = self.display.map(|text| { self.displayed.clear(); if let Err(err) = write!(&mut self.displayed, "{}", text.as_display(context)) { @@ -61,14 +64,22 @@ where .gfx .measure_text(Text::new(&self.displayed, color).wrap_at(width)) }); - self.prepared_text - .set(context, (measured, check_generation, width, color)); + self.prepared_text.set( + context, + LabelCacheKey { + text: measured, + generation: check_generation, + width, + color, + families: current_families, + }, + ); } } self.prepared_text .get(context) - .map(|(prepared, _, _, _)| prepared) + .map(|cache| &cache.text) .expect("always initialized") } } @@ -133,6 +144,15 @@ impl_make_widget!( ReadOnly => String ); +#[derive(Debug)] +struct LabelCacheKey { + text: MeasuredText, + generation: Option, + width: Px, + color: Color, + families: FontFamilyList, +} + /// A context-aware [`Display`] implementation. /// /// This trait is automatically implemented for all types that implement diff --git a/src/window.rs b/src/window.rs index d6c8a01..d3b2105 100644 --- a/src/window.rs +++ b/src/window.rs @@ -49,6 +49,7 @@ use crate::context::{ AsEventContext, EventContext, Exclusive, GraphicsContext, LayoutContext, Trackable, WidgetContext, }; +use crate::fonts::FontCollection; use crate::graphics::{FontState, Graphics}; use crate::styles::{Edges, FontFamilyList, ThemePair}; use crate::tree::Tree; @@ -472,9 +473,8 @@ where /// The list of font families to try to find when a [`FamilyOwned::Cursive`] /// font is requested. pub cursive_font_family: FontFamilyList, - /// A list of data buffers that contain font data to ensure are loaded - /// during drawing operations. - pub font_data_to_load: Vec>, + /// A collection of fonts that this window will load. + pub fonts: FontCollection, on_closed: Option, inner_size: Option>>, @@ -572,8 +572,8 @@ impl Window { /// rendering. /// /// All font families contained in `font_data` will be loaded. - pub fn loading_font(mut self, font_data: Vec) -> Self { - self.font_data_to_load.push(font_data); + pub fn loading_font(self, font_data: Vec) -> Self { + self.fonts.push(font_data); self } @@ -636,10 +636,12 @@ where fantasy_font_family: FontFamilyList::default(), monospace_font_family: FontFamilyList::default(), cursive_font_family: FontFamilyList::default(), - font_data_to_load: vec![ + fonts: { + let fonts = FontCollection::default(); #[cfg(feature = "roboto-flex")] - include_bytes!("../assets/RobotoFlex.ttf").to_vec(), - ], + fonts.push(include_bytes!("../assets/RobotoFlex.ttf").to_vec()); + fonts + }, } } } @@ -681,7 +683,7 @@ where inner_size: self.inner_size.unwrap_or_default(), theme: Some(self.theme), theme_mode: self.theme_mode, - font_data_to_load: self.font_data_to_load, + font_data_to_load: self.fonts, serif_font_family: self.serif_font_family, sans_serif_font_family: self.sans_serif_font_family, fantasy_font_family: self.fantasy_font_family, @@ -802,6 +804,7 @@ where previously_active, &self.current_theme, window, + &mut self.fonts, self.theme_mode.get(), &mut self.cursor, ), @@ -814,6 +817,7 @@ where default.clone(), &self.current_theme, window, + &mut self.fonts, self.theme_mode.get(), &mut self.cursor, ), @@ -832,6 +836,7 @@ where keyboard_activated, &self.current_theme, window, + &mut self.fonts, self.theme_mode.get(), &mut self.cursor, ), @@ -864,6 +869,7 @@ where managed, &self.current_theme, window, + &mut self.fonts, self.theme_mode.get(), &mut self.cursor, ), @@ -938,13 +944,10 @@ where fn load_fonts( settings: &mut sealed::WindowSettings, + app_fonts: FontCollection, fontdb: &mut fontdb::Database, ) -> FontState { - for font_to_load in settings.font_data_to_load.drain(..) { - fontdb.load_font_data(font_to_load); - } - - let fonts = FontState::new(fontdb); + let fonts = FontState::new(fontdb, settings.font_data_to_load.clone(), app_fonts); fonts.apply_font_family_list( &settings.serif_font_family, || default_family(Family::Serif), @@ -1017,6 +1020,7 @@ where target, &self.current_theme, window, + &mut self.fonts, self.theme_mode.get(), &mut self.cursor, ), @@ -1049,6 +1053,7 @@ where target, &self.current_theme, window, + &mut self.fonts, self.theme_mode.get(), &mut self.cursor, ), @@ -1122,7 +1127,11 @@ where inner_size.set(window.inner_size()); - let fonts = Self::load_fonts(&mut settings, graphics.font_system().db_mut()); + let fonts = Self::load_fonts( + &mut settings, + cushy.fonts.clone(), + graphics.font_system().db_mut(), + ); let theme_mode = match settings.theme_mode.take() { Some(Value::Dynamic(dynamic)) => { @@ -1201,17 +1210,22 @@ where ); let root_mode = self.constrain_window_resizing(resizable, &mut window, graphics); - self.fonts.next_frame(); + let fonts_changed = self.fonts.next_frame(graphics.font_system().db_mut()); + if fonts_changed { + println!("Rebuilding font system"); + graphics.rebuild_font_system(); + } let graphics = self.contents.new_frame(graphics); let mut context = GraphicsContext { widget: WidgetContext::new( self.root.clone(), &self.current_theme, &mut window, + &mut self.fonts, self.theme_mode.get(), &mut self.cursor, ), - gfx: Exclusive::Owned(Graphics::new(graphics, &mut self.fonts)), + gfx: Exclusive::Owned(Graphics::new(graphics)), }; if self.initial_frame { self.root @@ -1342,6 +1356,7 @@ where target, &self.current_theme, &mut window, + &mut self.fonts, self.theme_mode.get(), &mut self.cursor, ), @@ -1391,6 +1406,7 @@ where widget, &self.current_theme, &mut window, + &mut self.fonts, self.theme_mode.get(), &mut self.cursor, ), @@ -1430,6 +1446,7 @@ where widget, &self.current_theme, &mut window, + &mut self.fonts, self.theme_mode.get(), &mut self.cursor, ), @@ -1470,6 +1487,7 @@ where self.root.clone(), &self.current_theme, &mut window, + &mut self.fonts, self.theme_mode.get(), &mut self.cursor, ), @@ -1488,6 +1506,7 @@ where handler.clone(), &self.current_theme, &mut window, + &mut self.fonts, self.theme_mode.get(), &mut self.cursor, ), @@ -1521,6 +1540,7 @@ where self.root.clone(), &self.current_theme, &mut window, + &mut self.fonts, self.theme_mode.get(), &mut self.cursor, ), @@ -1557,6 +1577,7 @@ where self.root.clone(), &self.current_theme, &mut window, + &mut self.fonts, self.theme_mode.get(), &mut self.cursor, ), @@ -1575,6 +1596,7 @@ where hovered.clone(), &self.current_theme, &mut window, + &mut self.fonts, self.theme_mode.get(), &mut self.cursor, ), @@ -1616,6 +1638,7 @@ where handler, &self.current_theme, &mut window, + &mut self.fonts, self.theme_mode.get(), &mut self.cursor, ), @@ -1934,6 +1957,7 @@ pub(crate) mod sealed { use crate::app::Cushy; use crate::context::sealed::InvalidationStatus; + use crate::fonts::FontCollection; use crate::styles::{FontFamilyList, ThemePair}; use crate::value::{Dynamic, Value}; use crate::widget::OnceCallback; @@ -1960,7 +1984,7 @@ pub(crate) mod sealed { pub fantasy_font_family: FontFamilyList, pub monospace_font_family: FontFamilyList, pub cursive_font_family: FontFamilyList, - pub font_data_to_load: Vec>, + pub font_data_to_load: FontCollection, pub on_closed: Option, } @@ -2488,7 +2512,7 @@ impl CushyWindowBuilder { fantasy_font_family: FontFamilyList::default(), monospace_font_family: FontFamilyList::default(), cursive_font_family: FontFamilyList::default(), - font_data_to_load: Vec::default(), + font_data_to_load: FontCollection::default(), on_closed: None, }, );