Switcher, h/v expand

This commit is contained in:
Jonathan Johnson 2023-11-13 09:14:38 -08:00
parent 07b93397c5
commit ee3813f44d
No known key found for this signature in database
GPG key ID: A66D6A34D6620579
12 changed files with 364 additions and 87 deletions

22
Cargo.lock generated
View file

@ -281,11 +281,10 @@ dependencies = [
[[package]]
name = "cc"
version = "1.0.83"
version = "1.0.84"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0"
checksum = "0f8e7c90afad890484a21653d08b6e209ae34770fb5ee298f9c699fcc1e5c856"
dependencies = [
"jobserver",
"libc",
]
@ -838,15 +837,6 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
[[package]]
name = "jobserver"
version = "0.1.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c37f63953c4c63420ed5fd3d6d398c719489b9f872b9fa683262f8edd363c7d"
dependencies = [
"libc",
]
[[package]]
name = "js-sys"
version = "0.3.65"
@ -1970,9 +1960,9 @@ dependencies = [
[[package]]
name = "tracing-log"
version = "0.1.4"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f751112709b4e791d8ce53e32c4ed2d353565a795ce84da2285393f41557bdf2"
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
dependencies = [
"log",
"once_cell",
@ -1981,9 +1971,9 @@ dependencies = [
[[package]]
name = "tracing-subscriber"
version = "0.3.17"
version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77"
checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b"
dependencies = [
"matchers",
"nu-ansi-term",

46
examples/switcher.rs Normal file
View file

@ -0,0 +1,46 @@
use gooey::value::Dynamic;
use gooey::widget::{MakeWidget, WidgetInstance};
use gooey::widgets::{Button, Label, Switcher};
use gooey::Run;
#[derive(Debug)]
enum ActiveContent {
Intro,
Success,
}
fn main() -> gooey::Result {
let active = Dynamic::new(ActiveContent::Intro);
Switcher::new(active.clone(), move |content| match content {
ActiveContent::Intro => intro(active.clone()),
ActiveContent::Success => success(active.clone()),
})
.contain()
.centered()
.expand()
.run()
}
fn intro(active: Dynamic<ActiveContent>) -> WidgetInstance {
const INTRO: &str = "This example demonstrates the Switcher<T> widget, which uses a mapping function to convert from a generic type to the widget it uses for its contents.";
Label::new(INTRO)
.and(
Button::new("Switch!")
.on_click(move |_| active.set(ActiveContent::Success))
.centered(),
)
.into_rows()
.make_widget()
}
fn success(active: Dynamic<ActiveContent>) -> WidgetInstance {
Label::new("The value changed to `ActiveContent::Success`!")
.and(
Button::new("Start Over")
.on_click(move |_| active.set(ActiveContent::Intro))
// .centered(),
)
.into_rows()
.make_widget()
}

View file

@ -5,10 +5,9 @@ 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::{Input, Label, Scroll, Slider, Stack, Themed};
use gooey::widgets::{Input, Label, ModeSwitch, Scroll, Slider, Stack, Themed};
use gooey::window::ThemeMode;
use gooey::Run;
use kludgine::figures::units::Lp;
use kludgine::Color;
const PRIMARY_HUE: f32 = 240.;
@ -59,16 +58,22 @@ fn main() -> gooey::Result {
.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.map_each(|theme| theme.primary_fixed),
default_theme.map_each(|theme| theme.secondary_fixed),
default_theme.map_each(|theme| theme.tertiary_fixed),
))
.and(theme(
default_theme.map_each(|theme| theme.dark),
ThemeMode::Dark,
))
.and(theme(
default_theme.map_each(|theme| theme.light),
ThemeMode::Light,
)),
),
)
.pad_by(Lp::points(16))
.pad()
.expand()
.into_window()
.with_theme_mode(theme_mode)
@ -132,6 +137,7 @@ fn fixed_themes(
.and(fixed_theme(secondary, "Secondary"))
.and(fixed_theme(tertiary, "Tertiary")),
)
.contain()
.expand()
}
@ -156,12 +162,18 @@ fn fixed_theme(theme: Dynamic<FixedTheme>, label: &str) -> impl MakeWidget {
color,
)),
)
.contain()
.expand()
}
fn theme(theme: Dynamic<Theme>, label: &str) -> impl MakeWidget {
Stack::rows(
Label::new(label)
fn theme(theme: Dynamic<Theme>, mode: ThemeMode) -> impl MakeWidget {
ModeSwitch::new(
mode,
Stack::rows(
Label::new(match mode {
ThemeMode::Light => "Light",
ThemeMode::Dark => "Dark",
})
.and(
Stack::columns(
color_theme(theme.map_each(|theme| theme.primary), "Primary")
@ -175,9 +187,12 @@ fn theme(theme: Dynamic<Theme>, label: &str) -> impl MakeWidget {
))
.and(color_theme(theme.map_each(|theme| theme.error), "Error")),
)
.contain()
.expand(),
)
.and(surface_theme(theme.map_each(|theme| theme.surface))),
)
.contain(),
)
.expand()
}
@ -199,6 +214,7 @@ fn surface_theme(theme: Dynamic<SurfaceTheme>) -> impl MakeWidget {
on_color.clone(),
)),
)
.contain()
.expand()
.and(
Stack::columns(
@ -228,6 +244,7 @@ fn surface_theme(theme: Dynamic<SurfaceTheme>) -> impl MakeWidget {
on_color.clone(),
)),
)
.contain()
.expand(),
)
.and(
@ -254,9 +271,11 @@ fn surface_theme(theme: Dynamic<SurfaceTheme>) -> impl MakeWidget {
on_color,
)),
)
.contain()
.expand(),
),
)
.contain()
.expand()
}
@ -289,6 +308,7 @@ fn color_theme(theme: Dynamic<ColorTheme>, label: &str) -> impl MakeWidget {
container,
)),
)
.contain()
.expand()
}

View file

@ -22,14 +22,14 @@
//! use gooey::value::Dynamic;
//!
//! let value = Dynamic::new(0);
//!
//! let mut reader = value.create_reader();
//! value
//! .transition_to(100)
//! .over(Duration::from_millis(100))
//! .with_easing(EaseInOutElastic)
//! .launch();
//! drop(value);
//!
//! let mut reader = value.into_reader();
//! while reader.block_until_updated() {
//! println!("{}", reader.get());
//! }
@ -726,6 +726,48 @@ impl LinearInterpolate for Color {
}
}
/// A wrapper that implements [`LinearInterpolate`] such that the value switches
/// after 50%.
///
/// This wrapper can be used to add [`LinearInterpolate`] to types that normally
/// don't support interpolation.
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
pub struct BinaryLerp<T>(T);
impl<T> LinearInterpolate for BinaryLerp<T>
where
T: Clone + PartialEq,
{
fn lerp(&self, target: &Self, percent: f32) -> Self {
if false.lerp(&true, percent) {
target.clone()
} else {
self.clone()
}
}
}
/// A wrapper that implements [`LinearInterpolate`] such that the target value
/// is immediately returned as long as percent is > 0.
///
/// This wrapper can be used to add [`LinearInterpolate`] to types that normally
/// don't support interpolation.
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
pub struct ImmediateLerp<T>(T);
impl<T> LinearInterpolate for ImmediateLerp<T>
where
T: Clone + PartialEq,
{
fn lerp(&self, target: &Self, percent: f32) -> Self {
if percent > 0. {
target.clone()
} else {
self.clone()
}
}
}
/// Calculates the ratio of one value against a minimum and maximum.
pub trait PercentBetween {
/// Return the percentage that `self` is between `min` and `max`.

View file

@ -30,7 +30,7 @@ use crate::styles::{Dimension, FocusableWidgets, VisualOrder};
/// /// This component defaults to picking a contrasting color between `TextColor` and `SurfaceColor`
/// ContrastingColor(Color, "contrasting_color", contrasting!(ThemedComponent, TextColor, SurfaceColor))
/// /// This component shows how to use a closure for nearly infinite flexibility in computing the default value.
/// ClosureDefaultComponent(Color, "closure_component", |context| context.query_style(&TextColor))
/// ClosureDefaultComponent(Color, "closure_component", |context| context.get(&TextColor))
/// }
/// }
/// ```

View file

@ -418,6 +418,7 @@ impl TreeData {
while let Some(node) = detached_nodes.pop() {
let mut node = self.nodes.remove(node).expect("detached node missing");
self.nodes_by_id.remove(&node.widget.id());
detached_nodes.append(&mut node.children);
}
}

View file

@ -43,6 +43,12 @@ pub trait Widget: Send + UnwindSafe + Debug + 'static {
context: &mut LayoutContext<'_, '_, '_, '_, '_>,
) -> Size<UPx>;
/// Return true if this widget should expand to fill the window when it is
/// the root widget.
fn expand_if_at_root(&self) -> Option<bool> {
Some(false)
}
/// The widget has been mounted into a parent widget.
#[allow(unused_variables)]
fn mounted(&mut self, context: &mut EventContext<'_, '_>) {}
@ -227,9 +233,8 @@ pub trait WrapperWidget: Debug + Send + UnwindSafe + 'static {
available_space: Size<ConstraintLimit>,
context: &mut LayoutContext<'_, '_, '_, '_, '_>,
) -> WrappedLayout {
let child = self.child_mut().mounted(&mut context.as_event_context());
let adjusted_space = self.adjust_child_constraint(available_space, context);
let child = self.child_mut().mounted(&mut context.as_event_context());
let size = context
.for_other(&child)
.layout(adjusted_space)
@ -420,9 +425,8 @@ where
available_space: Size<ConstraintLimit>,
context: &mut LayoutContext<'_, '_, '_, '_, '_>,
) -> Size<UPx> {
let child = self.child_mut().mounted(&mut context.as_event_context());
let layout = self.layout_child(available_space, context);
let child = self.child_mut().mounted(&mut context.as_event_context());
context.set_child_layout(&child, layout.child);
layout.size
}
@ -606,6 +610,18 @@ pub trait MakeWidget: Sized {
Expand::weighted(weight, self)
}
/// Expands `self` to grow to fill its parent horizontally.
#[must_use]
fn expand_horizontally(self) -> Expand {
Expand::horizontal(self)
}
/// Expands `self` to grow to fill its parent vertically.
#[must_use]
fn expand_vertically(self) -> Expand {
Expand::horizontal(self)
}
/// Aligns `self` to the center vertically and horizontally.
#[must_use]
fn centered(self) -> Align {

View file

@ -14,6 +14,7 @@ mod slider;
mod space;
pub mod stack;
mod style;
mod switcher;
mod themed;
mod tilemap;
@ -31,5 +32,6 @@ pub use slider::Slider;
pub use space::Space;
pub use stack::Stack;
pub use style::Style;
pub use switcher::Switcher;
pub use themed::Themed;
pub use tilemap::TileMap;

View file

@ -12,12 +12,17 @@ use crate::ConstraintLimit;
/// [`Expand`]ed widget.
#[derive(Debug)]
pub struct Expand {
/// The weight to use when splitting available space with multiple
/// [`Expand`] widgets.
pub weight: u8,
kind: ExpandKind,
child: WidgetRef,
}
#[derive(Debug)]
enum ExpandKind {
Weighted(u8),
Horizontal,
Vertical,
}
impl Default for Expand {
fn default() -> Self {
Self::empty()
@ -30,7 +35,25 @@ impl Expand {
pub fn new(child: impl MakeWidget) -> Self {
Self {
child: WidgetRef::new(child),
weight: 1,
kind: ExpandKind::Weighted(1),
}
}
/// Returns a widget that expands `child` to fill the parent widget horizontally.
#[must_use]
pub fn horizontal(child: impl MakeWidget) -> Self {
Self {
child: WidgetRef::new(child),
kind: ExpandKind::Horizontal,
}
}
/// Returns a widget that expands `child` to fill the parent widget vertically.
#[must_use]
pub fn vertical(child: impl MakeWidget) -> Self {
Self {
child: WidgetRef::new(child),
kind: ExpandKind::Vertical,
}
}
@ -39,7 +62,7 @@ impl Expand {
pub fn empty() -> Self {
Self {
child: WidgetRef::new(Space::clear()),
weight: 1,
kind: ExpandKind::Weighted(1),
}
}
@ -51,7 +74,7 @@ impl Expand {
pub fn weighted(weight: u8, child: impl MakeWidget) -> Self {
Self {
child: WidgetRef::new(child),
weight,
kind: ExpandKind::Weighted(weight),
}
}
@ -60,6 +83,14 @@ impl Expand {
pub const fn child(&self) -> &WidgetRef {
&self.child
}
#[must_use]
pub(crate) fn weight(&self) -> Option<u8> {
match self.kind {
ExpandKind::Weighted(weight) => Some(weight),
ExpandKind::Horizontal | ExpandKind::Vertical => None,
}
}
}
impl WrapperWidget for Expand {
@ -79,15 +110,29 @@ impl WrapperWidget for Expand {
let child = self.child.mounted(&mut context.as_event_context());
let size = context.for_other(&child).layout(available_space);
Size::<UPx>::new(
available_space
.width
.fit_measured(size.width, context.gfx.scale()),
available_space
.height
.fit_measured(size.height, context.gfx.scale()),
)
.into_signed()
.into()
let (width, height) = match &self.kind {
ExpandKind::Weighted(_) => (
available_space
.width
.fit_measured(size.width, context.gfx.scale()),
available_space
.height
.fit_measured(size.height, context.gfx.scale()),
),
ExpandKind::Horizontal => (
available_space
.width
.fit_measured(size.width, context.gfx.scale()),
size.height,
),
ExpandKind::Vertical => (
size.width,
available_space
.height
.fit_measured(size.height, context.gfx.scale()),
),
};
Size::<UPx>::new(width, height).into_signed().into()
}
}

View file

@ -85,37 +85,38 @@ impl Stack {
} else {
// This is a brand new child.
let guard = widget.lock();
let (mut widget, dimension) =
if let Some(expand) = guard.downcast_ref::<Expand>() {
let weight = expand.weight;
(
expand.child().clone(),
StackDimension::Fractional { weight },
)
} else if let Some((child, size)) =
guard.downcast_ref::<Resize>().and_then(|r| {
let range = match self.layout.orientation.orientation {
StackOrientation::Row => r.height,
StackOrientation::Column => r.width,
};
range.minimum().map(|min| {
(
r.child().clone(),
StackDimension::Measured {
min,
_max: range.end,
},
)
})
let (mut widget, dimension) = if let Some((weight, expand)) = guard
.downcast_ref::<Expand>()
.and_then(|expand| expand.weight().map(|weight| (weight, expand)))
{
(
expand.child().clone(),
StackDimension::Fractional { weight },
)
} else if let Some((child, size)) =
guard.downcast_ref::<Resize>().and_then(|r| {
let range = match self.layout.orientation.orientation {
StackOrientation::Row => r.height,
StackOrientation::Column => r.width,
};
range.minimum().map(|min| {
(
r.child().clone(),
StackDimension::Measured {
min,
_max: range.end,
},
)
})
{
(child, size)
} else {
(
WidgetRef::Unmounted(widget.clone()),
StackDimension::FitContent,
)
};
})
{
(child, size)
} else {
(
WidgetRef::Unmounted(widget.clone()),
StackDimension::FitContent,
)
};
drop(guard);
self.synced_children.insert(index, widget.mounted(context));
@ -449,7 +450,7 @@ impl Layout {
ConstraintLimit::Known(self.layouts[index].size.into_px(scale).into_unsigned()),
other_constraint,
),
true,
false,
));
self.other = self.other.max(measured);
}
@ -459,6 +460,18 @@ impl Layout {
ConstraintLimit::ClippedAfter(clip_limit) => self.other.min(clip_limit),
};
// Finally layout the widgets with the final constraints
for index in 0..self.children.len() {
self.orientation.split_size(measure(
index,
self.orientation.make_size(
ConstraintLimit::Known(self.layouts[index].size.into_px(scale).into_unsigned()),
ConstraintLimit::Known(self.other),
),
true,
));
}
self.orientation.make_size(offset, self.other)
}
}

91
src/widgets/switcher.rs Normal file
View file

@ -0,0 +1,91 @@
use std::fmt::Debug;
use std::panic::UnwindSafe;
use kludgine::figures::Size;
use crate::context::{AsEventContext, LayoutContext};
use crate::value::{Generation, IntoValue, Value};
use crate::widget::{MakeWidget, WidgetInstance, WidgetRef, WrapperWidget};
use crate::ConstraintLimit;
/// A widget that switches its contents based on a value of `T`.
pub struct Switcher<T> {
value: Value<T>,
value_generation: Option<Generation>,
factory: Box<dyn SwitchMap<T>>,
child: WidgetRef,
}
impl<T> Switcher<T> {
/// Returns a new widget that replaces its contents with the result of
/// `widget_factory` each time `value` changes.
#[must_use]
pub fn new<W, F>(value: impl IntoValue<T>, mut widget_factory: F) -> Self
where
F: for<'a> FnMut(&'a T) -> W + Send + UnwindSafe + 'static,
W: MakeWidget,
{
let value = value.into_value();
let value_generation = value.generation();
let child = WidgetRef::new(value.map(|value| widget_factory(value)));
Self {
value,
value_generation,
factory: Box::new(widget_factory),
child,
}
}
}
impl<T> Debug for Switcher<T>
where
T: Debug,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Switcher")
.field("value", &self.value)
.field("child", &self.child)
.finish_non_exhaustive()
}
}
impl<T> WrapperWidget for Switcher<T>
where
T: Debug + Send + UnwindSafe + 'static,
{
fn child_mut(&mut self) -> &mut WidgetRef {
&mut self.child
}
// TODO this should be moved to an invalidated() event once we have it.
fn adjust_child_constraint(
&mut self,
available_space: Size<ConstraintLimit>,
context: &mut LayoutContext<'_, '_, '_, '_, '_>,
) -> Size<ConstraintLimit> {
let current_generation = self.value.generation();
if self.value_generation != current_generation {
self.value_generation = current_generation;
let new_child = WidgetRef::new(self.value.map(|value| self.factory.invoke(value)));
let removed = std::mem::replace(&mut self.child, new_child);
if let WidgetRef::Mounted(removed) = removed {
context.remove_child(&removed);
}
}
available_space
}
}
trait SwitchMap<T>: UnwindSafe + Send {
fn invoke(&mut self, value: &T) -> WidgetInstance;
}
impl<W, T, F> SwitchMap<T> for F
where
F: for<'a> FnMut(&'a T) -> W + Send + UnwindSafe,
W: MakeWidget,
{
fn invoke(&mut self, value: &T) -> WidgetInstance {
self(value).make_widget()
}
}

View file

@ -36,7 +36,9 @@ use crate::styles::ThemePair;
use crate::tree::Tree;
use crate::utils::ModifiersExt;
use crate::value::{Dynamic, DynamicReader, IntoDynamic, IntoValue, Value};
use crate::widget::{EventHandling, ManagedWidget, Widget, WidgetInstance, HANDLED, IGNORED};
use crate::widget::{
EventHandling, ManagedWidget, Widget, WidgetId, WidgetInstance, HANDLED, IGNORED,
};
use crate::widgets::{Expand, Resize};
use crate::window::sealed::WindowCommand;
use crate::{initialize_tracing, ConstraintLimit, Run};
@ -841,6 +843,9 @@ where
if let Some(state) = self.mouse_state.devices.get(&device_id) {
// Mouse Drag
for (button, handler) in state {
let Some(handler) = self.root.tree.widget(*handler) else {
continue;
};
let mut context = EventContext::new(
WidgetContext::new(
handler.clone(),
@ -878,7 +883,7 @@ where
if widget_context.hit_test(relative) {
widget_context.hover(relative);
drop(widget_context);
self.mouse_state.widget = Some(widget);
self.mouse_state.widget = Some(widget.id());
break;
}
}
@ -934,9 +939,13 @@ where
)
.clear_focus();
if let (ElementState::Pressed, Some(location), Some(hovered)) =
(state, &self.mouse_state.location, &self.mouse_state.widget)
{
if let (ElementState::Pressed, Some(location), Some(hovered)) = (
state,
&self.mouse_state.location,
self.mouse_state
.widget
.and_then(|id| self.root.tree.widget(id)),
) {
if let Some(handler) = recursively_handle_event(
&mut EventContext::new(
WidgetContext::new(
@ -958,7 +967,7 @@ where
.devices
.entry(device_id)
.or_default()
.insert(button, handler);
.insert(button, handler.id());
}
}
}
@ -972,7 +981,9 @@ where
if device_buttons.is_empty() {
self.mouse_state.devices.remove(&device_id);
}
let Some(handler) = self.root.tree.widget(handler) else {
return;
};
let mut context = EventContext::new(
WidgetContext::new(
handler,
@ -1036,8 +1047,8 @@ fn recursively_handle_event(
#[derive(Default)]
struct MouseState {
location: Option<Point<Px>>,
widget: Option<ManagedWidget>,
devices: AHashMap<DeviceId, AHashMap<MouseButton, ManagedWidget>>,
widget: Option<WidgetId>,
devices: AHashMap<DeviceId, AHashMap<MouseButton, WidgetId>>,
}
pub(crate) mod sealed {