diff --git a/CHANGELOG.md b/CHANGELOG.md index b44e7c1..ebdee56 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -213,6 +213,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 returns a `plotters::DrawingArea` that can be used to draw any plot that the `plotters` crate supports. - `Delimiter` is a new widget that is similar to html's `hr` tag. +- `List` is a new widget that creates lists similar to HTML's `ol` and `ul` + tags. [plotters]: https://github.com/plotters-rs/plotters @@ -233,6 +235,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `Window::new` no longer accepts a `Cushy` parameter. The window now adopts the `Cushy` from the application it is opened within. - `MakeWidget::into_window()` no longer takes any parameters. +- `Label` is now generic over a new trait: `DynamicDisplay`. This new trait + allows a way to query a `WidgetContext` to resolve the value to display. The + trait is automatically implemented for all types that implement `Display`, so + this change in practice shouldn't break much code. ### Changed diff --git a/Cargo.lock b/Cargo.lock index ca934ed..a0d2dde 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -576,6 +576,7 @@ dependencies = [ "interner", "kempt", "kludgine", + "nominals", "palette", "plotters", "png", @@ -1479,6 +1480,12 @@ dependencies = [ "memoffset", ] +[[package]] +name = "nominals" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c1ae56aa93d00075cffa92cf1a4f525bfb6664fe70c062cf793b642e352414f" + [[package]] name = "nu-ansi-term" version = "0.46.0" diff --git a/Cargo.toml b/Cargo.toml index 738a790..24876d5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,6 +44,7 @@ pollster = "0.3.0" png = "0.17.10" image = { version = "0.24.7", features = ["png"] } plotters = { version = "0.3.5", default-features = false, optional = true } +nominals = "0.2.1" # [patch.crates-io] diff --git a/examples/list.rs b/examples/list.rs new file mode 100644 index 0000000..b3a8834 --- /dev/null +++ b/examples/list.rs @@ -0,0 +1,28 @@ +use cushy::value::Dynamic; +use cushy::widget::{MakeWidget, WidgetList}; +use cushy::widgets::list::ListStyle; +use cushy::Run; + +fn main() -> cushy::Result { + let current_style: Dynamic = Dynamic::default(); + let options = ListStyle::provided() + .into_iter() + .map(|style| current_style.new_radio(style.clone(), format!("{style:?}"))) + .collect::(); + + let rows = (1..100).map(|i| i.to_string()).collect::(); + + options + .into_rows() + .vertical_scroll() + .and( + rows.into_list() + .style(current_style) + .vertical_scroll() + .expand(), + ) + .into_columns() + .expand() + .pad() + .run() +} diff --git a/src/graphics.rs b/src/graphics.rs index 994dec8..907736c 100644 --- a/src/graphics.rs +++ b/src/graphics.rs @@ -81,10 +81,10 @@ impl<'clip, 'gfx, 'pass> Graphics<'clip, 'gfx, 'pass> { /// Sets the font family to the first family in `list`. pub fn set_available_font_family(&mut self, list: &FontFamilyList) { if self.font_state.current_font_family.as_ref() != Some(list) { - self.font_state.current_font_family = Some(list.clone()); if let Some(family) = self.find_available_font_family(list) { self.set_font_family(family); } + self.font_state.current_font_family = Some(list.clone()); } } diff --git a/src/styles.rs b/src/styles.rs index 79e7be4..19a9b30 100644 --- a/src/styles.rs +++ b/src/styles.rs @@ -25,6 +25,7 @@ use crate::context::{Trackable, WidgetContext}; use crate::names::Name; use crate::utils::Lazy; use crate::value::{Dynamic, IntoValue, Source, Value}; +use crate::widgets::input::CowString; #[macro_use] pub mod components; @@ -410,6 +411,8 @@ pub enum Component { FontWeight(Weight), /// The style of a font. FontStyle(Style), + /// A string value. + String(CowString), /// A custom component type. Custom(CustomComponent), @@ -444,6 +447,38 @@ impl Component { } } +macro_rules! impl_component_from_string { + ($type:ty) => { + impl From<$type> for Component { + fn from(s: $type) -> Self { + Self::String(s.into()) + } + } + }; +} + +impl_component_from_string!(String); +impl_component_from_string!(CowString); +impl_component_from_string!(&'_ str); + +macro_rules! impl_component_try_from_string { + ($type:ty) => { + impl TryFrom for $type { + type Error = Component; + + fn try_from(s: Component) -> Result { + match s { + Component::String(s) => Ok(s.into()), + other => Err(other), + } + } + } + }; +} + +impl_component_try_from_string!(String); +impl_component_try_from_string!(CowString); + impl From for Component { fn from(value: FamilyOwned) -> Self { Self::FontFamily(value) @@ -2548,6 +2583,18 @@ impl From> for FontFamilyList { } } +impl IntoValue for FamilyOwned { + fn into_value(self) -> Value { + FontFamilyList::from(self).into_value() + } +} + +impl IntoValue for Vec { + fn into_value(self) -> Value { + FontFamilyList::from(self).into_value() + } +} + impl From for Component { fn from(list: FontFamilyList) -> Self { Component::custom(list) diff --git a/src/styles/components.rs b/src/styles/components.rs index 8cd92f2..ff8e105 100644 --- a/src/styles/components.rs +++ b/src/styles/components.rs @@ -39,7 +39,7 @@ use crate::styles::{Dimension, FocusableWidgets, FontFamilyList, VisualOrder}; /// ``` #[macro_export] macro_rules! define_components { - ($($widget:ident { $($(#$doc:tt)* $component:ident($type:ty, $name:expr, $($default:tt)*))* })*) => {$($( + ($($widget:ident { $($(#$doc:tt)* $component:ident($type:ty, $name:expr $(, $($default:tt)*)?))* })*) => {$($( $(#$doc)* #[derive(Clone, Copy, Eq, PartialEq, Debug)] pub struct $component; @@ -60,7 +60,7 @@ macro_rules! define_components { impl ComponentDefinition for $component { type ComponentType = $type; - define_components!($type, $($default)*); + define_components!($type, $($($default)*)?); } }; @@ -84,7 +84,10 @@ macro_rules! define_components { ]) }); }; - ($type:ty, $($expr:tt)*) => { + ($type:ty, ) => { + define_components!($type, |_context| <$type>::default()); + }; + ($type:ty, $($expr:tt)+) => { define_components!($type, |_context| $($expr)*); }; } diff --git a/src/value.rs b/src/value.rs index de123fd..c037ae1 100644 --- a/src/value.rs +++ b/src/value.rs @@ -1122,7 +1122,7 @@ impl Dynamic { // Technically this trait bound isn't necessary, but it prevents trying // to call new_radio on unsupported types. The MakeWidget/Widget // implementations require these bounds (and more). - T: Clone + Eq, + T: Clone + PartialEq, { Radio::new(widget_value, self.clone(), label) } @@ -1137,7 +1137,7 @@ impl Dynamic { // 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, + T: Clone + PartialEq, { Select::new(widget_value, self.clone(), label) } diff --git a/src/widgets/input.rs b/src/widgets/input.rs index 14df6ff..7de7185 100644 --- a/src/widgets/input.rs +++ b/src/widgets/input.rs @@ -1385,6 +1385,15 @@ where } } +impl From for String { + fn from(s: CowString) -> Self { + match Arc::try_unwrap(s.0) { + Ok(s) => s, + Err(arc) => (*arc).clone(), + } + } +} + /// A cheap-to-clone, copy-on-write [`String`] type that masks its contents in /// [`Debug`] and [`InputStorage`] implementations. /// diff --git a/src/widgets/label.rs b/src/widgets/label.rs index 5162347..e993e6f 100644 --- a/src/widgets/label.rs +++ b/src/widgets/label.rs @@ -1,6 +1,6 @@ //! A read-only text widget. -use std::fmt::Write; +use std::fmt::{Display, Write}; use figures::units::{Px, UPx}; use figures::{Point, Round, Size}; @@ -8,7 +8,7 @@ use kludgine::text::{MeasuredText, Text, TextOrigin}; use kludgine::{CanRenderTo, Color, DrawableExt}; use super::input::CowString; -use crate::context::{GraphicsContext, LayoutContext, Trackable}; +use crate::context::{GraphicsContext, LayoutContext, Trackable, WidgetContext}; use crate::styles::components::TextColor; use crate::value::{Dynamic, Generation, IntoReadOnly, ReadOnly, Value}; use crate::widget::{Widget, WidgetInstance}; @@ -26,7 +26,7 @@ pub struct Label { impl Label where - T: std::fmt::Debug + std::fmt::Display + Send + 'static, + T: std::fmt::Debug + DynamicDisplay + Send + 'static, { /// Returns a new label that displays `text`. pub fn new(text: impl IntoReadOnly) -> Self { @@ -54,7 +54,7 @@ where context.apply_current_font_settings(); let measured = self.display.map(|text| { self.displayed.clear(); - if let Err(err) = write!(&mut self.displayed, "{text}") { + if let Err(err) = write!(&mut self.displayed, "{}", text.as_display(context)) { tracing::error!("Error invoking Display: {err}"); } context @@ -75,7 +75,7 @@ where impl Widget for Label where - T: std::fmt::Debug + std::fmt::Display + Send + 'static, + T: std::fmt::Debug + DynamicDisplay + Send + 'static, { fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_>) { self.display.invalidate_when_changed(context); @@ -132,3 +132,46 @@ impl_make_widget!( Value => String, ReadOnly => String ); + +/// A context-aware [`Display`] implementation. +/// +/// This trait is automatically implemented for all types that implement +/// [`Display`]. +pub trait DynamicDisplay { + /// Format `self` with any needed information from `context`. + fn fmt(&self, context: &WidgetContext<'_>, f: &mut std::fmt::Formatter<'_>) + -> std::fmt::Result; + + /// Returns a type that implements [`Display`]. + fn as_display<'display, 'ctx>( + &'display self, + context: &'display WidgetContext<'ctx>, + ) -> DynamicDisplayer<'display, 'ctx> + where + Self: Sized, + { + DynamicDisplayer(self, context) + } +} + +impl DynamicDisplay for T +where + T: Display, +{ + fn fmt( + &self, + _context: &WidgetContext<'_>, + f: &mut std::fmt::Formatter<'_>, + ) -> std::fmt::Result { + self.fmt(f) + } +} + +/// A generic [`Display`] implementation for a [`DynamicDisplay`] implementor. +pub struct DynamicDisplayer<'a, 'w>(&'a dyn DynamicDisplay, &'a WidgetContext<'w>); + +impl Display for DynamicDisplayer<'_, '_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(self.1, f) + } +} diff --git a/src/widgets/list.rs b/src/widgets/list.rs index a4c530c..3c9738d 100644 --- a/src/widgets/list.rs +++ b/src/widgets/list.rs @@ -3,9 +3,21 @@ use std::fmt::Debug; use std::sync::Arc; +use nominals::{ + ArmenianLower, ArmenianUpper, Bengali, Cambodian, CjkDecimal, CjkEarthlyBranch, + CjkHeavenlyStem, Decimal, Devanagari, DigitCollection, EasternArabic, Ethiopic, Georgian, + GreekLower, GreekUpper, Gujarati, Gurmukhi, HangeulFormal, HangeulInformal, HangeulJamo, + HangeulSyllable, HanjaFormal, Hebrew, HexLower, HexUpper, Hiragana, HiraganaIroha, + JapaneseFormal, JapaneseInformal, Kannada, Katakana, KatakanaIroha, Lao, LetterLower, + LetterUpper, Malayalam, Mongolian, Myanmar, NominalSystem, Oriya, Persian, RomanLower, + RomanUpper, Tamil, Telugu, Thai, Tibetan, +}; + use super::grid::GridWidgets; use super::input::CowString; -use super::Grid; +use super::label::DynamicDisplay; +use super::{Grid, Label}; +use crate::styles::{Component, RequireInvalidation}; use crate::value::{IntoValue, MapEach, Source, Value}; use crate::widget::{MakeWidget, WidgetInstance, WidgetList}; @@ -16,25 +28,198 @@ pub struct List { } impl List { - /// Returns a new list with the default [`ListStyle`]/ + /// Returns a new list with the default [`ListStyle`]. + #[must_use] pub fn new(children: impl IntoValue) -> Self { Self { children: children.into_value(), style: Value::Constant(ListStyle::default()), } } + + /// Sets the style of list identifiers to `style`. + #[must_use] + pub fn style(mut self, style: impl IntoValue) -> Self { + self.style = style.into_value(); + self + } } /// The style of a [`List`] widget's item indicators. #[derive(Default, Debug, Clone)] pub enum ListStyle { + /// This list should have no indicators. + None, + /// A solid circle indicator, using the unicode bullet indicator. #[default] Disc, + /// A hollow circle. + Circle, + /// A filled square. + Square, + + /// Decimal digits (0-9). + Decimal, + /// Eastern Arabic digits. + EasternArabic, + /// Persian digits. + Persian, + + /// Lowercase Armenian numbering. + ArmenianLower, + /// Uppercase Armenian numbering. + ArmenianUpper, + /// Bengali numeric digits. + Bengali, + /// Cambodian numeric digits. + Cambodian, + /// CJK Han decimal digits. + CjkDecimal, + /// CJK Earthly Branch symbols. + /// + /// This digit collection back to [`CjkDecimal`] after the set is enumerated. + CjkEarthlyBranch, + /// CJK Heavenly Stems symbols. + /// + /// This digit collection falls back to [`CjkDecimal`] after the set is + /// enumerated. + CjkHeavenlyStem, + /// Devanagari numeric digits. + Devanagari, + /// Ethiopic numerical system. + Ethiopic, + /// Traditional Georgian numbering. + Georgian, + /// Gujarati numeric digits. + Gujarati, + /// Gurmukhi numeric digits. + Gurmukhi, + /// Korean Hangeul numbering. + HangeulFormal, + /// Informal Korean Hangeul numbering. + HangeulInformal, + /// Formal Korean Hanja numbering. + HanjaFormal, + /// Formal Japanese Kanji numbering. + JapaneseFormal, + /// Informal Japanese Kanji numbering. + JapaneseInformal, + /// Kannada numeric digits. + Kannada, + /// Lao numeric digits. + Lao, + /// Malayalam numeric digits. + Malayalam, + /// Mongolian numeric digits. + Mongolian, + /// Myanmar numeric digits. + Myanmar, + /// Oriya numeric digits. + Oriya, + /// Tamil numeric digits. + Tamil, + /// Telugu numeric digits. + Telugu, + /// Thai numeric digits. + Thai, + /// Tibetan numeric digits. + Tibetan, + + /// ASCII lowercase alphabet (a-z). + LetterLower, + /// ASCII uppercase alphabet (A-Z). + LetterUpper, + /// Hexadecimal lowercase digits (0-9a-f) + HexLower, + /// Hexadecimal uppercase digits (0-9A-F) + HexUpper, + /// Greek lowercase alphabet. + GreekUpper, + /// Greek uppercase alphabet. + GreekLower, + /// Japanese Hiragana Aiueo alphabet. + Hiragana, + /// Japanese Hiragana Iroha alphabet. + HiraganaIroha, + /// Japanese Katakana Aiueo alphabet. + Katakana, + /// Japanese Katakana Iroha alphabet. + KatakanaIroha, + /// Korean Hangeul Jamo alphabet. + HangeulJamo, + /// Korean Hangeul Syllable alphabet. + HangeulSyllable, + + /// Lowercase Roman numerals (i, ii, iii, iv, ...). + RomanLower, + /// Uppercase Roman numerals (I, II, III, IV, ...). + RomanUpper, + /// Hebrew numerals. + Hebrew, + /// A custom list indicator style. Custom(Arc), } +impl ListStyle { + /// Returns an iterator containing all built-in list styles. + #[must_use] + pub const fn provided() -> impl IntoIterator { + [ + ListStyle::None, + ListStyle::Disc, + ListStyle::Circle, + ListStyle::Square, + ListStyle::Decimal, + ListStyle::ArmenianLower, + ListStyle::ArmenianUpper, + ListStyle::Bengali, + ListStyle::Cambodian, + ListStyle::CjkDecimal, + ListStyle::CjkEarthlyBranch, + ListStyle::CjkHeavenlyStem, + ListStyle::Devanagari, + ListStyle::EasternArabic, + ListStyle::Ethiopic, + ListStyle::Georgian, + ListStyle::GreekLower, + ListStyle::GreekUpper, + ListStyle::Gujarati, + ListStyle::Gurmukhi, + ListStyle::HangeulFormal, + ListStyle::HangeulInformal, + ListStyle::HangeulJamo, + ListStyle::HangeulSyllable, + ListStyle::HanjaFormal, + ListStyle::Hebrew, + ListStyle::HexLower, + ListStyle::HexUpper, + ListStyle::Hiragana, + ListStyle::HiraganaIroha, + ListStyle::JapaneseFormal, + ListStyle::JapaneseInformal, + ListStyle::Kannada, + ListStyle::Katakana, + ListStyle::KatakanaIroha, + ListStyle::Lao, + ListStyle::LetterLower, + ListStyle::LetterUpper, + ListStyle::Malayalam, + ListStyle::Mongolian, + ListStyle::Myanmar, + ListStyle::Oriya, + ListStyle::Persian, + ListStyle::RomanLower, + ListStyle::RomanUpper, + ListStyle::Tamil, + ListStyle::Telugu, + ListStyle::Thai, + ListStyle::Tibetan, + ] + } +} + impl PartialEq for ListStyle { fn eq(&self, other: &Self) -> bool { match (self, other) { @@ -48,14 +233,153 @@ impl PartialEq for ListStyle { /// given list index. pub trait ListIndicator: Debug + Sync + Send + 'static { /// Returns the indicator to use at `index`. - fn list_indicator(&self, index: usize) -> Option; + fn list_indicator(&self, index: usize) -> Option; } impl ListIndicator for ListStyle { - fn list_indicator(&self, index: usize) -> Option { + #[allow(clippy::too_many_lines)] // can't avoid the match + fn list_indicator(&self, index: usize) -> Option { match self { - ListStyle::Disc => Some(CowString::new("\u{2022}")), + ListStyle::None => None, + ListStyle::Decimal => Some(Indicator::delimited(String::from( + Decimal.one_based().format_nominal(index), + ))), + ListStyle::Disc => Some(Indicator::bare(CowString::new("\u{2022}"))), + ListStyle::Circle => Some(Indicator::bare(CowString::new("\u{25E6}"))), + ListStyle::Square => Some(Indicator::bare(CowString::new("\u{25AA}"))), ListStyle::Custom(style) => style.list_indicator(index), + ListStyle::EasternArabic => Some(Indicator::delimited(String::from( + EasternArabic.one_based().format_nominal(index), + ))), + ListStyle::Persian => Some(Indicator::delimited(String::from( + Persian.one_based().format_nominal(index), + ))), + ListStyle::LetterLower => Some(Indicator::delimited(String::from( + LetterLower.one_based().format_nominal(index), + ))), + ListStyle::LetterUpper => Some(Indicator::delimited(String::from( + LetterUpper.one_based().format_nominal(index), + ))), + ListStyle::HexLower => Some(Indicator::delimited(String::from( + HexLower.one_based().format_nominal(index), + ))), + ListStyle::HexUpper => Some(Indicator::delimited(String::from( + HexUpper.one_based().format_nominal(index), + ))), + ListStyle::GreekUpper => Some(Indicator::delimited(String::from( + GreekUpper.one_based().format_nominal(index), + ))), + ListStyle::GreekLower => Some(Indicator::delimited(String::from( + GreekLower.one_based().format_nominal(index), + ))), + ListStyle::Hiragana => Some(Indicator::delimited(String::from( + Hiragana.one_based().format_nominal(index), + ))), + ListStyle::HiraganaIroha => Some(Indicator::delimited(String::from( + HiraganaIroha.one_based().format_nominal(index), + ))), + ListStyle::Katakana => Some(Indicator::delimited(String::from( + Katakana.one_based().format_nominal(index), + ))), + ListStyle::KatakanaIroha => Some(Indicator::delimited(String::from( + KatakanaIroha.one_based().format_nominal(index), + ))), + ListStyle::HangeulJamo => Some(Indicator::delimited(String::from( + HangeulJamo.one_based().format_nominal(index), + ))), + ListStyle::HangeulSyllable => Some(Indicator::delimited(String::from( + HangeulSyllable.one_based().format_nominal(index), + ))), + ListStyle::RomanLower => Some(Indicator::delimited(String::from( + RomanLower.format_nominal(index), + ))), + ListStyle::RomanUpper => Some(Indicator::delimited(String::from( + RomanUpper.format_nominal(index), + ))), + ListStyle::Hebrew => Some(Indicator::delimited(String::from( + Hebrew.format_nominal(index), + ))), + ListStyle::ArmenianLower => Some(Indicator::delimited(String::from( + ArmenianLower.format_nominal(index), + ))), + ListStyle::ArmenianUpper => Some(Indicator::delimited(String::from( + ArmenianUpper.format_nominal(index), + ))), + ListStyle::Bengali => Some(Indicator::delimited(String::from( + Bengali.one_based().format_nominal(index), + ))), + ListStyle::Cambodian => Some(Indicator::delimited(String::from( + Cambodian.one_based().format_nominal(index), + ))), + ListStyle::CjkDecimal => Some(Indicator::delimited(String::from( + CjkDecimal.one_based().format_nominal(index), + ))), + ListStyle::CjkEarthlyBranch => Some(Indicator::delimited(String::from( + CjkEarthlyBranch.one_based().format_nominal(index), + ))), + ListStyle::CjkHeavenlyStem => Some(Indicator::delimited(String::from( + CjkHeavenlyStem.one_based().format_nominal(index), + ))), + ListStyle::Devanagari => Some(Indicator::delimited(String::from( + Devanagari.one_based().format_nominal(index), + ))), + ListStyle::Ethiopic => Some(Indicator::delimited(String::from( + Ethiopic.format_nominal(index), + ))), + ListStyle::Georgian => Some(Indicator::delimited(String::from( + Georgian.format_nominal(index), + ))), + ListStyle::Gujarati => Some(Indicator::delimited(String::from( + Gujarati.one_based().format_nominal(index), + ))), + ListStyle::Gurmukhi => Some(Indicator::delimited(String::from( + Gurmukhi.one_based().format_nominal(index), + ))), + ListStyle::HangeulFormal => Some(Indicator::delimited(String::from( + HangeulFormal.format_nominal(index), + ))), + ListStyle::HangeulInformal => Some(Indicator::delimited(String::from( + HangeulInformal.format_nominal(index), + ))), + ListStyle::HanjaFormal => Some(Indicator::delimited(String::from( + HanjaFormal.format_nominal(index), + ))), + ListStyle::JapaneseFormal => Some(Indicator::delimited(String::from( + JapaneseFormal.format_nominal(index), + ))), + ListStyle::JapaneseInformal => Some(Indicator::delimited(String::from( + JapaneseInformal.format_nominal(index), + ))), + ListStyle::Kannada => Some(Indicator::delimited(String::from( + Kannada.one_based().format_nominal(index), + ))), + ListStyle::Lao => Some(Indicator::delimited(String::from( + Lao.one_based().format_nominal(index), + ))), + ListStyle::Malayalam => Some(Indicator::delimited(String::from( + Malayalam.one_based().format_nominal(index), + ))), + ListStyle::Mongolian => Some(Indicator::delimited(String::from( + Mongolian.one_based().format_nominal(index), + ))), + ListStyle::Myanmar => Some(Indicator::delimited(String::from( + Myanmar.one_based().format_nominal(index), + ))), + ListStyle::Oriya => Some(Indicator::delimited(String::from( + Oriya.one_based().format_nominal(index), + ))), + ListStyle::Tamil => Some(Indicator::delimited(String::from( + Tamil.one_based().format_nominal(index), + ))), + ListStyle::Telugu => Some(Indicator::delimited(String::from( + Telugu.one_based().format_nominal(index), + ))), + ListStyle::Thai => Some(Indicator::delimited(String::from( + Thai.one_based().format_nominal(index), + ))), + ListStyle::Tibetan => Some(Indicator::delimited(String::from( + Tibetan.one_based().format_nominal(index), + ))), } } } @@ -86,9 +410,99 @@ fn build_grid_widgets(style: &ListStyle, children: &WidgetList) -> GridWidgets<2 .enumerate() .map(|(index, child)| { ( - style.list_indicator(index).unwrap_or_default(), + Label::new( + style + .list_indicator(index.wrapping_add(1)) + .unwrap_or_default(), + ) + .align_right(), child.clone().align_left().make_widget(), ) }) .collect() } + +/// An indicator used in a [`List`] widget. +#[derive(Default, Debug, Clone, Eq, PartialEq)] +pub struct Indicator { + display: CowString, + delimited: bool, +} + +impl Indicator { + /// Returns an indicator that should show a [`Delimiter`] between itself and + /// the list item. + pub fn delimited(display: impl Into) -> Self { + Self { + display: display.into(), + delimited: true, + } + } + + /// Returns an indicator that skips rendering a [`Delimiter`]. + pub fn bare(display: impl Into) -> Self { + Self { + display: display.into(), + delimited: false, + } + } +} + +impl DynamicDisplay for Indicator { + fn fmt( + &self, + context: &crate::context::WidgetContext<'_>, + f: &mut std::fmt::Formatter<'_>, + ) -> std::fmt::Result { + let prefix = context.get(&Prefix); + if self.delimited { + let delimiter = context.get(&TrailingDelimiter); + write!(f, "{}{}{}", prefix.0, self.display, delimiter.0) + } else { + write!(f, "{}{}", prefix.0, self.display) + } + } +} + +/// A [`CowString`] type used in [`List`] widget style components. +#[derive(Default)] +pub struct ListDelimiter(CowString); + +impl From<&'_ str> for ListDelimiter { + fn from(value: &'_ str) -> Self { + Self(value.into()) + } +} + +impl From for Component { + fn from(value: ListDelimiter) -> Self { + Component::String(value.0) + } +} + +impl TryFrom for ListDelimiter { + type Error = Component; + + fn try_from(value: Component) -> Result { + CowString::try_from(value).map(Self) + } +} + +impl RequireInvalidation for ListDelimiter { + fn requires_invalidation(&self) -> bool { + true + } +} + +define_components! { + List { + /// The delimiter to place between nested lists when using merged list + /// indicators. + Delimiter(ListDelimiter, "delimiter", ".".into()) + /// The delimiter to place after the list indictor, when the list is + /// ordered. + TrailingDelimiter(ListDelimiter, "trailing_delimiter", @Delimiter) + /// The prefix to display before the list indicator. + Prefix(ListDelimiter, "prefix") + } +} diff --git a/src/widgets/radio.rs b/src/widgets/radio.rs index b53e27f..f3c2aad 100644 --- a/src/widgets/radio.rs +++ b/src/widgets/radio.rs @@ -52,7 +52,7 @@ impl Radio { impl MakeWidgetWithTag for Radio where - T: Clone + Debug + Eq + Send + 'static, + T: Clone + Debug + PartialEq + Send + 'static, { fn make_with_tag(self, id: crate::widget::WidgetTag) -> WidgetInstance { RadioOrnament { @@ -78,7 +78,7 @@ struct RadioOrnament { impl Widget for RadioOrnament where - T: Debug + Eq + Send + 'static, + T: Debug + PartialEq + Send + 'static, { fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_>) { let radio_size = context diff --git a/src/widgets/select.rs b/src/widgets/select.rs index 4b608d9..d27f790 100644 --- a/src/widgets/select.rs +++ b/src/widgets/select.rs @@ -47,7 +47,7 @@ impl Select { impl MakeWidgetWithTag for Select where - T: Clone + Debug + Eq + Send + Sync + 'static, + T: Clone + Debug + PartialEq + Send + Sync + 'static, { fn make_with_tag(self, id: crate::widget::WidgetTag) -> WidgetInstance { let selected = self.state.map_each({