From 17847d6947f41e9dbf0dcb60d6b4c2fc2e6e733f Mon Sep 17 00:00:00 2001 From: Jonathan Johnson Date: Sun, 3 Dec 2023 15:35:37 -0800 Subject: [PATCH] Various fixes/improvements - On Linux, `fm-match` is used to query for the default fonts. - DynamicComponents now have their own trait and can now be specified with a constant or dynamic. - Roboto Flex is now always loaded when the feature is enabled. Overriding the default sans serif font prefers the overridden value, then roboto, then the result of fc-match/fontdb's default. - Button now supports background colors being set on a transparent button. --- examples/nested-scroll.rs | 5 ++ src/context.rs | 13 ++++ src/styles.rs | 132 ++++++++++++++++++++------------------ src/widget.rs | 6 +- src/widgets/button.rs | 21 ++++-- src/widgets/style.rs | 4 +- src/window.rs | 65 +++++++++++++++++-- 7 files changed, 169 insertions(+), 77 deletions(-) diff --git a/examples/nested-scroll.rs b/examples/nested-scroll.rs index 0dc0358..7446639 100644 --- a/examples/nested-scroll.rs +++ b/examples/nested-scroll.rs @@ -1,14 +1,19 @@ +use gooey::styles::components::FontFamily; +use gooey::styles::FontFamilyList; use gooey::widget::MakeWidget; use gooey::Run; +use kludgine::cosmic_text::FamilyOwned; use kludgine::figures::units::Lp; fn main() -> gooey::Result { include_str!("./nested-scroll.rs") .vertical_scroll() + .with(&FontFamily, FontFamilyList::from(FamilyOwned::Monospace)) .height(Lp::inches(3)) .and( include_str!("./canvas.rs") .vertical_scroll() + .with(&FontFamily, FontFamilyList::from(FamilyOwned::Monospace)) .height(Lp::inches(3)), ) .into_rows() diff --git a/src/context.rs b/src/context.rs index 771c694..ed72737 100644 --- a/src/context.rs +++ b/src/context.rs @@ -1111,6 +1111,19 @@ impl<'context, 'window> WidgetContext<'context, 'window> { self.effective_styles.get(query, self) } + /// Queries the widget hierarchy for a single style component. + /// + /// This function traverses up the widget hierarchy looking for the + /// component being requested. If a matching component is found, it will be + /// returned. + #[must_use] + pub fn try_get( + &self, + query: &Component, + ) -> Option { + self.effective_styles.try_get(query, self) + } + pub(crate) fn handle(&self) -> WindowHandle { WindowHandle { kludgine: self.window.handle(), diff --git a/src/styles.rs b/src/styles.rs index c73c5a2..ceeb358 100644 --- a/src/styles.rs +++ b/src/styles.rs @@ -66,9 +66,13 @@ impl Styles { pub fn insert_dynamic( &mut self, name: &impl NamedComponent, - dynamic: impl Into, + dynamic: impl IntoDynamicComponentValue, ) { - self.insert(name, Component::Dynamic(dynamic.into())); + let component = match dynamic.into_dynamic_component() { + Value::Constant(dynamic) => Value::Constant(Component::Dynamic(dynamic)), + Value::Dynamic(dynamic) => Value::Dynamic(dynamic.map_each_cloned(Component::Dynamic)), + }; + self.insert(name, component); } /// Adds a [`Component`] for the name provided and returns self. @@ -91,7 +95,7 @@ impl Styles { pub fn with_dynamic( mut self, name: &C, - dynamic: impl Into, + dynamic: impl IntoDynamicComponentValue, ) -> Self { self.insert_dynamic(name, dynamic); self @@ -145,6 +149,23 @@ impl Styles { } } + /// Returns the component associated with the given name, if a value is + /// specified. + #[must_use] + pub fn try_get( + &self, + component: &Named, + context: &WidgetContext<'_, '_>, + ) -> Option + where + Named: ComponentDefinition + ?Sized, + { + self.0 + .components + .get(&component.name()) + .and_then(|component| Self::resolve_component(component, context)) + } + /// Returns the component associated with the given name, or if not found, /// returns the default value provided by the definition. #[must_use] @@ -156,10 +177,7 @@ impl Styles { where Named: ComponentDefinition + ?Sized, { - self.0 - .components - .get(&component.name()) - .and_then(|component| Self::resolve_component(component, context)) + self.try_get(component, context) .unwrap_or_else(|| component.default_value(context)) } @@ -186,6 +204,29 @@ impl Debug for Styles { } } +impl FromIterator<(ComponentName, Component)> for Styles { + fn from_iter>(iter: T) -> Self { + let iter = iter.into_iter(); + let mut styles = Self::with_capacity(iter.size_hint().0); + for (name, component) in iter { + styles.insert_named(name, component); + } + styles + } +} + +impl IntoIterator for Styles { + type IntoIter = hash_map::IntoIter>; + type Item = (ComponentName, Value); + + fn into_iter(self) -> Self::IntoIter { + Arc::try_unwrap(self.0) + .unwrap_or_else(|err| err.as_ref().clone()) + .components + .into_iter() + } +} + #[derive(Default, Clone)] struct StyleData { components: AHashMap>, @@ -226,51 +267,35 @@ where } } -impl FromIterator<(ComponentName, Component)> for Styles { - fn from_iter>(iter: T) -> Self { - let iter = iter.into_iter(); - let mut styles = Self::with_capacity(iter.size_hint().0); - for (name, component) in iter { - styles.insert_named(name, component); - } - styles +/// A type that can convert into a [`Value`] containing a [`DynamicComponent`]. +pub trait IntoDynamicComponentValue { + /// Returns this type converted into a dynamic component value. + fn into_dynamic_component(self) -> Value; +} + +impl IntoDynamicComponentValue for DynamicComponent { + fn into_dynamic_component(self) -> Value { + Value::Constant(self) } } -impl IntoIterator for Styles { - type IntoIter = hash_map::IntoIter>; - type Item = (ComponentName, Value); - - fn into_iter(self) -> Self::IntoIter { - Arc::try_unwrap(self.0) - .unwrap_or_else(|err| err.as_ref().clone()) - .components - .into_iter() +impl IntoDynamicComponentValue for T +where + T: ComponentDefinition + Clone + RefUnwindSafe + Send + Sync + 'static, +{ + fn into_dynamic_component(self) -> Value { + Value::Constant(DynamicComponent::from(self)) } } -// /// An iterator over the owned contents of a [`Styles`] instance. -// pub struct StylesIntoIter { -// main: hash_map::IntoIter>, -// } - -// impl Iterator for StylesIntoIter { -// type Item = (ComponentName, Value); - -// fn next(&mut self) -> Option { -// loop { -// if let Some((group, names)) = &mut self.names { -// if let Some((name, component)) = names.next() { -// return Some((ComponentName::new(group.clone(), name), component)); -// } -// self.names = None; -// } - -// let (group, names) = self.main.next()?; -// self.names = Some((group, names.into_iter())); -// } -// } -// } +impl IntoDynamicComponentValue for Dynamic +where + T: ComponentDefinition + Clone + RefUnwindSafe + Send + Sync + 'static, +{ + fn into_dynamic_component(self) -> Value { + Value::Dynamic(self.map_each_into()) + } +} /// A value of a style component. #[derive(Debug, Clone, PartialEq)] @@ -1019,21 +1044,6 @@ where } } -/// A type that represents a named component with a default value. -pub trait ComponentDefaultvalue: NamedComponent { - /// Returns the default value for this component. - fn default_component_value(&self, context: &WidgetContext<'_, '_>) -> Component; -} - -impl ComponentDefaultvalue for T -where - T: ComponentDefinition, -{ - fn default_component_value(&self, context: &WidgetContext<'_, '_>) -> Component { - self.default_value(context).into_component() - } -} - impl NamedComponent for ComponentName { fn name(&self) -> Cow<'_, ComponentName> { Cow::Borrowed(self) diff --git a/src/widget.rs b/src/widget.rs index 517c765..777f6cd 100644 --- a/src/widget.rs +++ b/src/widget.rs @@ -30,8 +30,8 @@ use crate::styles::components::{ TextSize7, TextSize8, }; use crate::styles::{ - ComponentDefinition, ContainerLevel, Dimension, DimensionRange, DynamicComponent, Edges, - IntoComponentValue, Styles, ThemePair, VisualOrder, + ComponentDefinition, ContainerLevel, Dimension, DimensionRange, Edges, IntoComponentValue, + IntoDynamicComponentValue, Styles, ThemePair, VisualOrder, }; use crate::tree::Tree; use crate::utils::IgnorePoison; @@ -712,7 +712,7 @@ pub trait MakeWidget: Sized { fn with_dynamic( self, name: &C, - dynamic: impl Into, + dynamic: impl IntoDynamicComponentValue, ) -> Style where Value: IntoComponentValue, diff --git a/src/widgets/button.rs b/src/widgets/button.rs index 68d7282..859c676 100644 --- a/src/widgets/button.rs +++ b/src/widgets/button.rs @@ -32,6 +32,7 @@ pub struct Button { pub on_click: Option>, /// The kind of button to draw. pub kind: Value, + focusable: bool, buttons_pressed: usize, cached_state: CacheState, active_colors: Option>, @@ -140,6 +141,7 @@ impl Button { active_colors: None, kind: Value::Constant(ButtonKind::default()), color_animation: AnimationHandle::default(), + focusable: true, } } @@ -162,6 +164,13 @@ impl Button { self } + /// Prevents focus being given to this button. + #[must_use] + pub fn prevent_focus(mut self) -> Self { + self.focusable = false; + self + } + fn invoke_on_click(&mut self, context: &WidgetContext<'_, '_>) { if context.enabled() { if let Some(on_click) = self.on_click.as_mut() { @@ -190,7 +199,9 @@ impl Button { ) -> ButtonColors { match visual_state { VisualState::Normal => ButtonColors { - background: Color::CLEAR_BLACK, + background: context + .try_get(&ButtonBackground) + .unwrap_or(Color::CLEAR_BLACK), foreground: context.get(&TextColor), outline: context.get(&ButtonOutline), }, @@ -205,7 +216,9 @@ impl Button { outline: context.get(&ButtonActiveOutline), }, VisualState::Disabled => ButtonColors { - background: Color::CLEAR_BLACK, + background: context + .try_get(&ButtonDisabledBackground) + .unwrap_or(Color::CLEAR_BLACK), foreground: context.theme().surface.on_color_variant, outline: context.get(&ButtonDisabledOutline), }, @@ -391,7 +404,7 @@ impl Widget for Button { } fn accept_focus(&mut self, context: &mut EventContext<'_, '_>) -> bool { - context.enabled() && context.get(&AutoFocusableControls).is_all() + self.focusable && context.enabled() && context.get(&AutoFocusableControls).is_all() } fn mouse_down( @@ -437,7 +450,7 @@ impl Widget for Button { if self.buttons_pressed == 0 { context.deactivate(); - if let Some(location) = location { + if let (true, Some(location)) = (self.focusable, location) { if Rect::from(context.last_layout().expect("must have been rendered").size) .contains(location) { diff --git a/src/widgets/style.rs b/src/widgets/style.rs index b4c3047..301d98b 100644 --- a/src/widgets/style.rs +++ b/src/widgets/style.rs @@ -8,7 +8,7 @@ use crate::styles::components::{ LineHeight8, TextSize, TextSize1, TextSize2, TextSize3, TextSize4, TextSize5, TextSize6, TextSize7, TextSize8, }; -use crate::styles::{ComponentDefinition, DynamicComponent, IntoComponentValue, Styles}; +use crate::styles::{ComponentDefinition, IntoComponentValue, IntoDynamicComponentValue, Styles}; use crate::value::{IntoValue, Value}; use crate::widget::{MakeWidget, WidgetRef, WrapperWidget}; @@ -58,7 +58,7 @@ impl Style { pub fn with_dynamic( mut self, name: &C, - dynamic: impl Into, + dynamic: impl IntoDynamicComponentValue, ) -> Style where Value: IntoComponentValue, diff --git a/src/window.rs b/src/window.rs index ce5f1db..81d427a 100644 --- a/src/window.rs +++ b/src/window.rs @@ -5,6 +5,7 @@ use std::ffi::OsStr; use std::ops::{Deref, DerefMut, Not}; use std::panic::{AssertUnwindSafe, UnwindSafe}; use std::path::Path; +use std::process::Command; use std::string::ToString; use std::sync::{MutexGuard, OnceLock}; @@ -18,7 +19,7 @@ use kludgine::app::winit::event::{ use kludgine::app::winit::keyboard::{Key, NamedKey}; use kludgine::app::winit::window; use kludgine::app::WindowBehavior as _; -use kludgine::cosmic_text::FamilyOwned; +use kludgine::cosmic_text::{Family, FamilyOwned}; use kludgine::figures::units::{Px, UPx}; use kludgine::figures::{IntoSigned, IntoUnsigned, Point, Ranged, Rect, ScreenScale, Size}; use kludgine::render::Drawing; @@ -534,34 +535,53 @@ where let theme = settings.theme.take().expect("theme always present"); let fontdb = graphics.font_system().db_mut(); + if let Some(FamilyOwned::Name(name)) = Graphics::inner_find_available_font_family(fontdb, &settings.serif_font_family) + .or_else(|| default_family(Family::Serif)) { fontdb.set_serif_family(name); } + + let bundled_font_name; + #[cfg(feature = "roboto-flex")] + { + fontdb.load_font_data(include_bytes!("../assets/RobotoFlex.ttf").to_vec()); + bundled_font_name = Some(String::from("Roboto Flex")); + } + #[cfg(not(feature = "roboto-flex"))] + { + bundled_font_name = None; + } + if let Some(FamilyOwned::Name(name)) = Graphics::inner_find_available_font_family(fontdb, &settings.sans_serif_font_family) + .or_else(|| { + if let Some(name) = bundled_font_name { + Some(FamilyOwned::Name(name)) + } else { + default_family(Family::SansSerif) + } + }) { fontdb.set_sans_serif_family(name); - } else { - #[cfg(feature = "roboto-flex")] - { - fontdb.load_font_data(include_bytes!("../assets/RobotoFlex.ttf").to_vec()); - fontdb.set_sans_serif_family("Roboto Flex"); - } } + if let Some(FamilyOwned::Name(name)) = Graphics::inner_find_available_font_family(fontdb, &settings.fantasy_font_family) + .or_else(|| default_family(Family::Fantasy)) { fontdb.set_fantasy_family(name); } if let Some(FamilyOwned::Name(name)) = Graphics::inner_find_available_font_family(fontdb, &settings.monospace_font_family) + .or_else(|| default_family(Family::Monospace)) { fontdb.set_monospace_family(name); } if let Some(FamilyOwned::Name(name)) = Graphics::inner_find_available_font_family(fontdb, &settings.cursive_font_family) + .or_else(|| default_family(Family::Cursive)) { fontdb.set_cursive_family(name); } @@ -1292,3 +1312,34 @@ impl Ranged for ThemeMode { const MAX: Self = Self::Dark; const MIN: Self = Self::Light; } + +#[cfg(any(target_os = "macos", target_os = "ios", target_os = "windows"))] +fn default_family(query: Family<'_>) -> Option { + // fontdb uses system APIs to determine these defaults. + None +} + +#[cfg(not(any(target_os = "macos", target_os = "ios", target_os = "windows")))] +fn default_family(query: Family<'_>) -> Option { + // fontdb does not yet support configuring itself automatically. We will try + // to use `fc-match` to query font config. Once this is supported, we can + // remove this functionality. + // + let query = match query { + Family::Serif => "serif", + Family::SansSerif => "sans", + Family::Cursive => "cursive", + Family::Fantasy => "fantasy", + Family::Monospace => "monospace", + Family::Name(_) => return None, + }; + + Command::new("fc-match") + .arg("-f") + .arg("%{family}") + .arg(query) + .output() + .ok() + .and_then(|output| String::from_utf8(output.stdout).ok()) + .map(FamilyOwned::Name) +}