Merge branch 'main' into button-fun

This commit is contained in:
Jonathan Johnson 2023-11-11 20:18:06 -08:00
commit 6220394df2
No known key found for this signature in database
GPG key ID: A66D6A34D6620579
22 changed files with 1729 additions and 296 deletions

10
Cargo.lock generated
View file

@ -105,7 +105,7 @@ dependencies = [
[[package]]
name = "appit"
version = "0.1.0"
source = "git+https://github.com/khonsulabs/appit#043bfe2c78524d6a06ed159289ea1cd7a62b0fec"
source = "git+https://github.com/khonsulabs/appit#5ed0d923ded6520950d14b3b869cbcac89452f5c"
dependencies = [
"raw-window-handle 0.5.2",
"winit",
@ -455,9 +455,9 @@ dependencies = [
[[package]]
name = "etagere"
version = "0.2.9"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5bf70b9ea3a235a7432b4f481854815e2d4fb2fe824c1f5fb09b8985dd06b3e9"
checksum = "fcf22f748754352918e082e0039335ee92454a5d62bcaf69b5e8daf5907d9644"
dependencies = [
"euclid",
"svg_fmt",
@ -481,7 +481,7 @@ checksum = "dd2e7510819d6fbf51a5545c8f922716ecfb14df168a3242f7d33e0239efe6a1"
[[package]]
name = "figures"
version = "0.1.0"
source = "git+https://github.com/khonsulabs/figures#f5b9ca5cf181b748897b269ad47d7a9f2d1f3eac"
source = "git+https://github.com/khonsulabs/figures#7b41393c44d4def606790e340c98450b603010b4"
dependencies = [
"bytemuck",
"euclid",
@ -880,7 +880,7 @@ checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc"
[[package]]
name = "kludgine"
version = "0.1.0"
source = "git+https://github.com/khonsulabs/kludgine#a88961b726101ef9bb46bdae4737308d2dcb12a0"
source = "git+https://github.com/khonsulabs/kludgine#a26299823498dccbbbb3c28abc820b660fcc1289"
dependencies = [
"ahash",
"alot",

View file

@ -46,6 +46,7 @@ fn main() -> gooey::Result {
Lp::points(300)..Lp::points(600),
Stack::rows(username_row.and(password_row).and(buttons)),
)
.scroll()
.centered()
.expand()
.run()

View file

@ -5,5 +5,6 @@ use gooey::Run;
fn main() -> gooey::Result {
Label::new(include_str!("../src/widgets/scroll.rs"))
.scroll()
.expand()
.run()
}

View file

@ -1,30 +1,128 @@
use gooey::styles::components::TextColor;
use gooey::styles::{ColorTheme, FixedTheme, InverseTheme, SurfaceTheme, Theme, ThemePair};
use std::str::FromStr;
use gooey::animation::ZeroToOne;
use gooey::styles::components::{TextColor, WidgetBackground};
use gooey::styles::{ColorSource, ColorTheme, FixedTheme, SurfaceTheme, Theme, ThemePair};
use gooey::value::{Dynamic, MapEach};
use gooey::widget::MakeWidget;
use gooey::widgets::label::LabelBackground;
use gooey::widgets::{Label, Stack};
use gooey::widgets::{Input, Label, Scroll, Slider, Stack, Themed};
use gooey::window::ThemeMode;
use gooey::Run;
use kludgine::Color;
const PRIMARY_HUE: f32 = 240.;
const SECONDARY_HUE: f32 = 0.;
const TERTIARY_HUE: f32 = 330.;
const ERROR_HUE: f32 = 30.;
fn main() -> gooey::Result {
let default_theme = ThemePair::default();
Stack::columns(
theme(default_theme.dark, "Dark")
.and(theme(default_theme.light, "Light"))
let (primary, primary_editor) = color_editor(PRIMARY_HUE, 0.8, "Primary");
let (secondary, secondary_editor) = color_editor(SECONDARY_HUE, 0.3, "Secondary");
let (tertiary, tertiary_editor) = color_editor(TERTIARY_HUE, 0.3, "Tertiary");
let (error, error_editor) = color_editor(ERROR_HUE, 0.8, "Error");
let (neutral, neutral_editor) = color_editor(PRIMARY_HUE, 0.001, "Neutral");
let (neutral_variant, neutral_variant_editor) =
color_editor(PRIMARY_HUE, 0.001, "Neutral Variant");
let (theme_mode, theme_switcher) = dark_mode_slider();
let default_theme = (
&primary,
&secondary,
&tertiary,
&error,
&neutral,
&neutral_variant,
)
.map_each(
|(primary, secondary, tertiary, error, neutral, neutral_variant)| {
ThemePair::from_sources(
*primary,
*secondary,
*tertiary,
*error,
*neutral,
*neutral_variant,
)
},
);
Themed::new(
default_theme.clone(),
Stack::columns(
Scroll::vertical(Stack::rows(
theme_switcher
.and(primary_editor)
.and(secondary_editor)
.and(tertiary_editor)
.and(error_editor)
.and(neutral_editor)
.and(neutral_variant_editor),
))
.and(theme(default_theme.map_each(|theme| theme.dark), "Dark"))
.and(theme(default_theme.map_each(|theme| theme.light), "Light"))
.and(fixed_themes(
default_theme.primary_fixed,
default_theme.secondary_fixed,
default_theme.tertiary_fixed,
default_theme.map_each(|theme| theme.primary_fixed),
default_theme.map_each(|theme| theme.secondary_fixed),
default_theme.map_each(|theme| theme.tertiary_fixed),
)),
),
)
.expand()
.into_window()
.with_theme_mode(theme_mode)
.run()
}
fn dark_mode_slider() -> (Dynamic<ThemeMode>, impl MakeWidget) {
let theme_mode = Dynamic::default();
(
theme_mode.clone(),
Stack::rows(Label::new("Theme Mode").and(Slider::<ThemeMode>::from_value(theme_mode))),
)
}
fn create_paired_string<T>(initial_value: T) -> (Dynamic<T>, Dynamic<String>)
where
T: ToString + PartialEq + FromStr + Default + Send + Sync + 'static,
{
let float = Dynamic::new(initial_value);
let text = float.map_each_unique(|f| f.to_string());
text.for_each(float.with_clone(|float| {
move |text: &String| {
let _result = float.try_update(text.parse().unwrap_or_default());
}
}));
(float, text)
}
fn color_editor(
initial_hue: f32,
initial_saturation: impl Into<ZeroToOne>,
label: &str,
) -> (Dynamic<ColorSource>, impl MakeWidget) {
let (hue, hue_text) = create_paired_string(initial_hue);
let (saturation, saturation_text) = create_paired_string(initial_saturation.into());
let color =
(&hue, &saturation).map_each(|(hue, saturation)| ColorSource::new(*hue, *saturation));
(
color,
Stack::rows(
Label::new(label)
.and(Slider::<f32>::new(hue, 0., 360.))
.and(Input::new(hue_text))
.and(Slider::<ZeroToOne>::from_value(saturation))
.and(Input::new(saturation_text)),
),
)
}
fn fixed_themes(
primary: FixedTheme,
secondary: FixedTheme,
tertiary: FixedTheme,
primary: Dynamic<FixedTheme>,
secondary: Dynamic<FixedTheme>,
tertiary: Dynamic<FixedTheme>,
) -> impl MakeWidget {
Stack::rows(
Label::new("Fixed")
@ -35,85 +133,118 @@ fn fixed_themes(
.expand()
}
fn fixed_theme(theme: FixedTheme, label: &str) -> impl MakeWidget {
fn fixed_theme(theme: Dynamic<FixedTheme>, label: &str) -> impl MakeWidget {
let color = theme.map_each(|theme| theme.color);
let on_color = theme.map_each(|theme| theme.on_color);
Stack::columns(
swatch(theme.color, &format!("{label} Fixed"), theme.on_color)
swatch(color.clone(), &format!("{label} Fixed"), on_color.clone())
.and(swatch(
theme.dim_color,
theme.map_each(|theme| theme.dim_color),
&format!("Dim {label}"),
theme.on_color,
on_color.clone(),
))
.and(swatch(
theme.on_color,
on_color.clone(),
&format!("On {label} Fixed"),
theme.color,
color.clone(),
))
.and(swatch(
theme.on_color_variant,
theme.map_each(|theme| theme.on_color_variant),
&format!("Variant On {label} Fixed"),
theme.color,
color,
)),
)
.expand()
}
fn theme(theme: Theme, label: &str) -> impl MakeWidget {
fn theme(theme: Dynamic<Theme>, label: &str) -> impl MakeWidget {
Stack::rows(
Label::new(label)
.and(
Stack::columns(
color_theme(theme.primary, "Primary")
.and(color_theme(theme.secondary, "Secondary"))
.and(color_theme(theme.tertiary, "Tertiary"))
.and(color_theme(theme.error, "Error")),
color_theme(theme.map_each(|theme| theme.primary), "Primary")
.and(color_theme(
theme.map_each(|theme| theme.secondary),
"Secondary",
))
.and(color_theme(
theme.map_each(|theme| theme.tertiary),
"Tertiary",
))
.and(color_theme(theme.map_each(|theme| theme.error), "Error")),
)
.expand(),
)
.and(surface_and_inverse_themes(theme.surface, theme.inverse)),
.and(surface_theme(theme.map_each(|theme| theme.surface))),
)
.expand()
}
fn surface_and_inverse_themes(theme: SurfaceTheme, inverse: InverseTheme) -> impl MakeWidget {
fn surface_theme(theme: Dynamic<SurfaceTheme>) -> impl MakeWidget {
let color = theme.map_each(|theme| theme.color);
let on_color = theme.map_each(|theme| theme.on_color);
Stack::rows(
Stack::columns(
swatch(theme.color, "Surface", theme.on_color)
.and(swatch(theme.dim_color, "Dim Surface", theme.on_color))
.and(swatch(theme.bright_color, "Bright Surface", theme.on_color)),
swatch(color.clone(), "Surface", on_color.clone())
.and(swatch(
theme.map_each(|theme| theme.dim_color),
"Dim Surface",
on_color.clone(),
))
.and(swatch(
theme.map_each(|theme| theme.bright_color),
"Bright Surface",
on_color.clone(),
)),
)
.expand()
.and(inverse_theme(inverse))
.and(
Stack::columns(
swatch(theme.lowest_container, "Lowest Container", theme.on_color)
.and(swatch(theme.low_container, "Low Container", theme.on_color))
.and(swatch(theme.container, "Container", theme.on_color))
.and(swatch(
theme.high_container,
"High Container",
theme.on_color,
))
.and(swatch(
theme.highest_container,
"Highest Container",
theme.on_color,
)),
swatch(
theme.map_each(|theme| theme.lowest_container),
"Lowest Container",
on_color.clone(),
)
.and(swatch(
theme.map_each(|theme| theme.low_container),
"Low Container",
on_color.clone(),
))
.and(swatch(
theme.map_each(|theme| theme.container),
"Container",
on_color.clone(),
))
.and(swatch(
theme.map_each(|theme| theme.high_container),
"High Container",
on_color.clone(),
))
.and(swatch(
theme.map_each(|theme| theme.highest_container),
"Highest Container",
on_color.clone(),
)),
)
.expand(),
)
.and(
Stack::columns(
swatch(theme.on_color, "On Surface", theme.color)
swatch(on_color.clone(), "On Surface", color.clone())
.and(swatch(
theme.on_color_variant,
theme.map_each(|theme| theme.on_color_variant),
"On Color Variant",
theme.color,
color.clone(),
))
.and(swatch(theme.outline, "Outline", theme.color))
.and(swatch(
theme.outline_variant,
theme.map_each(|theme| theme.outline),
"Outline",
color.clone(),
))
.and(swatch(
theme.map_each(|theme| theme.outline_variant),
"Outline Variant",
theme.color,
color,
)),
)
.expand(),
@ -122,42 +253,37 @@ fn surface_and_inverse_themes(theme: SurfaceTheme, inverse: InverseTheme) -> imp
.expand()
}
fn inverse_theme(theme: InverseTheme) -> impl MakeWidget {
Stack::columns(
swatch(theme.surface, "Inverse Surface", theme.on_surface)
.and(swatch(
theme.on_surface,
"On Inverse Surface",
theme.surface,
))
.and(swatch(theme.primary, "Inverse Primary", theme.surface)),
)
.expand()
}
fn color_theme(theme: ColorTheme, label: &str) -> impl MakeWidget {
fn color_theme(theme: Dynamic<ColorTheme>, label: &str) -> impl MakeWidget {
let color = theme.map_each(|theme| theme.color);
let on_color = theme.map_each(|theme| theme.on_color);
let container = theme.map_each(|theme| theme.container);
let on_container = theme.map_each(|theme| theme.on_container);
Stack::rows(
swatch(theme.color, label, theme.on_color)
.and(swatch(theme.on_color, &format!("On {label}"), theme.color))
swatch(color.clone(), label, on_color.clone())
.and(swatch(
theme.container,
&format!("{label} Container"),
theme.on_container,
on_color.clone(),
&format!("On {label}"),
color.clone(),
))
.and(swatch(
theme.on_container,
container.clone(),
&format!("{label} Container"),
on_container.clone(),
))
.and(swatch(
on_container,
&format!("On {label} Container"),
theme.container,
container,
)),
)
.expand()
}
fn swatch(background: Color, label: &str, text: Color) -> impl MakeWidget {
fn swatch(background: Dynamic<Color>, label: &str, text: Dynamic<Color>) -> impl MakeWidget {
Label::new(label)
.with(&TextColor, text)
.with(&WidgetBackground, background)
.fit_horizontally()
.fit_vertically()
.with(&TextColor, text)
.with(&LabelBackground, background)
.expand()
}

View file

@ -39,15 +39,18 @@
pub mod easings;
use std::fmt::Debug;
use std::ops::{ControlFlow, Deref};
use std::fmt::{Debug, Display};
use std::ops::{ControlFlow, Deref, Div, Mul};
use std::panic::{RefUnwindSafe, UnwindSafe};
use std::str::FromStr;
use std::sync::{Arc, Condvar, Mutex, MutexGuard, OnceLock, PoisonError};
use std::thread;
use std::time::{Duration, Instant};
use alot::{LotId, Lots};
use intentional::Cast;
use kempt::Set;
use kludgine::figures::Ranged;
use kludgine::Color;
use crate::animation::easings::Linear;
@ -672,6 +675,26 @@ impl LinearInterpolate for f64 {
}
}
impl LinearInterpolate for bool {
fn lerp(&self, target: &Self, percent: f32) -> Self {
if percent >= 0.5 {
*target
} else {
*self
}
}
}
impl PercentBetween for bool {
fn percent_between(&self, min: &Self, max: &Self) -> ZeroToOne {
if *min == *max || *self == *min {
ZeroToOne::ZERO
} else {
ZeroToOne::ONE
}
}
}
#[test]
fn integer_lerps() {
#[track_caller]
@ -703,6 +726,56 @@ impl LinearInterpolate for Color {
}
}
/// Calculates the ratio of one value against a minimum and maximum.
pub trait PercentBetween {
/// Return the percentage that `self` is between `min` and `max`.
fn percent_between(&self, min: &Self, max: &Self) -> ZeroToOne;
}
macro_rules! impl_percent_between {
($type:ident, $float:ident) => {
impl PercentBetween for $type {
fn percent_between(&self, min: &Self, max: &Self) -> ZeroToOne {
let range = *max - *min;
ZeroToOne::from(*self as $float / range as $float)
}
}
};
}
impl_percent_between!(u8, f32);
impl_percent_between!(u16, f32);
impl_percent_between!(u32, f32);
impl_percent_between!(u64, f32);
impl_percent_between!(u128, f64);
impl_percent_between!(usize, f64);
impl_percent_between!(i8, f32);
impl_percent_between!(i16, f32);
impl_percent_between!(i32, f32);
impl_percent_between!(i64, f32);
impl_percent_between!(i128, f64);
impl_percent_between!(isize, f64);
impl_percent_between!(f32, f32);
impl_percent_between!(f64, f64);
impl PercentBetween for Color {
fn percent_between(&self, min: &Self, max: &Self) -> ZeroToOne {
fn channel_percent(
value: Color,
min: Color,
max: Color,
func: impl Fn(Color) -> u8,
) -> ZeroToOne {
func(value).percent_between(&func(min), &func(max))
}
channel_percent(*self, *min, *max, Color::red)
* channel_percent(*self, *min, *max, Color::green)
* channel_percent(*self, *min, *max, Color::blue)
* channel_percent(*self, *min, *max, Color::alpha)
}
}
/// An `f32` that is clamped between 0.0 and 1.0 and cannot be NaN or Infinity.
///
/// Because of these restrictions, this type implements `Ord` and `Eq`.
@ -727,6 +800,12 @@ impl ZeroToOne {
Self(value.clamp(0., 1.))
}
/// Returns the difference between `self` and `other` as a positive number.
#[must_use]
pub fn difference_between(self, other: Self) -> Self {
Self((self.0 - other.0).abs())
}
/// Returns the contained floating point value.
#[must_use]
pub fn into_f32(self) -> f32 {
@ -734,6 +813,32 @@ impl ZeroToOne {
}
}
impl Display for ZeroToOne {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
Display::fmt(&self.0, f)
}
}
impl FromStr for ZeroToOne {
type Err = std::num::ParseFloatError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
s.parse().map(Self)
}
}
impl From<f32> for ZeroToOne {
fn from(value: f32) -> Self {
Self::new(value)
}
}
impl From<f64> for ZeroToOne {
fn from(value: f64) -> Self {
Self::new(value.cast())
}
}
impl Default for ZeroToOne {
fn default() -> Self {
Self::ZERO
@ -787,6 +892,33 @@ impl LinearInterpolate for ZeroToOne {
}
}
impl PercentBetween for ZeroToOne {
fn percent_between(&self, min: &Self, max: &Self) -> ZeroToOne {
self.0.percent_between(&min.0, &max.0)
}
}
impl Mul for ZeroToOne {
type Output = Self;
fn mul(self, rhs: Self) -> Self::Output {
Self(self.0 * rhs.0)
}
}
impl Div for ZeroToOne {
type Output = Self;
fn div(self, rhs: Self) -> Self::Output {
Self(self.0 / rhs.0)
}
}
impl Ranged for ZeroToOne {
const MAX: Self = Self::ONE;
const MIN: Self = Self::ZERO;
}
/// An easing function for customizing animations.
#[derive(Debug, Clone)]
pub enum EasingFunction {

View file

@ -1,4 +1,5 @@
//! Types that provide access to the Gooey runtime.
use std::borrow::Cow;
use std::ops::{Deref, DerefMut};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
@ -6,19 +7,18 @@ use std::sync::Arc;
use kludgine::app::winit::event::{
DeviceId, Ime, KeyEvent, MouseButton, MouseScrollDelta, TouchPhase,
};
use kludgine::app::winit::window;
use kludgine::figures::units::{Px, UPx};
use kludgine::figures::{IntoSigned, Point, Rect, Size};
use kludgine::figures::units::{Lp, Px, UPx};
use kludgine::figures::{IntoSigned, Point, Rect, ScreenScale, Size};
use kludgine::shapes::{Shape, StrokeOptions};
use kludgine::Kludgine;
use kludgine::{Color, Kludgine};
use crate::graphics::Graphics;
use crate::styles::components::{HighlightColor, VisualOrder};
use crate::styles::components::{HighlightColor, VisualOrder, WidgetBackground};
use crate::styles::{ComponentDefaultvalue, ComponentDefinition, Styles, Theme, ThemePair};
use crate::value::Dynamic;
use crate::value::{Dynamic, Value};
use crate::widget::{EventHandling, ManagedWidget, WidgetId, WidgetInstance, WidgetRef};
use crate::window::sealed::WindowCommand;
use crate::window::RunningWindow;
use crate::window::{RunningWindow, ThemeMode};
use crate::ConstraintLimit;
/// A context to an event function.
@ -349,6 +349,9 @@ impl<'context, 'window> EventContext<'context, 'window> {
///
/// This widget does not need to be focused.
pub fn advance_focus(&mut self, direction: VisualOrder) {
// TODO check to see if the current node has an explicit next_focus (or
// if we're going in the opposite direction, previous_focus).
self.pending_state.focus = self.next_focus_after(self.current_node.clone(), direction);
}
}
@ -447,6 +450,18 @@ impl<'context, 'window, 'clip, 'gfx, 'pass> GraphicsContext<'context, 'window, '
}
}
/// Strokes an outline around this widget's contents.
pub fn stroke_outline<Unit>(&mut self, color: Color, options: StrokeOptions<Unit>)
where
Unit: ScreenScale<Px = Px, Lp = Lp>,
{
let visible_rect = Rect::from(self.gfx.region().size - (Px(1), Px(1)));
let focus_ring =
Shape::stroked_rect(visible_rect, color, options.into_px(self.gfx.scale()));
self.gfx
.draw_shape(&focus_ring, Point::default(), None, None);
}
/// Renders the default focus ring for this widget.
///
/// To ensure the correct color is used, include [`HighlightColor`] in the
@ -457,14 +472,8 @@ impl<'context, 'window, 'clip, 'gfx, 'pass> GraphicsContext<'context, 'window, '
return;
}
let visible_rect = Rect::from(self.gfx.region().size - (Px(1), Px(1)));
let focus_ring = Shape::stroked_rect(
visible_rect,
styles.get(&HighlightColor, self),
StrokeOptions::default(),
);
self.gfx
.draw_shape(&focus_ring, Point::default(), None, None);
let color = styles.get(&HighlightColor, self);
self.stroke_outline::<Lp>(color, StrokeOptions::default());
}
/// Renders the default focus ring for this widget.
@ -485,6 +494,9 @@ impl<'context, 'window, 'clip, 'gfx, 'pass> GraphicsContext<'context, 'window, '
"redraw called without set_widget_layout"
);
let background = self.query_style(&WidgetBackground);
self.gfx.fill(background);
self.current_node
.tree
.note_widget_rendered(self.current_node.id());
@ -662,8 +674,9 @@ pub struct WidgetContext<'context, 'window> {
current_node: ManagedWidget,
redraw_status: &'context RedrawStatus,
window: &'context mut RunningWindow<'window>,
theme: &'context ThemePair,
theme: Cow<'context, ThemePair>,
pending_state: PendingState<'context>,
theme_mode: ThemeMode,
}
impl<'context, 'window> WidgetContext<'context, 'window> {
@ -672,6 +685,7 @@ impl<'context, 'window> WidgetContext<'context, 'window> {
redraw_status: &'context RedrawStatus,
theme: &'context ThemePair,
window: &'context mut RunningWindow<'window>,
theme_mode: ThemeMode,
) -> Self {
Self {
pending_state: PendingState::Owned(PendingWidgetState {
@ -686,7 +700,8 @@ impl<'context, 'window> WidgetContext<'context, 'window> {
}),
current_node,
redraw_status,
theme,
theme: Cow::Borrowed(theme),
theme_mode,
window,
}
}
@ -697,8 +712,9 @@ impl<'context, 'window> WidgetContext<'context, 'window> {
current_node: self.current_node.clone(),
redraw_status: self.redraw_status,
window: &mut *self.window,
theme: self.theme,
theme: Cow::Borrowed(self.theme.as_ref()),
pending_state: self.pending_state.borrowed(),
theme_mode: self.theme_mode,
}
}
@ -711,12 +727,26 @@ impl<'context, 'window> WidgetContext<'context, 'window> {
Widget: ManageWidget,
Widget::Managed: MapManagedWidget<WidgetContext<'child, 'window>>,
{
widget.manage(self).map(|current_node| WidgetContext {
current_node,
redraw_status: self.redraw_status,
window: &mut *self.window,
theme: self.theme,
pending_state: self.pending_state.borrowed(),
widget.manage(self).map(|current_node| {
let (theme, theme_mode) = current_node.overidden_theme();
let theme = if let Some(theme) = theme {
Cow::Owned(theme.get_tracked(self))
} else {
Cow::Borrowed(self.theme.as_ref())
};
let theme_mode = if let Some(mode) = theme_mode {
mode.get_tracked(self)
} else {
self.theme_mode
};
WidgetContext {
current_node,
redraw_status: self.redraw_status,
window: &mut *self.window,
theme,
pending_state: self.pending_state.borrowed(),
theme_mode,
}
})
}
@ -860,10 +890,24 @@ impl<'context, 'window> WidgetContext<'context, 'window> {
///
/// Style queries for children will return any values matching this
/// collection.
pub fn attach_styles(&self, styles: Styles) {
pub fn attach_styles(&self, styles: Value<Styles>) {
self.current_node.attach_styles(styles);
}
/// Attaches `theme` to the widget hierarchy for this widget.
///
/// All children nodes will access this theme in their contexts.
pub fn attach_theme(&self, theme: Value<ThemePair>) {
self.current_node.attach_theme(theme);
}
/// Attaches `theme_mode` to the widget hierarchy for this widget.
///
/// All children nodes will use this theme mode.
pub fn attach_theme_mode(&self, theme_mode: Value<ThemeMode>) {
self.current_node.attach_theme_mode(theme_mode);
}
/// Queries the widget hierarchy for matching style components.
///
/// This function traverses up the widget hierarchy looking for the
@ -878,7 +922,7 @@ impl<'context, 'window> WidgetContext<'context, 'window> {
pub fn query_styles(&self, query: &[&dyn ComponentDefaultvalue]) -> Styles {
self.current_node
.tree
.query_styles(&self.current_node, query)
.query_styles(&self.current_node, query, self)
}
/// Queries the widget hierarchy for a single style component.
@ -919,15 +963,24 @@ impl<'context, 'window> WidgetContext<'context, 'window> {
/// Returns the theme pair for the window.
#[must_use]
pub fn theme_pair(&self) -> &ThemePair {
self.theme
self.theme.as_ref()
}
/// Returns the current theme in either light or dark mode.
#[must_use]
pub fn theme(&self) -> &Theme {
match self.window.theme() {
window::Theme::Light => &self.theme.light,
window::Theme::Dark => &self.theme.dark,
match self.theme_mode {
ThemeMode::Light => &self.theme.light,
ThemeMode::Dark => &self.theme.dark,
}
}
/// Returns the opposite theme of [`Self::theme()`].
#[must_use]
pub fn inverse_theme(&self) -> &Theme {
match self.theme_mode {
ThemeMode::Light => &self.theme.dark,
ThemeMode::Dark => &self.theme.light,
}
}
}

View file

@ -48,10 +48,10 @@ impl ConstraintLimit {
}
/// Converts `measured` to unsigned pixels, and adjusts it according to the
/// contraint's intentions.
/// constraint's intentions.
///
/// If this constraint is of a known size, it will return the maximum of the
/// measured size and the contraint. If it is of an unknown size, it will
/// measured size and the constraint. If it is of an unknown size, it will
/// return the measured size.
pub fn fit_measured<Unit>(self, measured: Unit, scale: Fraction) -> UPx
where

View file

@ -459,7 +459,7 @@ where
fn from(value: RangeInclusive<T>) -> Self {
Self {
start: Bound::Included(value.start().clone().into()),
end: Bound::Excluded(value.end().clone().into()),
end: Bound::Included(value.end().clone().into()),
}
}
}
@ -572,9 +572,15 @@ where
pub struct Group(Name);
impl Group {
/// Returns a new group with `name`.
#[must_use]
pub fn new(name: impl Into<Cow<'static, str>>) -> Self {
Self(Name::new(name))
}
/// Returns a new instance using the group name of `T`.
#[must_use]
pub fn new<T>() -> Self
pub fn from_group<T>() -> Self
where
T: ComponentGroup,
{
@ -626,7 +632,7 @@ impl ComponentName {
/// Returns a new instance using `G` and `name`.
pub fn named<G: ComponentGroup>(name: impl Into<Name>) -> Self {
Self::new(Group::new::<G>(), name)
Self::new(Group::from_group::<G>(), name)
}
}
@ -921,9 +927,6 @@ pub struct Theme {
/// The theme to color surfaces.
pub surface: SurfaceTheme,
/// A theme of inverse colors to provide high contrast to other elements.
pub inverse: InverseTheme,
}
impl Theme {
@ -943,7 +946,6 @@ impl Theme {
tertiary: ColorTheme::light_from_source(tertiary),
error: ColorTheme::light_from_source(error),
surface: SurfaceTheme::light_from_sources(neutral, neutral_variant),
inverse: InverseTheme::light_from_sources(primary, neutral),
}
}
@ -963,7 +965,6 @@ impl Theme {
tertiary: ColorTheme::dark_from_source(tertiary),
error: ColorTheme::dark_from_source(error),
surface: SurfaceTheme::dark_from_sources(neutral, neutral_variant),
inverse: InverseTheme::dark_from_sources(primary, neutral),
}
}
}
@ -1106,40 +1107,6 @@ impl FixedTheme {
}
}
/// An inverse color theme for displaying highly contrasted elements.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct InverseTheme {
/// An inverse surface color.
pub surface: Color,
/// The default color for content atop an inverted surface.
pub on_surface: Color,
/// The inverted primary color.
pub primary: Color,
// TODO why not inverse for the other colorthemes?
}
impl InverseTheme {
/// Returns the light-mode, inverse theme for given sources.
#[must_use]
pub fn light_from_sources(primary: ColorSource, surface: ColorSource) -> Self {
Self {
surface: surface.color(30),
on_surface: surface.color(90),
primary: primary.color(80),
}
}
/// Returns the dark-mode, inverse theme for given sources.
#[must_use]
pub fn dark_from_sources(primary: ColorSource, surface: ColorSource) -> Self {
Self {
surface: surface.color(90),
on_surface: surface.color(10),
primary: primary.color(40),
}
}
}
/// A source for [`Color`]s.
///
/// This type is a combination of an [`OklabHue`] and a saturation ranging from
@ -1170,10 +1137,10 @@ impl ColorSource {
/// Returns a new source with the given hue (in degrees) and saturation (0.0
/// - 1.0).
#[must_use]
pub fn new(hue: f32, saturation: f32) -> Self {
pub fn new(hue: impl Into<OklabHue>, saturation: impl Into<ZeroToOne>) -> Self {
Self {
hue: OklabHue::new(hue),
saturation: ZeroToOne::new(saturation),
hue: hue.into(),
saturation: saturation.into(),
}
}
@ -1184,6 +1151,32 @@ impl ColorSource {
Okhsl::new(self.hue, *self.saturation, *lightness.into_lightness()).into_color();
Color::new_f32(rgb.red, rgb.blue, rgb.green, 1.0)
}
/// Calculates an approximate ratio between 0.0 and 1.0 of how contrasting
/// these colors are, with perfect constrast being two clors that are
/// opposite of each other on the hue circle and one fully desaturated and
/// the other fully saturated.
#[must_use]
pub fn contrast_between(self, other: Self) -> ZeroToOne {
let saturation_delta = self.saturation.difference_between(other.saturation);
let self_hue = self.hue.into_positive_degrees();
let other_hue = other.hue.into_positive_degrees();
// Calculate the shortest distance between the hues, taking into account
// that 0 and 359 are one degree apart.
let hue_delta = ZeroToOne::new(
if self_hue < other_hue {
let hue_delta_a = other_hue - self_hue;
let hue_delta_b = self_hue + 360. - other_hue;
hue_delta_a.min(hue_delta_b)
} else {
let hue_delta_a = self_hue - other_hue;
let hue_delta_b = other_hue + 360. - self_hue;
hue_delta_a.min(hue_delta_b)
} / 180.,
);
saturation_delta * hue_delta
}
}
/// A value that can represent the lightness of a color.
@ -1231,29 +1224,31 @@ pub trait ColorExt: Copy {
self.into_source_and_lightness().1
}
/// Returns the contrast between this color and the components provided.
///
/// To achieve a contrast of 1.0:
///
/// - `self`'s hue and `check_source.hue` must be 180 degrees apart.
/// - `self`'s saturation and `check_source.saturation` must be different by
/// 1.0.
/// - `self`'s lightness and `check_lightness` must be different by 1.0.
/// - `self`'s alpha and `check_alpha` must be different by 1.0.
///
/// The algorithm currently used is purposely left undocumented as it will
/// likely change. It should be a reasonable heuristic until someone smarter
/// than @ecton comes along.
fn contrast_between(
self,
check_source: ColorSource,
check_lightness: ZeroToOne,
check_alpha: ZeroToOne,
) -> ZeroToOne;
/// Returns the color in `others` that contrasts the most from `self`.
#[must_use]
fn most_contrasting(self, others: &[Self]) -> Self
where
Self: Copy,
{
// TODO this currently only checks lightness. We should probably factor
// in hue/saturation changes too.
let check = self.lightness();
let mut others = others.iter().copied();
let mut most_contrasting = others.next().expect("at least one comparison");
let mut most_contrast_amount = (*most_contrasting.lightness() - *check).abs();
for other in others {
let contrast_amount = (*other.lightness() - *check).abs();
if contrast_amount > most_contrast_amount {
most_contrasting = other;
most_contrast_amount = contrast_amount;
}
}
most_contrasting
}
Self: Copy;
}
impl ColorExt for Color {
@ -1268,4 +1263,44 @@ impl ColorExt for Color {
ZeroToOne::new(hsl.lightness * self.alpha_f32()),
)
}
fn contrast_between(
self,
check_source: ColorSource,
check_lightness: ZeroToOne,
check_alpha: ZeroToOne,
) -> ZeroToOne {
let (other_source, other_lightness) = self.into_source_and_lightness();
let lightness_delta = other_lightness.difference_between(check_lightness);
let source_change = check_source.contrast_between(other_source);
let other_alpha = ZeroToOne::new(self.alpha_f32());
let alpha_delta = check_alpha.difference_between(other_alpha);
lightness_delta * source_change * alpha_delta
}
fn most_contrasting(self, others: &[Self]) -> Self
where
Self: Copy,
{
let (check_source, check_lightness) = self.into_source_and_lightness();
let check_alpha = ZeroToOne::new(self.alpha_f32());
let mut others = others.iter().copied();
let mut most_contrasting = others.next().expect("at least one comparison");
let mut most_contrast_amount =
most_contrasting.contrast_between(check_source, check_lightness, check_alpha);
for other in others {
let contrast_amount =
other.contrast_between(check_source, check_lightness, check_alpha);
if contrast_amount > most_contrast_amount {
most_contrasting = other;
most_contrast_amount = contrast_amount;
}
}
most_contrasting
}
}

View file

@ -422,3 +422,57 @@ impl FocusableWidgets {
matches!(self, Self::OnlyTextual)
}
}
/// A [`Color`] to be used as a highlight color.
#[derive(Clone, Copy, Eq, PartialEq, Debug)]
pub struct WidgetBackground;
impl NamedComponent for WidgetBackground {
fn name(&self) -> Cow<'_, ComponentName> {
Cow::Owned(ComponentName::named::<Global>("widget_background_color"))
}
}
impl ComponentDefinition for WidgetBackground {
type ComponentType = Color;
fn default_value(&self, _context: &WidgetContext<'_, '_>) -> Color {
Color::CLEAR_WHITE
}
}
/// A [`Color`] to be used as an outline color.
#[derive(Clone, Copy, Eq, PartialEq, Debug)]
pub struct OutlineColor;
impl NamedComponent for OutlineColor {
fn name(&self) -> Cow<'_, ComponentName> {
Cow::Owned(ComponentName::named::<Global>("outline_color"))
}
}
impl ComponentDefinition for OutlineColor {
type ComponentType = Color;
fn default_value(&self, context: &WidgetContext<'_, '_>) -> Color {
context.theme().surface.outline
}
}
/// A [`Color`] to be used as an outline color.
#[derive(Clone, Copy, Eq, PartialEq, Debug)]
pub struct DisabledOutlineColor;
impl NamedComponent for DisabledOutlineColor {
fn name(&self) -> Cow<'_, ComponentName> {
Cow::Owned(ComponentName::named::<Global>("disabled_outline_color"))
}
}
impl ComponentDefinition for DisabledOutlineColor {
type ComponentType = Color;
fn default_value(&self, context: &WidgetContext<'_, '_>) -> Color {
context.theme().surface.outline_variant
}
}

View file

@ -7,8 +7,10 @@ use kludgine::figures::{Point, Rect};
use crate::context::WidgetContext;
use crate::styles::components::VisualOrder;
use crate::styles::{ComponentDefaultvalue, ComponentDefinition, ComponentType, Styles};
use crate::styles::{ComponentDefaultvalue, ComponentDefinition, ComponentType, Styles, ThemePair};
use crate::value::Value;
use crate::widget::{ManagedWidget, WidgetId, WidgetInstance};
use crate::window::ThemeMode;
#[derive(Clone, Default)]
pub struct Tree {
@ -31,6 +33,8 @@ impl Tree {
parent: parent.map(ManagedWidget::id),
layout: None,
styles: None,
theme: None,
theme_mode: None,
},
);
if widget.is_default() {
@ -277,20 +281,47 @@ impl Tree {
data.nodes.get(&id).expect("missing widget").parent
}
pub(crate) fn attach_styles(&self, id: WidgetId, styles: Styles) {
pub(crate) fn attach_styles(&self, id: WidgetId, styles: Value<Styles>) {
let mut data = self.data.lock().map_or_else(PoisonError::into_inner, |g| g);
data.nodes.get_mut(&id).expect("missing widget").styles = Some(styles);
}
pub(crate) fn attach_theme(&self, id: WidgetId, theme: Value<ThemePair>) {
let mut data = self.data.lock().map_or_else(PoisonError::into_inner, |g| g);
data.nodes.get_mut(&id).expect("missing widget").theme = Some(theme);
}
pub(crate) fn attach_theme_mode(&self, id: WidgetId, theme: Value<ThemeMode>) {
let mut data = self.data.lock().map_or_else(PoisonError::into_inner, |g| g);
data.nodes.get_mut(&id).expect("missing widget").theme_mode = Some(theme);
}
pub(crate) fn overriden_theme(
&self,
id: WidgetId,
) -> (Option<Value<ThemePair>>, Option<Value<ThemeMode>>) {
let data = self.data.lock().map_or_else(PoisonError::into_inner, |g| g);
(
data.nodes.get(&id).expect("missing widget").theme.clone(),
data.nodes
.get(&id)
.expect("missing widget")
.theme_mode
.clone(),
)
}
pub fn query_styles(
&self,
perspective: &ManagedWidget,
query: &[&dyn ComponentDefaultvalue],
context: &WidgetContext<'_, '_>,
) -> Styles {
self.data
.lock()
.map_or_else(PoisonError::into_inner, |g| g)
.query_styles(perspective.id(), query)
.query_styles(perspective.id(), query, context)
}
pub fn query_style<Component: ComponentDefinition>(
@ -391,19 +422,22 @@ impl TreeData {
&self,
mut perspective: WidgetId,
query: &[&dyn ComponentDefaultvalue],
context: &WidgetContext<'_, '_>,
) -> Styles {
let mut query = query.iter().map(|n| n.name()).collect::<Vec<_>>();
let mut resolved = Styles::new();
while !query.is_empty() {
let node = &self.nodes[&perspective];
if let Some(styles) = &node.styles {
query.retain(|name| {
if let Some(component) = styles.get_named(name) {
resolved.insert(name, component.clone());
false
} else {
true
}
styles.map_tracked(context, |styles| {
query.retain(|name| {
if let Some(component) = styles.get_named(name) {
resolved.insert(name, component.clone());
false
} else {
true
}
});
});
}
let Some(parent) = node.parent else { break };
@ -422,13 +456,21 @@ impl TreeData {
loop {
let node = &self.nodes[&perspective];
if let Some(styles) = &node.styles {
if let Some(component) = styles.get_named(&name) {
let Ok(value) = <Component::ComponentType>::try_from_component(component.get())
else {
break;
};
component.redraw_when_changed(context);
return value;
match styles.map_tracked(context, |styles| {
if let Some(component) = styles.get_named(&name) {
let Ok(value) =
<Component::ComponentType>::try_from_component(component.get())
else {
return Err(());
};
component.redraw_when_changed(context);
return Ok(Some(value));
}
Ok(None)
}) {
Ok(Some(value)) => return value,
Ok(None) => {}
Err(()) => break,
}
}
let Some(parent) = node.parent else { break };
@ -443,5 +485,7 @@ pub struct Node {
pub children: Vec<WidgetId>,
pub parent: Option<WidgetId>,
pub layout: Option<Rect<Px>>,
pub styles: Option<Styles>,
pub styles: Option<Value<Styles>>,
pub theme: Option<Value<ThemePair>>,
pub theme_mode: Option<Value<ThemeMode>>,
}

View file

@ -1,11 +1,15 @@
//! Types for storing and interacting with values in Widgets.
use std::fmt::Debug;
use std::cell::Cell;
use std::fmt::{Debug, Display};
use std::future::Future;
use std::ops::{Deref, DerefMut};
use std::panic::AssertUnwindSafe;
use std::sync::{Arc, Condvar, Mutex, MutexGuard, PoisonError};
use std::sync::{Arc, Condvar, Mutex, MutexGuard, PoisonError, TryLockError};
use std::task::{Poll, Waker};
use std::thread::ThreadId;
use intentional::Assert;
use crate::animation::{DynamicTransition, LinearInterpolate};
use crate::context::{WidgetContext, WindowHandle};
@ -30,21 +34,32 @@ impl<T> Dynamic<T> {
readers: 0,
wakers: Vec::new(),
}),
during_callback_state: Mutex::default(),
sync: AssertUnwindSafe(Condvar::new()),
}))
}
/// Maps the contents with read-only access.
///
/// # Panics
///
/// This function panics if this value is already locked by the current
/// thread.
pub fn map_ref<R>(&self, map: impl FnOnce(&T) -> R) -> R {
let state = self.state();
let state = self.state().expect("deadlocked");
map(&state.wrapped.value)
}
/// Maps the contents with exclusive access. Before returning from this
/// function, all observers will be notified that the contents have been
/// updated.
///
/// # Panics
///
/// This function panics if this value is already locked by the current
/// thread.
pub fn map_mut<R>(&self, map: impl FnOnce(&mut T) -> R) -> R {
self.0.map_mut(|value, _| map(value))
self.0.map_mut(|value, _| map(value)).expect("deadlocked")
}
/// Returns a new dynamic that is updated using `U::from(T.clone())` each
@ -99,6 +114,19 @@ impl<T> Dynamic<T> {
self.0.map_each(move |gen| map(&gen.value))
}
/// Creates a new dynamic value that contains the result of invoking `map`
/// each time this value is changed.
///
/// This version of `map_each` uses [`Dynamic::try_update`] to prevent
/// deadlocks and debounce dependent values.
pub fn map_each_unique<R, F>(&self, mut map: F) -> Dynamic<R>
where
F: for<'a> FnMut(&'a T) -> R + Send + 'static,
R: Send + PartialEq + 'static,
{
self.0.map_each_unique(move |gen| map(&gen.value))
}
/// A helper function that invokes `with_clone` with a clone of self. This
/// code may produce slightly more readable code.
///
@ -131,16 +159,43 @@ impl<T> Dynamic<T> {
}
/// Returns a clone of the currently contained value.
///
/// # Panics
///
/// This function panics if this value is already locked by the current
/// thread.
#[must_use]
pub fn get(&self) -> T
where
T: Clone,
{
self.0.get().value
self.0.get().expect("deadlocked").value
}
/// Returns a clone of the currently contained value.
///
/// `context` will be invalidated when the value is updated.
///
/// # Panics
///
/// This function panics if this value is already locked by the current
/// thread.
#[must_use]
pub fn get_tracked(&self, context: &WidgetContext<'_, '_>) -> T
where
T: Clone,
{
context.redraw_when_changed(self);
self.get()
}
/// Returns the currently stored value, replacing the current contents with
/// `T::default()`.
///
/// # Panics
///
/// This function panics if this value is already locked by the current
/// thread.
#[must_use]
pub fn take(&self) -> T
where
@ -151,6 +206,11 @@ impl<T> Dynamic<T> {
/// Checks if the currently stored value is different than `T::default()`,
/// and if so, returns `Some(self.take())`.
///
/// # Panics
///
/// This function panics if this value is already locked by the current
/// thread.
#[must_use]
pub fn take_if_not_default(&self) -> Option<T>
where
@ -168,44 +228,99 @@ impl<T> Dynamic<T> {
/// Replaces the contents with `new_value`, returning the previous contents.
/// Before returning from this function, all observers will be notified that
/// the contents have been updated.
///
/// # Panics
///
/// This function panics if this value is already locked by the current
/// thread.
#[must_use]
pub fn replace(&self, new_value: T) -> T {
self.0
.map_mut(|value, _| std::mem::replace(value, new_value))
.expect("deadlocked")
}
/// Stores `new_value` in this dynamic. Before returning from this function,
/// all observers will be notified that the contents have been updated.
///
/// # Panics
///
/// This function panics if this value is already locked by the current
/// thread.
pub fn set(&self, new_value: T) {
let _old = self.replace(new_value);
}
/// Updates this dynamic with `new_value`, but only if `new_value` is not
/// equal to the currently stored value.
pub fn update(&self, new_value: T)
///
/// Returns true if the value was updated.
///
/// # Panics
///
/// This function panics if this value is already locked by the current
/// thread.
pub fn update(&self, new_value: T) -> bool
where
T: PartialEq,
{
self.0.map_mut(|value, changed| {
if *value == new_value {
*changed = false;
} else {
*value = new_value;
}
});
self.0
.map_mut(|value, changed| {
if *value == new_value {
*changed = false;
false
} else {
*value = new_value;
true
}
})
.expect("deadlocked")
}
/// Attempt to store `new_value` in `self`. If the value cannot be stored
/// due to a deadlock, it is returned as an error.
///
/// Returns true if the value was updated.
pub fn try_update(&self, new_value: T) -> Result<bool, T>
where
T: PartialEq,
{
let cell = Cell::new(Some(new_value));
self.0
.map_mut(|value, changed| {
let new_value = cell.take().assert("only one callback will be invoked");
if *value == new_value {
*changed = false;
false
} else {
*value = new_value;
true
}
})
.map_err(|_| cell.take().assert("only one callback will be invoked"))
}
/// Returns a new reference-based reader for this dynamic value.
///
/// # Panics
///
/// This function panics if this value is already locked by the current
/// thread.
#[must_use]
pub fn create_reader(&self) -> DynamicReader<T> {
self.state().readers += 1;
self.state().expect("deadlocked").readers += 1;
DynamicReader {
source: self.0.clone(),
read_generation: self.0.state().wrapped.generation,
read_generation: self.0.state().expect("deadlocked").wrapped.generation,
}
}
/// Converts this [`Dynamic`] into a reader.
///
/// # Panics
///
/// This function panics if this value is already locked by the current
/// thread.
#[must_use]
pub fn into_reader(self) -> DynamicReader<T> {
self.create_reader()
@ -215,22 +330,32 @@ impl<T> Dynamic<T> {
///
/// This call will block until all other guards for this dynamic have been
/// dropped.
///
/// # Panics
///
/// This function panics if this value is already locked by the current
/// thread.
#[must_use]
pub fn lock(&self) -> DynamicGuard<'_, T> {
DynamicGuard {
guard: self.0.state(),
guard: self.0.state().expect("deadlocked"),
accessed_mut: false,
}
}
fn state(&self) -> MutexGuard<'_, State<T>> {
fn state(&self) -> Result<DynamicMutexGuard<'_, T>, DeadlockError> {
self.0.state()
}
/// Returns the current generation of the value.
///
/// # Panics
///
/// This function panics if this value is already locked by the current
/// thread.
#[must_use]
pub fn generation(&self) -> Generation {
self.state().wrapped.generation
self.state().expect("deadlocked").wrapped.generation
}
/// Returns a pending transition for this value to `new_value`.
@ -262,7 +387,7 @@ impl<T> Clone for Dynamic<T> {
impl<T> Drop for Dynamic<T> {
fn drop(&mut self) {
let state = self.state();
let state = self.state().expect("deadlocked");
if state.readers == 0 {
drop(state);
self.0.sync.notify_all();
@ -276,9 +401,47 @@ impl<T> From<Dynamic<T>> for DynamicReader<T> {
}
}
#[derive(Debug)]
struct DynamicMutexGuard<'a, T> {
dynamic: &'a DynamicData<T>,
guard: MutexGuard<'a, State<T>>,
}
impl<'a, T> Drop for DynamicMutexGuard<'a, T> {
fn drop(&mut self) {
let mut during_state = self
.dynamic
.during_callback_state
.lock()
.map_or_else(PoisonError::into_inner, |g| g);
*during_state = None;
drop(during_state);
self.dynamic.sync.notify_all();
}
}
impl<'a, T> Deref for DynamicMutexGuard<'a, T> {
type Target = State<T>;
fn deref(&self) -> &Self::Target {
&self.guard
}
}
impl<'a, T> DerefMut for DynamicMutexGuard<'a, T> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.guard
}
}
#[derive(Debug)]
struct LockState {
locked_thread: ThreadId,
}
#[derive(Debug)]
struct DynamicData<T> {
state: Mutex<State<T>>,
during_callback_state: Mutex<Option<LockState>>,
// The AssertUnwindSafe is only needed on Mac. For some reason on
// Mac OS, Condvar isn't RefUnwindSafe.
@ -286,27 +449,56 @@ struct DynamicData<T> {
}
impl<T> DynamicData<T> {
fn state(&self) -> MutexGuard<'_, State<T>> {
self.state
fn state(&self) -> Result<DynamicMutexGuard<'_, T>, DeadlockError> {
let mut during_sync = self
.during_callback_state
.lock()
.map_or_else(PoisonError::into_inner, |g| g)
.map_or_else(PoisonError::into_inner, |g| g);
let current_thread_id = std::thread::current().id();
let guard = loop {
match self.state.try_lock() {
Ok(g) => break g,
Err(TryLockError::Poisoned(poision)) => break poision.into_inner(),
Err(TryLockError::WouldBlock) => loop {
match &*during_sync {
Some(state) if state.locked_thread == current_thread_id => {
return Err(DeadlockError)
}
Some(_) => {
during_sync = self
.sync
.wait(during_sync)
.map_or_else(PoisonError::into_inner, |g| g);
}
None => break,
}
},
}
};
*during_sync = Some(LockState {
locked_thread: current_thread_id,
});
Ok(DynamicMutexGuard {
dynamic: self,
guard,
})
}
pub fn redraw_when_changed(&self, window: WindowHandle) {
let mut state = self.state();
let mut state = self.state().expect("deadlocked");
state.windows.push(window);
}
#[must_use]
pub fn get(&self) -> GenerationalValue<T>
pub fn get(&self) -> Result<GenerationalValue<T>, DeadlockError>
where
T: Clone,
{
self.state().wrapped.clone()
self.state().map(|state| state.wrapped.clone())
}
pub fn map_mut<R>(&self, map: impl FnOnce(&mut T, &mut bool) -> R) -> R {
let mut state = self.state();
pub fn map_mut<R>(&self, map: impl FnOnce(&mut T, &mut bool) -> R) -> Result<R, DeadlockError> {
let mut state = self.state()?;
let old = {
let state = &mut *state;
let mut changed = true;
@ -321,14 +513,14 @@ impl<T> DynamicData<T> {
self.sync.notify_all();
old
Ok(old)
}
pub fn for_each<F>(&self, map: F)
where
F: for<'a> FnMut(&'a GenerationalValue<T>) + Send + 'static,
{
let mut state = self.state();
let mut state = self.state().expect("deadlocked");
state.callbacks.push(Box::new(map));
}
@ -337,7 +529,7 @@ impl<T> DynamicData<T> {
F: for<'a> FnMut(&'a GenerationalValue<T>) -> R + Send + 'static,
R: Send + 'static,
{
let mut state = self.state();
let mut state = self.state().expect("deadlocked");
let initial_value = map(&state.wrapped);
let mapped_value = Dynamic::new(initial_value);
let returned = mapped_value.clone();
@ -349,6 +541,39 @@ impl<T> DynamicData<T> {
returned
}
pub fn map_each_unique<R, F>(&self, mut map: F) -> Dynamic<R>
where
F: for<'a> FnMut(&'a GenerationalValue<T>) -> R + Send + 'static,
R: PartialEq + Send + 'static,
{
let mut state = self.state().expect("deadlocked");
let initial_value = map(&state.wrapped);
let mapped_value = Dynamic::new(initial_value);
let returned = mapped_value.clone();
state
.callbacks
.push(Box::new(move |updated: &GenerationalValue<T>| {
let _deadlock = mapped_value.try_update(map(updated));
}));
returned
}
}
/// A deadlock occurred accessing a [`Dynamic`].
///
/// Currently Gooey is only able to detect deadlocks where a single thread tries
/// to lock the same [`Dynamic`] multiple times.
#[derive(Debug)]
pub struct DeadlockError;
impl std::error::Error for DeadlockError {}
impl Display for DeadlockError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("a deadlock was detected")
}
}
struct State<T> {
@ -412,7 +637,7 @@ struct GenerationalValue<T> {
/// notified of a change when this guard is dropped.
#[derive(Debug)]
pub struct DynamicGuard<'a, T> {
guard: MutexGuard<'a, State<T>>,
guard: DynamicMutexGuard<'a, T>,
accessed_mut: bool,
}
@ -450,28 +675,43 @@ impl<T> DynamicReader<T> {
/// Maps the contents of the dynamic value and returns the result.
///
/// This function marks the currently stored value as being read.
///
/// # Panics
///
/// This function panics if this value is already locked by the current
/// thread.
pub fn map_ref<R>(&mut self, map: impl FnOnce(&T) -> R) -> R {
let state = self.source.state();
let state = self.source.state().expect("deadlocked");
self.read_generation = state.wrapped.generation;
map(&state.wrapped.value)
}
/// Returns true if the dynamic has been modified since the last time the
/// value was accessed through this reader.
///
/// # Panics
///
/// This function panics if this value is already locked by the current
/// thread.
#[must_use]
pub fn has_updated(&self) -> bool {
self.source.state().wrapped.generation != self.read_generation
self.source.state().expect("deadlocked").wrapped.generation != self.read_generation
}
/// Returns a clone of the currently contained value.
///
/// This function marks the currently stored value as being read.
///
/// # Panics
///
/// This function panics if this value is already locked by the current
/// thread.
#[must_use]
pub fn get(&mut self) -> T
where
T: Clone,
{
let GenerationalValue { value, generation } = self.source.get();
let GenerationalValue { value, generation } = self.source.get().expect("deadlocked");
self.read_generation = generation;
value
}
@ -480,19 +720,42 @@ impl<T> DynamicReader<T> {
/// there are no remaining writers for the value.
///
/// Returns true if a newly updated value was discovered.
///
/// # Panics
///
/// This function panics if this value is already locked by the current
/// thread.
pub fn block_until_updated(&mut self) -> bool {
let mut state = self.source.state();
let mut deadlock_state = self
.source
.during_callback_state
.lock()
.map_or_else(PoisonError::into_inner, |g| g);
assert!(
deadlock_state
.as_ref()
.map_or(true, |state| state.locked_thread
!= std::thread::current().id()),
"deadlocked"
);
loop {
let state = self
.source
.state
.lock()
.map_or_else(PoisonError::into_inner, |g| g);
if state.wrapped.generation != self.read_generation {
return true;
} else if state.readers == Arc::strong_count(&self.source) {
return false;
}
drop(state);
state = self
// Wait for a notification of a change, which is synch
deadlock_state = self
.source
.sync
.wait(state)
.wait(deadlock_state)
.map_or_else(PoisonError::into_inner, |g| g);
}
}
@ -508,7 +771,7 @@ impl<T> DynamicReader<T> {
impl<T> Clone for DynamicReader<T> {
fn clone(&self) -> Self {
self.source.state().readers += 1;
self.source.state().expect("deadlocked").readers += 1;
Self {
source: self.source.clone(),
read_generation: self.read_generation,
@ -518,7 +781,7 @@ impl<T> Clone for DynamicReader<T> {
impl<T> Drop for DynamicReader<T> {
fn drop(&mut self) {
let mut state = self.source.state();
let mut state = self.source.state().expect("deadlocked");
state.readers -= 1;
}
}
@ -535,7 +798,7 @@ impl<'a, T> Future for BlockUntilUpdatedFuture<'a, T> {
type Output = bool;
fn poll(self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll<Self::Output> {
let mut state = self.0.source.state();
let mut state = self.0.source.state().expect("deadlocked");
if state.wrapped.generation != self.0.read_generation {
return Poll::Ready(true);
} else if state.readers == Arc::strong_count(&self.0.source) {
@ -669,6 +932,20 @@ impl<T> Value<T> {
}
}
/// Maps the current contents to `map` and returns the result.
///
/// If `self` is a dynamic, `context` will be invalidated when the value is
/// updated.
pub fn map_tracked<R>(&self, context: &WidgetContext<'_, '_>, map: impl FnOnce(&T) -> R) -> R {
match self {
Value::Constant(value) => map(value),
Value::Dynamic(dynamic) => {
context.redraw_when_changed(dynamic);
dynamic.map_ref(map)
}
}
}
/// Maps the current contents with exclusive access and returns the result.
pub fn map_mut<R>(&mut self, map: impl FnOnce(&mut T) -> R) -> R {
match self {
@ -685,6 +962,17 @@ impl<T> Value<T> {
self.map(Clone::clone)
}
/// Returns a clone of the currently stored value.
///
/// If `self` is a dynamic, `context` will be invalidated when the value is
/// updated.
pub fn get_tracked(&self, context: &WidgetContext<'_, '_>) -> T
where
T: Clone,
{
self.map_tracked(context, Clone::clone)
}
/// Returns the current generation of the data stored, if the contained
/// value is [`Dynamic`].
pub fn generation(&self) -> Option<Generation> {

View file

@ -16,11 +16,11 @@ use kludgine::figures::{IntoSigned, IntoUnsigned, Point, Rect, Size};
use crate::context::{AsEventContext, EventContext, GraphicsContext, LayoutContext};
use crate::styles::components::VisualOrder;
use crate::styles::{Component, NamedComponent, Styles};
use crate::styles::{IntoComponentValue, NamedComponent, Styles, ThemePair};
use crate::tree::Tree;
use crate::value::{IntoValue, Value};
use crate::widgets::{Align, Expand, Scroll, Style};
use crate::window::{RunningWindow, Window, WindowBehavior};
use crate::window::{RunningWindow, ThemeMode, Window, WindowBehavior};
use crate::{ConstraintLimit, Run};
/// A type that makes up a graphical user interface.
@ -448,7 +448,7 @@ pub trait MakeWidget: Sized {
/// Associates `styles` with this widget.
///
/// This is equivalent to `Style::new(styles, self)`.
fn with_styles(self, styles: impl Into<Styles>) -> Style
fn with_styles(self, styles: impl IntoValue<Styles>) -> Style
where
Self: Sized,
{
@ -456,7 +456,7 @@ pub trait MakeWidget: Sized {
}
/// Associates a style component with `self`.
fn with(self, name: &impl NamedComponent, component: impl Into<Component>) -> Style {
fn with(self, name: &impl NamedComponent, component: impl IntoComponentValue) -> Style {
let mut styles = Styles::new();
styles.insert(name, component);
Style::new(styles, self)
@ -957,10 +957,22 @@ impl ManagedWidget {
self.tree.parent(self.id()).is_some()
}
pub(crate) fn attach_styles(&self, styles: Styles) {
pub(crate) fn attach_styles(&self, styles: Value<Styles>) {
self.tree.attach_styles(self.id(), styles);
}
pub(crate) fn attach_theme(&self, theme: Value<ThemePair>) {
self.tree.attach_theme(self.id(), theme);
}
pub(crate) fn attach_theme_mode(&self, theme: Value<ThemeMode>) {
self.tree.attach_theme_mode(self.id(), theme);
}
pub(crate) fn overidden_theme(&self) -> (Option<Value<ThemePair>>, Option<Value<ThemeMode>>) {
self.tree.overriden_theme(self.id())
}
pub(crate) fn reset_child_layouts(&self) {
self.tree.reset_child_layouts(self.id());
}

View file

@ -6,11 +6,14 @@ mod canvas;
mod expand;
mod input;
pub mod label;
mod mode_switch;
mod resize;
pub mod scroll;
mod slider;
mod space;
pub mod stack;
mod style;
mod themed;
mod tilemap;
pub use align::Align;
@ -19,9 +22,12 @@ pub use canvas::Canvas;
pub use expand::Expand;
pub use input::Input;
pub use label::Label;
pub use mode_switch::ModeSwitch;
pub use resize::Resize;
pub use scroll::Scroll;
pub use slider::Slider;
pub use space::Space;
pub use stack::Stack;
pub use style::Style;
pub use themed::Themed;
pub use tilemap::TileMap;

View file

@ -5,17 +5,19 @@ use std::time::Duration;
use kludgine::app::winit::event::{ElementState, Ime, KeyEvent};
use kludgine::app::winit::keyboard::Key;
use kludgine::cosmic_text::{Action, Attrs, Buffer, Cursor, Edit, Editor, Metrics, Shaping};
use kludgine::figures::units::{Px, UPx};
use kludgine::cosmic_text::{
Action, Affinity, Attrs, Buffer, Cursor, Edit, Editor, Metrics, Shaping,
};
use kludgine::figures::units::{Lp, Px, UPx};
use kludgine::figures::{
FloatConversion, IntoSigned, IntoUnsigned, Point, Rect, ScreenScale, Size,
};
use kludgine::shapes::Shape;
use kludgine::shapes::{Shape, StrokeOptions};
use kludgine::text::TextOrigin;
use kludgine::{Color, Kludgine};
use crate::context::{EventContext, LayoutContext, WidgetContext};
use crate::styles::components::{HighlightColor, LineHeight, TextColor, TextSize};
use crate::styles::components::{HighlightColor, LineHeight, OutlineColor, TextColor, TextSize};
use crate::styles::Styles;
use crate::utils::ModifiersExt;
use crate::value::{Generation, IntoValue, Value};
@ -32,6 +34,8 @@ pub struct Input {
on_key: Option<Callback<KeyEvent, EventHandling>>,
editor: Option<LiveEditor>,
cursor_state: CursorState,
needs_to_select_all: bool,
mouse_buttons_down: usize,
}
impl Input {
@ -47,6 +51,8 @@ impl Input {
editor: None,
cursor_state: CursorState::default(),
on_key: None,
mouse_buttons_down: 0,
needs_to_select_all: true,
}
}
@ -100,6 +106,22 @@ impl Input {
fn styles(context: &WidgetContext<'_, '_>) -> Styles {
context.query_styles(&[&TextColor, &TextSize, &LineHeight])
}
fn select_all(&mut self) {
let Some(editor) = self.editor.as_mut().map(|editor| &mut editor.editor) else {
return;
};
if !editor.buffer().lines.is_empty() {
let line = editor.buffer().lines.len() - 1;
let end = Cursor::new_with_affinity(
line,
editor.buffer().lines[line].text().len(),
Affinity::After,
);
editor.set_cursor(end);
editor.set_select_opt(Some(Cursor::new_with_affinity(0, 0, Affinity::Before)));
}
}
}
impl Default for Input {
@ -132,7 +154,9 @@ impl Widget for Input {
_button: kludgine::app::winit::event::MouseButton,
context: &mut EventContext<'_, '_>,
) -> EventHandling {
self.mouse_buttons_down += 1;
context.focus();
self.needs_to_select_all = false;
let styles = context.query_styles(&[&TextColor]);
self.editor_mut(context.kludgine, &styles, &context.widget)
.action(
@ -166,12 +190,22 @@ impl Widget for Input {
context.set_needs_redraw();
}
fn mouse_up(
&mut self,
_location: Option<Point<Px>>,
_device_id: kludgine::app::winit::event::DeviceId,
_button: kludgine::app::winit::event::MouseButton,
_context: &mut EventContext<'_, '_>,
) {
self.mouse_buttons_down -= 1;
}
#[allow(clippy::too_many_lines)]
fn redraw(&mut self, context: &mut crate::context::GraphicsContext<'_, '_, '_, '_, '_>) {
self.cursor_state.update(context.elapsed());
let cursor_state = self.cursor_state;
let size = context.gfx.size();
let styles = context.query_styles(&[&TextColor, &HighlightColor]);
let styles = context.query_styles(&[&TextColor, &HighlightColor, &OutlineColor]);
let highlight = styles.get(&HighlightColor, context);
let editor = self.editor_mut(&mut context.gfx, &styles, &context.widget);
let cursor = editor.cursor();
@ -311,6 +345,9 @@ impl Widget for Input {
context.redraw_when_changed(context.window().focused());
}
}
} else {
let outline_color = styles.get(&OutlineColor, context);
context.stroke_outline::<Lp>(outline_color, StrokeOptions::default());
}
let text_color = styles.get(&TextColor, context);
@ -330,6 +367,10 @@ impl Widget for Input {
context: &mut LayoutContext<'_, '_, '_, '_, '_>,
) -> Size<UPx> {
let styles = context.query_styles(&[&TextColor]);
if self.needs_to_select_all {
self.needs_to_select_all = false;
self.select_all();
}
let editor = self.editor_mut(&mut context.graphics.gfx, &styles, &context.graphics.widget);
let buffer = editor.buffer_mut();
buffer.set_size(
@ -362,7 +403,7 @@ impl Widget for Input {
// "Keyboard input: {:?}. {:?}, {:?}",
// input.logical_key, input.text, input.physical_key
// );
let (text_changed, handled) = match (input.state, input.logical_key, input.text) {
let (text_changed, handled) = match (input.state, input.logical_key, input.text.as_deref()) {
(ElementState::Pressed, key @ (Key::Backspace | Key::Delete), _) => {
editor.action(
context.kludgine.font_system(),
@ -400,6 +441,12 @@ impl Widget for Input {
);
(false, HANDLED)
}
(state, _, Some("a")) if context.modifiers().primary() => {
if state.is_pressed() {
self.select_all();
}
(false, HANDLED)
}
(state, _, Some(text))
if !context.modifiers().primary()
&& text != "\t" // tab
@ -408,7 +455,7 @@ impl Widget for Input {
=>
{
if state.is_pressed() {
editor.insert_string(&text, None);
editor.insert_string(text, None);
}
(state.is_pressed(), HANDLED)
}
@ -456,6 +503,10 @@ impl Widget for Input {
}
fn focus(&mut self, context: &mut EventContext<'_, '_>) {
if self.mouse_buttons_down == 0 {
self.needs_to_select_all = true;
}
context.set_ime_allowed(true);
context.set_needs_redraw();
}

View file

@ -0,0 +1,31 @@
use crate::context::EventContext;
use crate::value::{IntoValue, Value};
use crate::widget::{MakeWidget, WidgetRef, WrapperWidget};
use crate::window::ThemeMode;
/// A widget that applies a set of [`Styles`] to all contained widgets.
#[derive(Debug)]
pub struct ModeSwitch {
mode: Value<ThemeMode>,
child: WidgetRef,
}
impl ModeSwitch {
/// Returns a new widget that applies `mode` to all of its children.
pub fn new(mode: impl IntoValue<ThemeMode>, child: impl MakeWidget) -> Self {
Self {
mode: mode.into_value(),
child: WidgetRef::new(child),
}
}
}
impl WrapperWidget for ModeSwitch {
fn child_mut(&mut self) -> &mut WidgetRef {
&mut self.child
}
fn mounted(&mut self, context: &mut EventContext<'_, '_>) {
context.attach_theme_mode(self.mode.clone());
}
}

View file

@ -1,3 +1,4 @@
use kludgine::figures::units::UPx;
use kludgine::figures::{Fraction, IntoSigned, IntoUnsigned, Rect, ScreenScale, Size};
use crate::context::{AsEventContext, LayoutContext};
@ -81,7 +82,12 @@ impl WrapperWidget for Resize {
);
context.for_other(&child).layout(available_space)
};
Rect::from(size.into_signed())
Size::<UPx>::new(
self.width.clamp(size.width, context.gfx.scale()),
self.height.clamp(size.height, context.gfx.scale()),
)
.into_signed()
.into()
}
}
@ -92,8 +98,11 @@ fn override_constraint(
) -> ConstraintLimit {
match constraint {
ConstraintLimit::Known(size) => ConstraintLimit::Known(range.clamp(size, scale)),
ConstraintLimit::ClippedAfter(clipped_after) => {
ConstraintLimit::ClippedAfter(range.clamp(clipped_after, scale))
}
ConstraintLimit::ClippedAfter(clipped_after) => match (range.minimum(), range.maximum()) {
(Some(min), Some(max)) if min == max => {
ConstraintLimit::Known(min.into_px(scale).into_unsigned())
}
_ => ConstraintLimit::ClippedAfter(range.clamp(clipped_after, scale)),
},
}
}

View file

@ -126,22 +126,17 @@ impl Widget for Scroll {
fn redraw(&mut self, context: &mut crate::context::GraphicsContext<'_, '_, '_, '_, '_>) {
context.redraw_when_changed(&self.scrollbar_opacity);
let Some(visible_rect) = context.gfx.visible_rect() else {
return;
};
let visible_bottom_right = visible_rect.into_signed().extent();
let managed = self.contents.mounted(&mut context.as_event_context());
context.for_other(&managed).redraw();
let size = context.gfx.region().size;
if self.horizontal_bar.amount_hidden > 0 {
context.gfx.draw_shape(
&Shape::filled_rect(
Rect::new(
Point::new(
self.horizontal_bar.offset,
self.control_size.height - self.bar_width,
),
Point::new(self.horizontal_bar.offset, size.height - self.bar_width),
Size::new(self.horizontal_bar.size, self.bar_width),
),
Color::new_f32(1.0, 1.0, 1.0, *self.scrollbar_opacity.get()),
@ -156,10 +151,7 @@ impl Widget for Scroll {
context.gfx.draw_shape(
&Shape::filled_rect(
Rect::new(
Point::new(
visible_bottom_right.x - self.bar_width,
self.vertical_bar.offset,
),
Point::new(size.width - self.bar_width, self.vertical_bar.offset),
Size::new(self.bar_width, self.vertical_bar.size),
),
Color::new_f32(1.0, 1.0, 1.0, *self.scrollbar_opacity.get()),
@ -193,12 +185,12 @@ impl Widget for Scroll {
if self.enabled.x {
ConstraintLimit::ClippedAfter(UPx::MAX - scroll.x.into_unsigned())
} else {
ConstraintLimit::Known(control_size.width.into_unsigned())
available_space.width
},
if self.enabled.y {
ConstraintLimit::ClippedAfter(UPx::MAX - scroll.y.into_unsigned())
} else {
ConstraintLimit::Known(control_size.height.into_unsigned())
available_space.height
},
);
let managed = self.contents.mounted(&mut context.as_event_context());
@ -255,7 +247,22 @@ impl Widget for Scroll {
);
context.set_child_layout(&managed, region);
Size::new(available_space.width.max(), available_space.height.max())
Size::new(
if self.enabled.x {
available_space
.width
.fit_measured(self.content_size.width, context.gfx.scale())
} else {
self.content_size.width.into_unsigned()
},
if self.enabled.y {
available_space
.height
.fit_measured(self.content_size.height, context.gfx.scale())
} else {
self.content_size.height.into_unsigned()
},
)
}
fn mouse_wheel(

420
src/widgets/slider.rs Normal file
View file

@ -0,0 +1,420 @@
use std::borrow::Cow;
use std::fmt::Debug;
use std::panic::UnwindSafe;
use kludgine::app::winit::event::{DeviceId, MouseButton};
use kludgine::figures::units::{Lp, Px, UPx};
use kludgine::figures::{
FloatConversion, FromComponents, IntoComponents, IntoSigned, IntoUnsigned, Point, Ranged, Rect,
ScreenScale, Size,
};
use kludgine::shapes::Shape;
use kludgine::{Color, Origin};
use crate::animation::{LinearInterpolate, PercentBetween};
use crate::context::{EventContext, GraphicsContext, LayoutContext, WidgetContext};
use crate::styles::{ComponentDefinition, ComponentName, Dimension, Group, NamedComponent};
use crate::value::{Dynamic, IntoDynamic, IntoValue, Value};
use crate::widget::{EventHandling, Widget, HANDLED};
use crate::ConstraintLimit;
/// A widget that allows sliding between two values.
#[derive(Debug, Clone)]
pub struct Slider<T> {
/// The current value.
pub value: Dynamic<T>,
/// The minimum value represented by this slider.
pub minimum: Value<T>,
/// The maximum value represented by this slider.
pub maximum: Value<T>,
knob_size: UPx,
horizontal: bool,
rendered_size: Px,
}
impl<T> Slider<T>
where
T: Ranged,
{
/// Returns a new slider over `value` using the types full range.
#[must_use]
pub fn from_value(value: impl IntoDynamic<T>) -> Self {
Self::new(value, T::MIN, T::MAX)
}
}
impl<T> Slider<T> {
/// Returns a new slider using `value` as the slider's value, keeping the
/// value between `min` and `max`.
#[must_use]
pub fn new(value: impl IntoDynamic<T>, min: impl IntoValue<T>, max: impl IntoValue<T>) -> Self {
Self {
value: value.into_dynamic(),
minimum: min.into_value(),
maximum: max.into_value(),
knob_size: UPx(0),
horizontal: true,
rendered_size: Px(0),
}
}
/// Sets the maximum value of this slider to `max` and returns self.
#[must_use]
pub fn maximum(mut self, max: impl IntoValue<T>) -> Self {
self.maximum = max.into_value();
self
}
/// Sets the minimum value of this slider to `min` and returns self.
#[must_use]
pub fn minimum(mut self, min: impl IntoValue<T>) -> Self {
self.minimum = min.into_value();
self
}
fn draw_track(&mut self, spec: &TrackSpec, context: &mut GraphicsContext<'_, '_, '_, '_, '_>) {
if self.horizontal {
self.rendered_size = spec.size.width;
} else {
self.rendered_size = spec.size.height;
}
let track_length = self.rendered_size - spec.knob_size;
let value_location = (track_length) * spec.percent + spec.half_knob;
let half_track = spec.track_size / 2;
// Draw the track
if value_location > spec.half_knob {
context.gfx.draw_shape(
&Shape::filled_rect(
Rect::new(
flipped(
!self.horizontal,
Point::new(spec.half_knob, spec.half_knob - half_track),
),
flipped(!self.horizontal, Size::new(value_location, spec.track_size)),
),
spec.track_color,
),
Point::default(),
None,
None,
);
}
if value_location < track_length {
context.gfx.draw_shape(
&Shape::filled_rect(
Rect::new(
flipped(
!self.horizontal,
Point::new(value_location, spec.half_knob - half_track),
),
flipped(
!self.horizontal,
Size::new(
track_length - value_location + spec.half_knob,
spec.track_size,
),
),
),
spec.inactive_track_color,
),
Point::default(),
None,
None,
);
}
// Draw the knob
context.gfx.draw_shape(
&Shape::filled_circle(spec.half_knob, spec.knob_color, Origin::Center),
flipped(!self.horizontal, Point::new(value_location, spec.half_knob)),
None,
None,
);
}
}
impl<T> Slider<T>
where
T: LinearInterpolate + Clone,
{
fn update_from_click(&mut self, position: Point<Px>) {
let position = if self.horizontal {
position.x
} else {
position.y
};
let position = position.clamp(Px(0), self.rendered_size);
let percent = position.into_float() / self.rendered_size.into_float();
let min = self.minimum.get();
let max = self.maximum.get();
self.value.update(min.lerp(&max, percent));
}
}
impl<T> Widget for Slider<T>
where
T: Clone
+ Debug
+ PartialOrd
+ LinearInterpolate
+ PercentBetween
+ UnwindSafe
+ Send
+ 'static,
{
fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_, '_>) {
let styles =
context.query_styles(&[&TrackColor, &InactiveTrackColor, &KnobColor, &TrackSize]);
let track_color = styles.get(&TrackColor, context);
let inactive_track_color = styles.get(&InactiveTrackColor, context);
let knob_color = styles.get(&KnobColor, context);
let knob_size = self.knob_size.into_signed();
let track_size = styles
.get(&TrackSize, context)
.into_px(context.gfx.scale())
.min(knob_size);
let half_knob = knob_size / 2;
let mut value = self.value.get_tracked(context);
let min = self.minimum.get_tracked(context);
let mut max = self.maximum.get_tracked(context);
if max < min {
self.maximum.map_mut(|max| *max = min.clone());
max = min.clone();
}
let mut value_clamped = false;
if value < min {
value_clamped = true;
value = min.clone();
} else if value > max {
value_clamped = true;
value = max.clone();
}
if value_clamped {
self.value.map_mut(|v| *v = value.clone());
}
let percent = value.percent_between(&min, &max);
let size = context.gfx.region().size;
self.horizontal = size.width >= size.height;
self.draw_track(
&TrackSpec {
size,
percent: *percent,
half_knob,
knob_size,
track_size,
knob_color,
track_color,
inactive_track_color,
},
context,
);
}
fn layout(
&mut self,
available_space: Size<ConstraintLimit>,
context: &mut LayoutContext<'_, '_, '_, '_, '_>,
) -> Size<UPx> {
let styles = context.query_styles(&[&KnobSize, &MinimumSliderSize]);
self.knob_size = styles
.get(&KnobSize, context)
.into_px(context.gfx.scale())
.into_unsigned();
let minimum_size = styles
.get(&MinimumSliderSize, context)
.into_px(context.gfx.scale())
.into_unsigned();
match (available_space.width, available_space.height) {
(ConstraintLimit::Known(width), ConstraintLimit::Known(height)) => {
// This comparison is done such that if width == height, we end
// up with a horizontal slider.
if width < height {
// Vertical slider
Size::new(self.knob_size, height.max(minimum_size))
} else {
// Horizontal slider
Size::new(width.max(minimum_size), self.knob_size)
}
}
(ConstraintLimit::Known(width), ConstraintLimit::ClippedAfter(_)) => {
Size::new(width.max(minimum_size), self.knob_size)
}
(ConstraintLimit::ClippedAfter(_), ConstraintLimit::Known(height)) => {
Size::new(self.knob_size, height.max(minimum_size))
}
(ConstraintLimit::ClippedAfter(width), ConstraintLimit::ClippedAfter(_)) => {
// When we have no limit on our, we still want to be draggable.
// Since we have no limit in both directions, we have to make a
// choice: horizontal or vertical. It seems to @ecton at the
// time of writing this that when there is no intent from the
// user of the slider, a horizontal slider is expected. So, we
// set the minimum measurement based on a horizontal
// orientation.
Size::new(width.min(minimum_size), self.knob_size)
}
}
}
fn hit_test(&mut self, _location: Point<Px>, _context: &mut EventContext<'_, '_>) -> bool {
true
}
fn mouse_down(
&mut self,
location: Point<Px>,
_device_id: DeviceId,
_button: MouseButton,
_context: &mut EventContext<'_, '_>,
) -> EventHandling {
self.update_from_click(location);
HANDLED
}
fn mouse_drag(
&mut self,
location: Point<Px>,
_device_id: DeviceId,
_button: MouseButton,
_context: &mut EventContext<'_, '_>,
) {
self.update_from_click(location);
}
}
struct TrackSpec {
size: Size<Px>,
percent: f32,
half_knob: Px,
knob_size: Px,
track_size: Px,
knob_color: Color,
track_color: Color,
inactive_track_color: Color,
}
fn flipped<T, Unit>(flip: bool, value: T) -> T
where
T: IntoComponents<Unit> + FromComponents<Unit>,
{
if flip {
let (a, b) = value.into_components();
T::from_components((b, a))
} else {
value
}
}
/// The size of the track that the knob of a [`Slider`] traversesq.
pub struct TrackSize;
impl ComponentDefinition for TrackSize {
type ComponentType = Dimension;
fn default_value(&self, _context: &WidgetContext<'_, '_>) -> Self::ComponentType {
Dimension::Lp(Lp::points(5))
}
}
impl NamedComponent for TrackSize {
fn name(&self) -> Cow<'_, ComponentName> {
Cow::Owned(ComponentName::new(Group::new("Slider"), "track_size"))
}
}
/// The width and height of the draggable portion of a [`Slider`].
pub struct KnobSize;
impl ComponentDefinition for KnobSize {
type ComponentType = Dimension;
fn default_value(&self, _context: &WidgetContext<'_, '_>) -> Self::ComponentType {
Dimension::Lp(Lp::points(14))
}
}
impl NamedComponent for KnobSize {
fn name(&self) -> Cow<'_, ComponentName> {
Cow::Owned(ComponentName::new(Group::new("Slider"), "knob_size"))
}
}
/// The minimum length of the slidable dimension.
pub struct MinimumSliderSize;
impl ComponentDefinition for MinimumSliderSize {
type ComponentType = Dimension;
fn default_value(&self, _context: &WidgetContext<'_, '_>) -> Self::ComponentType {
Dimension::Lp(Lp::points(14))
}
}
impl NamedComponent for MinimumSliderSize {
fn name(&self) -> Cow<'_, ComponentName> {
Cow::Owned(ComponentName::new(Group::new("Slider"), "minimum_size"))
}
}
/// The color of the draggable portion of the knob.
pub struct KnobColor;
impl ComponentDefinition for KnobColor {
type ComponentType = Color;
fn default_value(&self, context: &WidgetContext<'_, '_>) -> Self::ComponentType {
context.theme().primary.color
}
}
impl NamedComponent for KnobColor {
fn name(&self) -> Cow<'_, ComponentName> {
Cow::Owned(ComponentName::new(Group::new("Slider"), "knob_color"))
}
}
/// The color of the track that the knob rests on.
pub struct TrackColor;
impl ComponentDefinition for TrackColor {
type ComponentType = Color;
fn default_value(&self, context: &WidgetContext<'_, '_>) -> Self::ComponentType {
context.theme().primary.color
}
}
impl NamedComponent for TrackColor {
fn name(&self) -> Cow<'_, ComponentName> {
Cow::Owned(ComponentName::new(Group::new("Slider"), "track_color"))
}
}
/// The color of the draggable portion of the knob.
pub struct InactiveTrackColor;
impl ComponentDefinition for InactiveTrackColor {
type ComponentType = Color;
fn default_value(&self, context: &WidgetContext<'_, '_>) -> Self::ComponentType {
context.theme().surface.outline
}
}
impl NamedComponent for InactiveTrackColor {
fn name(&self) -> Cow<'_, ComponentName> {
Cow::Owned(ComponentName::new(
Group::new("Slider"),
"inactive_track_color",
))
}
}

View file

@ -89,7 +89,7 @@ impl Stack {
if let Some(expand) = guard.downcast_ref::<Expand>() {
let weight = expand.weight;
(
WidgetRef::Unmounted(widget.clone()),
expand.child().clone(),
StackDimension::Fractional { weight },
)
} else if let Some((child, size)) =
@ -403,18 +403,14 @@ impl Layout {
// Measure the children that fit their content
for &id in &self.measured {
let index = self.children.index_of_id(id).expect("child not found");
if remaining > 0 {
let (measured, _) = self.orientation.split_size(measure(
index,
self.orientation
.make_size(ConstraintLimit::ClippedAfter(remaining), other_constraint),
false,
));
self.layouts[index].size = measured;
remaining = remaining.saturating_sub(measured);
} else {
self.layouts[index].size = UPx(0);
}
let (measured, _) = self.orientation.split_size(measure(
index,
self.orientation
.make_size(ConstraintLimit::ClippedAfter(remaining), other_constraint),
false,
));
self.layouts[index].size = measured;
remaining = remaining.saturating_sub(measured);
}
// Measure the weighted children within the remaining space

View file

@ -1,20 +1,21 @@
use crate::context::EventContext;
use crate::styles::Styles;
use crate::value::{IntoValue, Value};
use crate::widget::{MakeWidget, WidgetRef, WrapperWidget};
/// A widget that applies a set of [`Styles`] to all contained widgets.
#[derive(Debug)]
pub struct Style {
styles: Styles,
styles: Value<Styles>,
child: WidgetRef,
}
impl Style {
/// Returns a new widget that applies `styles` to `child` and any children
/// it may have.
pub fn new(styles: impl Into<Styles>, child: impl MakeWidget) -> Self {
pub fn new(styles: impl IntoValue<Styles>, child: impl MakeWidget) -> Self {
Self {
styles: styles.into(),
styles: styles.into_value(),
child: WidgetRef::new(child),
}
}

31
src/widgets/themed.rs Normal file
View file

@ -0,0 +1,31 @@
use crate::context::EventContext;
use crate::styles::ThemePair;
use crate::value::{IntoValue, Value};
use crate::widget::{MakeWidget, WidgetRef, WrapperWidget};
/// A widget that applies a set of [`Styles`] to all contained widgets.
#[derive(Debug)]
pub struct Themed {
theme: Value<ThemePair>,
child: WidgetRef,
}
impl Themed {
/// Returns a new widget that applies `theme` to all of its children.
pub fn new(theme: impl IntoValue<ThemePair>, child: impl MakeWidget) -> Self {
Self {
theme: theme.into_value(),
child: WidgetRef::new(child),
}
}
}
impl WrapperWidget for Themed {
fn child_mut(&mut self) -> &mut WidgetRef {
&mut self.child
}
fn mounted(&mut self, context: &mut EventContext<'_, '_>) {
context.attach_theme(self.theme.clone());
}
}

View file

@ -15,13 +15,16 @@ use kludgine::app::winit::event::{
DeviceId, ElementState, Ime, KeyEvent, MouseButton, MouseScrollDelta, TouchPhase,
};
use kludgine::app::winit::keyboard::Key;
use kludgine::app::winit::window;
use kludgine::app::WindowBehavior as _;
use kludgine::figures::units::{Px, UPx};
use kludgine::figures::{IntoSigned, IntoUnsigned, Point, Rect, ScreenScale, Size};
use kludgine::figures::{IntoSigned, IntoUnsigned, Point, Ranged, Rect, ScreenScale, Size};
use kludgine::render::Drawing;
use kludgine::wgpu::CompositeAlphaMode;
use kludgine::Kludgine;
use tracing::Level;
use crate::animation::{LinearInterpolate, PercentBetween, ZeroToOne};
use crate::context::{
AsEventContext, EventContext, Exclusive, GraphicsContext, LayoutContext, RedrawStatus,
WidgetContext,
@ -31,7 +34,7 @@ use crate::styles::components::LayoutOrder;
use crate::styles::ThemePair;
use crate::tree::Tree;
use crate::utils::ModifiersExt;
use crate::value::{Dynamic, DynamicReader, IntoDynamic, Value};
use crate::value::{Dynamic, DynamicReader, IntoDynamic, IntoValue, Value};
use crate::widget::{
EventHandling, ManagedWidget, Widget, WidgetId, WidgetInstance, HANDLED, IGNORED,
};
@ -104,6 +107,7 @@ where
pub theme: Value<ThemePair>,
occluded: Option<Dynamic<bool>>,
focused: Option<Dynamic<bool>>,
theme_mode: Option<Value<ThemeMode>>,
}
impl<Behavior> Default for Window<Behavior>
@ -155,6 +159,24 @@ impl Window<WidgetInstance> {
self.occluded = Some(occluded);
self
}
/// Sets the [`ThemeMode`] for this window.
///
/// If a [`ThemeMode`] is provided, the window will be set to this theme
/// mode upon creation and will not be updated while the window is running.
///
/// If a [`Dynamic`] is provided, the initial value will be ignored and the
/// dynamic will be updated when the window opens with the user's current
/// theme mode. The dynamic will also be updated any time the user's theme
/// mode changes.
///
/// Setting the [`Dynamic`]'s value will also update the window with the new
/// mode until a mode change is detected, upon which the new mode will be
/// stored.
pub fn with_theme_mode(mut self, theme_mode: impl IntoValue<ThemeMode>) -> Self {
self.theme_mode = Some(theme_mode.into_value());
self
}
}
impl<Behavior> Window<Behavior>
@ -188,6 +210,7 @@ where
theme: Value::default(),
occluded: None,
focused: None,
theme_mode: None,
}
}
}
@ -201,10 +224,12 @@ where
GooeyWindow::<Behavior>::run_with(AssertUnwindSafe(sealed::Context {
user: self.context,
settings: RefCell::new(sealed::WindowSettings {
transparent: self.attributes.transparent,
attributes: Some(self.attributes),
occluded: self.occluded,
focused: self.focused,
theme: Some(self.theme),
theme_mode: self.theme_mode,
}),
}))
}
@ -258,6 +283,8 @@ struct GooeyWindow<T> {
max_inner_size: Option<Size<UPx>>,
theme: Option<DynamicReader<ThemePair>>,
current_theme: ThemePair,
theme_mode: Dynamic<ThemeMode>,
transparent: bool,
}
impl<T> GooeyWindow<T>
@ -286,6 +313,7 @@ where
&self.redraw_status,
&self.current_theme,
window,
self.theme_mode.get(),
),
kludgine,
)
@ -297,6 +325,7 @@ where
&self.redraw_status,
&self.current_theme,
window,
self.theme_mode.get(),
),
kludgine,
)
@ -310,6 +339,7 @@ where
&self.redraw_status,
&self.current_theme,
window,
self.theme_mode.get(),
),
kludgine,
)
@ -348,7 +378,7 @@ where
let new_min_size = (min_width > 0 || min_height > 0)
.then_some(Size::<Px>::new(min_width, min_height).into_unsigned());
if new_min_size != self.min_inner_size {
if new_min_size != self.min_inner_size && resizable {
window.set_min_inner_size(new_min_size);
self.min_inner_size = new_min_size;
}
@ -405,6 +435,15 @@ where
.theme
.take()
.expect("theme always present");
let theme_mode = match context.settings.borrow_mut().theme_mode.take() {
Some(Value::Dynamic(dynamic)) => {
dynamic.update(window.theme().into());
dynamic
}
Some(Value::Constant(_)) | None => Dynamic::new(window.theme().into()),
};
let transparent = context.settings.borrow().transparent;
let mut behavior = T::initialize(
&mut RunningWindow::new(window, &focused, &occluded),
context.user,
@ -435,6 +474,8 @@ where
max_inner_size: None,
current_theme,
theme,
theme_mode,
transparent,
}
}
@ -467,14 +508,19 @@ where
&self.redraw_status,
&self.current_theme,
&mut window,
self.theme_mode.get(),
),
gfx: Exclusive::Owned(Graphics::new(graphics)),
};
context.redraw_when_changed(&self.theme_mode);
let mut layout_context = LayoutContext::new(&mut context);
let window_size = layout_context.gfx.size();
let background_color = layout_context.theme().surface.color;
layout_context.graphics.gfx.fill(background_color);
if !self.transparent {
let background_color = layout_context.theme().surface.color;
layout_context.graphics.gfx.fill(background_color);
}
let actual_size = layout_context.layout(if is_expanded {
Size::new(
ConstraintLimit::Known(window_size.width),
@ -549,12 +595,16 @@ where
fn initial_window_attributes(
context: &Self::Context,
) -> kludgine::app::WindowAttributes<WindowCommand> {
context
let mut attrs = context
.settings
.borrow_mut()
.attributes
.take()
.expect("called more than once")
.expect("called more than once");
if let Some(Value::Constant(theme_mode)) = &context.settings.borrow().theme_mode {
attrs.preferred_theme = Some((*theme_mode).into());
}
attrs
}
fn close_requested(
@ -577,9 +627,21 @@ where
// wgpu::Limits::downlevel_webgl2_defaults().using_resolution(adapter_limits)
// }
// fn clear_color() -> Option<kludgine::Color> {
// Some(kludgine::Color::BLACK)
// }
fn clear_color(&self) -> Option<kludgine::Color> {
Some(if self.transparent {
kludgine::Color::CLEAR_BLACK
} else {
kludgine::Color::BLACK
})
}
fn composite_alpha_mode(&self, supported_modes: &[CompositeAlphaMode]) -> CompositeAlphaMode {
if self.transparent && supported_modes.contains(&CompositeAlphaMode::PreMultiplied) {
CompositeAlphaMode::PreMultiplied
} else {
CompositeAlphaMode::Auto
}
}
// fn focus_changed(&mut self, window: kludgine::app::Window<'_, ()>) {}
@ -616,6 +678,7 @@ where
&self.redraw_status,
&self.current_theme,
&mut window,
self.theme_mode.get(),
),
kludgine,
);
@ -645,6 +708,7 @@ where
&self.redraw_status,
&self.current_theme,
&mut window,
self.theme_mode.get(),
),
kludgine,
);
@ -711,6 +775,7 @@ where
&self.redraw_status,
&self.current_theme,
&mut window,
self.theme_mode.get(),
),
kludgine,
);
@ -745,6 +810,7 @@ where
&self.redraw_status,
&self.current_theme,
&mut window,
self.theme_mode.get(),
),
kludgine,
);
@ -773,6 +839,7 @@ where
&self.redraw_status,
&self.current_theme,
&mut window,
self.theme_mode.get(),
),
kludgine,
);
@ -787,6 +854,7 @@ where
&self.redraw_status,
&self.current_theme,
&mut window,
self.theme_mode.get(),
),
kludgine,
);
@ -827,6 +895,7 @@ where
&self.redraw_status,
&self.current_theme,
&mut window,
self.theme_mode.get(),
),
kludgine,
);
@ -851,6 +920,7 @@ where
&self.redraw_status,
&self.current_theme,
&mut window,
self.theme_mode.get(),
),
kludgine,
)
@ -866,6 +936,7 @@ where
&self.redraw_status,
&self.current_theme,
&mut window,
self.theme_mode.get(),
),
kludgine,
),
@ -900,6 +971,7 @@ where
&self.redraw_status,
&self.current_theme,
&mut window,
self.theme_mode.get(),
),
kludgine,
);
@ -917,6 +989,14 @@ where
}
}
fn theme_changed(
&mut self,
window: kludgine::app::Window<'_, WindowCommand>,
_kludgine: &mut Kludgine,
) {
self.theme_mode.update(window.theme().into());
}
fn event(
&mut self,
mut window: kludgine::app::Window<'_, WindowCommand>,
@ -955,7 +1035,7 @@ pub(crate) mod sealed {
use crate::styles::ThemePair;
use crate::value::{Dynamic, Value};
use crate::window::WindowAttributes;
use crate::window::{ThemeMode, WindowAttributes};
pub struct Context<C> {
pub user: C,
@ -967,6 +1047,8 @@ pub(crate) mod sealed {
pub occluded: Option<Dynamic<bool>>,
pub focused: Option<Dynamic<bool>>,
pub theme: Option<Value<ThemePair>>,
pub theme_mode: Option<Value<ThemeMode>>,
pub transparent: bool,
}
pub enum WindowCommand {
@ -974,3 +1056,56 @@ pub(crate) mod sealed {
// RequestClose,
}
}
/// Controls whether the light or dark theme is applied.
#[derive(Default, Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
pub enum ThemeMode {
/// Applies the light theme
Light,
/// Applies the dark theme
#[default]
Dark,
}
impl From<window::Theme> for ThemeMode {
fn from(value: window::Theme) -> Self {
match value {
window::Theme::Light => Self::Light,
window::Theme::Dark => Self::Dark,
}
}
}
impl From<ThemeMode> for window::Theme {
fn from(value: ThemeMode) -> Self {
match value {
ThemeMode::Light => Self::Light,
ThemeMode::Dark => Self::Dark,
}
}
}
impl LinearInterpolate for ThemeMode {
fn lerp(&self, target: &Self, percent: f32) -> Self {
if percent >= 0.5 {
*target
} else {
*self
}
}
}
impl PercentBetween for ThemeMode {
fn percent_between(&self, min: &Self, max: &Self) -> ZeroToOne {
if *min == *max || *self == *min {
ZeroToOne::ZERO
} else {
ZeroToOne::ONE
}
}
}
impl Ranged for ThemeMode {
const MAX: Self = Self::Dark;
const MIN: Self = Self::Light;
}