mirror of
https://github.com/danbulant/cushy
synced 2026-07-06 03:30:38 +00:00
Select buttons
This commit is contained in:
parent
63a4549f29
commit
03e93adb15
7 changed files with 227 additions and 11 deletions
34
examples/manual-tabs.rs
Normal file
34
examples/manual-tabs.rs
Normal 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
23
examples/select.rs
Normal 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()
|
||||||
|
}
|
||||||
74
src/value.rs
74
src/value.rs
|
|
@ -1,7 +1,9 @@
|
||||||
//! Types for storing and interacting with values in Widgets.
|
//! Types for storing and interacting with values in Widgets.
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::fmt::{Debug, Display};
|
use std::fmt::{Debug, Display};
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
|
use std::hash::{BuildHasher, Hash};
|
||||||
use std::ops::{Deref, DerefMut, Not};
|
use std::ops::{Deref, DerefMut, Not};
|
||||||
use std::panic::UnwindSafe;
|
use std::panic::UnwindSafe;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
@ -12,13 +14,14 @@ use std::thread::ThreadId;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use ahash::AHashSet;
|
use ahash::AHashSet;
|
||||||
|
use kempt::{Map, Sort};
|
||||||
|
|
||||||
use crate::animation::{AnimationHandle, DynamicTransition, IntoAnimate, LinearInterpolate, Spawn};
|
use crate::animation::{AnimationHandle, DynamicTransition, IntoAnimate, LinearInterpolate, Spawn};
|
||||||
use crate::context::sealed::WindowHandle;
|
use crate::context::sealed::WindowHandle;
|
||||||
use crate::context::{self, WidgetContext};
|
use crate::context::{self, WidgetContext};
|
||||||
use crate::utils::{run_in_bg, IgnorePoison, UnwindsafeCondvar, WithClone};
|
use crate::utils::{run_in_bg, IgnorePoison, UnwindsafeCondvar, WithClone};
|
||||||
use crate::widget::{MakeWidget, WidgetId, WidgetInstance};
|
use crate::widget::{Children, MakeWidget, WidgetId, WidgetInstance};
|
||||||
use crate::widgets::{Radio, Space, Switcher};
|
use crate::widgets::{Radio, Select, Space, Switcher};
|
||||||
|
|
||||||
/// An instance of a value that provides APIs to observe and react to its
|
/// An instance of a value that provides APIs to observe and react to its
|
||||||
/// contents.
|
/// contents.
|
||||||
|
|
@ -550,13 +553,28 @@ impl<T> Dynamic<T> {
|
||||||
where
|
where
|
||||||
Self: Clone,
|
Self: Clone,
|
||||||
// Technically this trait bound isn't necessary, but it prevents trying
|
// 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).
|
// implementations require these bounds (and more).
|
||||||
T: Clone + Eq,
|
T: Clone + Eq,
|
||||||
{
|
{
|
||||||
Radio::new(widget_value, self.clone(), label)
|
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,
|
/// Validates the contents of this dynamic using the `check` function,
|
||||||
/// returning a dynamic that contains the validation status.
|
/// returning a dynamic that contains the validation status.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
|
|
@ -1287,6 +1305,56 @@ pub trait Switchable<T>: IntoDynamic<T> + Sized {
|
||||||
{
|
{
|
||||||
Switcher::mapping(self, map)
|
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> {}
|
impl<T, W> Switchable<T> for W where W: IntoDynamic<T> {}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ pub mod progress;
|
||||||
mod radio;
|
mod radio;
|
||||||
mod resize;
|
mod resize;
|
||||||
pub mod scroll;
|
pub mod scroll;
|
||||||
|
pub mod select;
|
||||||
pub mod slider;
|
pub mod slider;
|
||||||
mod space;
|
mod space;
|
||||||
pub mod stack;
|
pub mod stack;
|
||||||
|
|
@ -41,6 +42,7 @@ pub use progress::ProgressBar;
|
||||||
pub use radio::Radio;
|
pub use radio::Radio;
|
||||||
pub use resize::Resize;
|
pub use resize::Resize;
|
||||||
pub use scroll::Scroll;
|
pub use scroll::Scroll;
|
||||||
|
pub use select::Select;
|
||||||
pub use slider::Slider;
|
pub use slider::Slider;
|
||||||
pub use space::Space;
|
pub use space::Space;
|
||||||
pub use stack::Stack;
|
pub use stack::Stack;
|
||||||
|
|
|
||||||
|
|
@ -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::fmt::Debug;
|
||||||
use std::panic::UnwindSafe;
|
use std::panic::UnwindSafe;
|
||||||
|
|
||||||
|
|
@ -14,12 +14,11 @@ use crate::widget::{MakeWidget, Widget, WidgetInstance};
|
||||||
use crate::widgets::button::ButtonKind;
|
use crate::widgets::button::ButtonKind;
|
||||||
use crate::ConstraintLimit;
|
use crate::ConstraintLimit;
|
||||||
|
|
||||||
/// A labeled-widget that supports three states: Checked, Unchecked, and
|
/// A labeled widget with a circular indicator representing a value.
|
||||||
/// Indeterminant
|
|
||||||
pub struct Radio<T> {
|
pub struct Radio<T> {
|
||||||
/// The value this button represents.
|
/// The value this button represents.
|
||||||
pub value: T,
|
pub value: T,
|
||||||
/// The state (value) of the checkbox.
|
/// The state (value) of the radio.
|
||||||
pub state: Dynamic<T>,
|
pub state: Dynamic<T>,
|
||||||
/// The button kind to use as the basis for this radio. Radios default to
|
/// The button kind to use as the basis for this radio. Radios default to
|
||||||
/// [`ButtonKind::Transparent`].
|
/// [`ButtonKind::Transparent`].
|
||||||
|
|
@ -29,7 +28,8 @@ pub struct Radio<T> {
|
||||||
|
|
||||||
impl<T> Radio<T> {
|
impl<T> Radio<T> {
|
||||||
/// Returns a new radio that sets `state` to `value` when pressed. `label`
|
/// 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 {
|
pub fn new(value: T, state: impl IntoDynamic<T>, label: impl MakeWidget) -> Self {
|
||||||
Self {
|
Self {
|
||||||
value,
|
value,
|
||||||
|
|
|
||||||
91
src/widgets/select.rs
Normal file
91
src/widgets/select.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -431,9 +431,7 @@ impl Layout {
|
||||||
!needs_final_layout,
|
!needs_final_layout,
|
||||||
));
|
));
|
||||||
self.layouts[index].size = measured;
|
self.layouts[index].size = measured;
|
||||||
if measured == 0 {
|
if measured > 0 {
|
||||||
self.other = UPx::ZERO;
|
|
||||||
} else {
|
|
||||||
if fit_index < self.fit_to_content.len() - 1
|
if fit_index < self.fit_to_content.len() - 1
|
||||||
|| self.fit_to_content.len() != self.children.len()
|
|| self.fit_to_content.len() != self.children.len()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue