mirror of
https://github.com/danbulant/cushy
synced 2026-06-19 06:21:15 +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.
|
||||
|
||||
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> {}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
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,
|
||||
));
|
||||
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()
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in a new issue