Select buttons

This commit is contained in:
Jonathan Johnson 2023-11-29 17:14:42 -08:00
parent 63a4549f29
commit 03e93adb15
No known key found for this signature in database
GPG key ID: A66D6A34D6620579
7 changed files with 227 additions and 11 deletions

34
examples/manual-tabs.rs Normal file
View file

@ -0,0 +1,34 @@
//! This example show show to use a stack of buttons and a switcher to achieve a
//! tab-like widget.
use std::collections::HashMap;
use gooey::value::{Dynamic, Switchable};
use gooey::widget::MakeWidget;
use gooey::Run;
#[derive(Debug, Eq, PartialEq, Clone, Hash)]
enum Tab {
First,
Second,
Missing,
}
fn main() -> gooey::Result {
let mut tab_contents = HashMap::new();
tab_contents.insert(Tab::First, "This is the first tab!".make_widget());
tab_contents.insert(Tab::Second, "This is the second tab!".make_widget());
let selected_tab = Dynamic::new(Tab::First);
let tabs = selected_tab
.new_select(Tab::First, "First")
.and(selected_tab.new_select(Tab::Second, "Second"))
.and(selected_tab.new_select(Tab::Missing, "Missing"))
.into_columns();
tabs.and(selected_tab.switch_between(tab_contents))
.into_rows()
.fit_vertically()
.run()
}

23
examples/select.rs Normal file
View file

@ -0,0 +1,23 @@
use gooey::value::Dynamic;
use gooey::widget::MakeWidget;
use gooey::Run;
#[derive(Default, Eq, PartialEq, Debug, Clone, Copy)]
pub enum Choice {
#[default]
A,
B,
C,
}
fn main() -> gooey::Result {
let option = Dynamic::default();
option
.new_select(Choice::A, "A")
.and(option.new_select(Choice::B, "B"))
.and(option.new_select(Choice::C, "C"))
.into_rows()
.centered()
.run()
}

View file

@ -1,7 +1,9 @@
//! Types for storing and interacting with values in Widgets.
use std::collections::HashMap;
use std::fmt::{Debug, Display};
use std::future::Future;
use std::hash::{BuildHasher, Hash};
use std::ops::{Deref, DerefMut, Not};
use std::panic::UnwindSafe;
use std::str::FromStr;
@ -12,13 +14,14 @@ use std::thread::ThreadId;
use std::time::Duration;
use ahash::AHashSet;
use kempt::{Map, Sort};
use crate::animation::{AnimationHandle, DynamicTransition, IntoAnimate, LinearInterpolate, Spawn};
use crate::context::sealed::WindowHandle;
use crate::context::{self, WidgetContext};
use crate::utils::{run_in_bg, IgnorePoison, UnwindsafeCondvar, WithClone};
use crate::widget::{MakeWidget, WidgetId, WidgetInstance};
use crate::widgets::{Radio, Space, Switcher};
use crate::widget::{Children, MakeWidget, WidgetId, WidgetInstance};
use crate::widgets::{Radio, Select, Space, Switcher};
/// An instance of a value that provides APIs to observe and react to its
/// contents.
@ -550,13 +553,28 @@ impl<T> Dynamic<T> {
where
Self: Clone,
// Technically this trait bound isn't necessary, but it prevents trying
// to call into_radio on unsupported types. The MakeWidget/Widget
// to call new_radio on unsupported types. The MakeWidget/Widget
// implementations require these bounds (and more).
T: Clone + Eq,
{
Radio::new(widget_value, self.clone(), label)
}
/// Returns a new [`Select`] that updates this dynamic to `widget_value`
/// when pressed. `label` is drawn next to the checkbox and is also
/// clickable to select the widget.
#[must_use]
pub fn new_select(&self, widget_value: T, label: impl MakeWidget) -> Select<T>
where
Self: Clone,
// Technically this trait bound isn't necessary, but it prevents trying
// to call new_select on unsupported types. The MakeWidget/Widget
// implementations require these bounds (and more).
T: Clone + Eq,
{
Select::new(widget_value, self.clone(), label)
}
/// Validates the contents of this dynamic using the `check` function,
/// returning a dynamic that contains the validation status.
#[must_use]
@ -1287,6 +1305,56 @@ pub trait Switchable<T>: IntoDynamic<T> + Sized {
{
Switcher::mapping(self, map)
}
/// Returns a new [`Switcher`] whose contents switches between the values
/// contained in `map` using the value in `self` as the key.
fn switch_between<Collection>(self, map: Collection) -> Switcher
where
Collection: GetWidget<T> + Send + 'static,
T: Send + 'static,
{
Switcher::mapping(self, move |key, _| {
map.get(key)
.map_or_else(|| Space::clear().make_widget(), Clone::clone)
})
}
}
/// A collection of widgets that can be queried by `Key`.
pub trait GetWidget<Key> {
/// Returns the widget associated with `key`, if found.
fn get<'a>(&'a self, key: &Key) -> Option<&'a WidgetInstance>;
}
impl<Key, State> GetWidget<Key> for HashMap<Key, WidgetInstance, State>
where
Key: Hash + Eq,
State: BuildHasher,
{
fn get<'a>(&'a self, key: &Key) -> Option<&'a WidgetInstance> {
HashMap::get(self, key)
}
}
impl<Key> GetWidget<Key> for Map<Key, WidgetInstance>
where
Key: Sort,
{
fn get<'a>(&'a self, key: &Key) -> Option<&'a WidgetInstance> {
Map::get(self, key)
}
}
impl GetWidget<usize> for Children {
fn get<'a>(&'a self, key: &usize) -> Option<&'a WidgetInstance> {
(**self).get(*key)
}
}
impl GetWidget<usize> for Vec<WidgetInstance> {
fn get<'a>(&'a self, key: &usize) -> Option<&'a WidgetInstance> {
(**self).get(*key)
}
}
impl<T, W> Switchable<T> for W where W: IntoDynamic<T> {}

View file

@ -16,6 +16,7 @@ pub mod progress;
mod radio;
mod resize;
pub mod scroll;
pub mod select;
pub mod slider;
mod space;
pub mod stack;
@ -41,6 +42,7 @@ pub use progress::ProgressBar;
pub use radio::Radio;
pub use resize::Resize;
pub use scroll::Scroll;
pub use select::Select;
pub use slider::Slider;
pub use space::Space;
pub use stack::Stack;

View file

@ -1,4 +1,4 @@
//! A tri-state, labelable checkbox widget.
//! A labeled widget with a circular indicator representing a value.
use std::fmt::Debug;
use std::panic::UnwindSafe;
@ -14,12 +14,11 @@ use crate::widget::{MakeWidget, Widget, WidgetInstance};
use crate::widgets::button::ButtonKind;
use crate::ConstraintLimit;
/// A labeled-widget that supports three states: Checked, Unchecked, and
/// Indeterminant
/// A labeled widget with a circular indicator representing a value.
pub struct Radio<T> {
/// The value this button represents.
pub value: T,
/// The state (value) of the checkbox.
/// The state (value) of the radio.
pub state: Dynamic<T>,
/// The button kind to use as the basis for this radio. Radios default to
/// [`ButtonKind::Transparent`].
@ -29,7 +28,8 @@ pub struct Radio<T> {
impl<T> Radio<T> {
/// Returns a new radio that sets `state` to `value` when pressed. `label`
/// is drawn next to the checkbox and is also clickable to select the radio.
/// is drawn next to the radio indicator and is also clickable to select the
/// radio.
pub fn new(value: T, state: impl IntoDynamic<T>, label: impl MakeWidget) -> Self {
Self {
value,

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

@ -0,0 +1,91 @@
//! A selectable, labeled widget representing a value.
use std::fmt::Debug;
use std::panic::{RefUnwindSafe, UnwindSafe};
use kludgine::Color;
use crate::styles::components::OutlineColor;
use crate::styles::{Component, DynamicComponent};
use crate::value::{Dynamic, IntoDynamic, IntoValue, MapEach, Value};
use crate::widget::{MakeWidget, WidgetInstance};
use crate::widgets::button::{ButtonBackground, ButtonHoverBackground, ButtonKind};
/// A selectable, labeled widget representing a value.
pub struct Select<T> {
/// The value this button represents.
pub value: T,
/// The state (value) of the select.
pub state: Dynamic<T>,
/// The button kind to use as the basis for this select. Selects default to
/// [`ButtonKind::Transparent`].
pub kind: Value<ButtonKind>,
label: WidgetInstance,
}
impl<T> Select<T> {
/// Returns a new select that sets `state` to `value` when pressed. `label`
/// is drawn inside of the button.
pub fn new(value: T, state: impl IntoDynamic<T>, label: impl MakeWidget) -> Self {
Self {
value,
state: state.into_dynamic(),
kind: Value::Constant(ButtonKind::Transparent),
label: label.make_widget(),
}
}
/// Updates the button kind to use as the basis for this select, and
/// returns self.
///
/// Selects default to [`ButtonKind::Transparent`].
#[must_use]
pub fn kind(mut self, kind: impl IntoValue<ButtonKind>) -> Self {
self.kind = kind.into_value();
self
}
}
impl<T> MakeWidget for Select<T>
where
T: Clone + Debug + Eq + RefUnwindSafe + UnwindSafe + Send + Sync + 'static,
{
fn make_widget(self) -> WidgetInstance {
let selected = self.state.map_each({
let value = self.value.clone();
move |state| state == &value
});
let selected_color = DynamicComponent::new({
let selected = selected.clone();
move |context| {
if selected.get_tracking_refresh(context) {
Some(Component::Color(context.get(&SelectedColor)))
} else {
None
}
}
});
let kind = (&selected, &self.kind.into_dynamic()).map_each(|(selected, default_kind)| {
if *selected {
ButtonKind::Solid
} else {
*default_kind
}
});
self.label
.into_button()
.on_click(move |()| {
self.state.set(self.value.clone());
})
.kind(kind)
.with_dynamic(&ButtonBackground, selected_color.clone())
.with_dynamic(&ButtonHoverBackground, selected_color)
.make_widget()
}
}
define_components! {
Select {
/// The color of the selected [`Select`] widget.
SelectedColor(Color, "color", @OutlineColor)
}
}

View file

@ -431,9 +431,7 @@ impl Layout {
!needs_final_layout,
));
self.layouts[index].size = measured;
if measured == 0 {
self.other = UPx::ZERO;
} else {
if measured > 0 {
if fit_index < self.fit_to_content.len() - 1
|| self.fit_to_content.len() != self.children.len()
{