diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c37beb..04a9cdc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -60,6 +60,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `ConstraintLimit::fit_measured` and `FitMeasuredSize::fit_measured` now accept either a `Px` or `UPx` measurement, and does not perform scaling adjustments. To convert `Lp` use `into_upx()` first. +- `IntoReader::to_label` and `IntoReader::into_label` have been moved to their + own trait: `Displayable`. This allows more flexible acceptance of types. ### Changed @@ -269,6 +271,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 @danbulant for helping with this change! - `ScrollBar` is a new widget that renders a scroll bar meant to scroll through a large container. +- `Label::overflow` allows customizing the behavior for a label when it cannot + be drawn on a single line. [139]: https://github.com/khonsulabs/cushy/issues/139 diff --git a/examples/basic-button.rs b/examples/basic-button.rs index 8d2b5b3..6259490 100644 --- a/examples/basic-button.rs +++ b/examples/basic-button.rs @@ -1,5 +1,6 @@ -use cushy::value::{Destination, Dynamic, IntoReader, Source}; +use cushy::value::{Destination, Dynamic, Source}; use cushy::widget::MakeWidget; +use cushy::widgets::label::Displayable; use cushy::Run; // begin rustme snippet: readme diff --git a/examples/counter.rs b/examples/counter.rs index c2cc86f..cfd40c2 100644 --- a/examples/counter.rs +++ b/examples/counter.rs @@ -1,6 +1,7 @@ use cushy::figures::units::Lp; -use cushy::value::{Dynamic, IntoReader}; +use cushy::value::Dynamic; use cushy::widget::MakeWidget; +use cushy::widgets::label::Displayable; use cushy::Run; fn main() -> cushy::Result { diff --git a/examples/debug-window.rs b/examples/debug-window.rs index 2f099a1..9113d62 100644 --- a/examples/debug-window.rs +++ b/examples/debug-window.rs @@ -1,6 +1,7 @@ use cushy::debug::DebugContext; -use cushy::value::{Destination, Dynamic, IntoReader}; +use cushy::value::{Destination, Dynamic}; use cushy::widget::MakeWidget; +use cushy::widgets::label::Displayable; use cushy::widgets::slider::Slidable; use cushy::{Application, Open, PendingApp}; diff --git a/examples/slider.rs b/examples/slider.rs index 0d20341..e2990be 100644 --- a/examples/slider.rs +++ b/examples/slider.rs @@ -1,10 +1,11 @@ use cushy::animation::{LinearInterpolate, PercentBetween}; use cushy::figures::units::Lp; use cushy::figures::Ranged; -use cushy::value::{Destination, Dynamic, ForEach, IntoReader, Source}; +use cushy::value::{Destination, Dynamic, ForEach, Source}; use cushy::widget::MakeWidget; use cushy::widgets::checkbox::Checkable; use cushy::widgets::input::InputValue; +use cushy::widgets::label::Displayable; use cushy::widgets::slider::Slidable; use cushy::Run; diff --git a/examples/virtual-list.rs b/examples/virtual-list.rs index 23bdc33..33f7850 100644 --- a/examples/virtual-list.rs +++ b/examples/virtual-list.rs @@ -2,6 +2,7 @@ use std::time::{SystemTime, UNIX_EPOCH}; use cushy::value::Dynamic; use cushy::widget::MakeWidget; +use cushy::widgets::label::{Displayable, LabelOverflow}; use cushy::widgets::slider::Slidable; use cushy::widgets::VirtualList; use cushy::Run; @@ -14,6 +15,8 @@ fn list() -> impl MakeWidget { .expect("System Time after 1970") .as_secs(); format!("Item {index} - {timestamp}") + .into_label() + .overflow(LabelOverflow::Clip) }); let content_changed = list.content_watcher().clone(); diff --git a/src/value.rs b/src/value.rs index 3ba8eea..bed8666 100644 --- a/src/value.rs +++ b/src/value.rs @@ -24,7 +24,7 @@ use crate::utils::WithClone; use crate::widget::{ MakeWidget, MakeWidgetWithTag, OnceCallback, WidgetId, WidgetInstance, WidgetList, }; -use crate::widgets::{Label, Radio, Select, Space, Switcher}; +use crate::widgets::{Radio, Select, Space, Switcher}; use crate::window::WindowHandle; /// A source of one or more `T` values. @@ -2662,24 +2662,6 @@ impl IntoReadOnly for Owned { pub trait IntoReader { /// Returns this value as a reader. fn into_reader(self) -> DynamicReader; - - /// Returns `self` being `Display`ed in a [`Label`] widget. - fn into_label(self) -> Label - where - Self: Sized, - T: Debug + Display + Send + 'static, - { - Label::new(self.into_reader()) - } - - /// Returns `self` being `Display`ed in a [`Label`] widget. - fn to_label(&self) -> Label - where - Self: Clone, - T: Debug + Display + Send + 'static, - { - self.clone().into_label() - } } impl IntoReader for Dynamic { diff --git a/src/widgets/label.rs b/src/widgets/label.rs index 67b6583..3bc7ba6 100644 --- a/src/widgets/label.rs +++ b/src/widgets/label.rs @@ -1,7 +1,7 @@ //! A read-only text widget. use std::borrow::Cow; -use std::fmt::{Display, Write}; +use std::fmt::{Debug, Display, Write}; use figures::units::{Px, UPx}; use figures::{Point, Round, Size}; @@ -12,7 +12,9 @@ use super::input::CowString; use crate::context::{GraphicsContext, LayoutContext, Trackable, WidgetContext}; use crate::styles::components::TextColor; use crate::styles::FontFamilyList; -use crate::value::{Dynamic, Generation, IntoReadOnly, ReadOnly, Value}; +use crate::value::{ + Dynamic, DynamicReader, Generation, IntoDynamic, IntoReadOnly, IntoValue, ReadOnly, Value, +}; use crate::widget::{MakeWidgetWithTag, Widget, WidgetInstance, WidgetTag}; use crate::window::WindowLocal; use crate::ConstraintLimit; @@ -22,29 +24,46 @@ use crate::ConstraintLimit; pub struct Label { /// The contents of the label. pub display: ReadOnly, + /// The behavior to use when too much text is able to be displayed on a + /// single line. + pub overflow: Value, displayed: String, prepared_text: WindowLocal, } impl Label where - T: std::fmt::Debug + DynamicDisplay + Send + 'static, + T: Debug + DynamicDisplay + Send + 'static, { - /// Returns a new label that displays `text`. + /// Returns a new label that displays `text`, wrapping if necessary to fit + /// the content in the provided space. pub fn new(text: impl IntoReadOnly) -> Self { Self { display: text.into_read_only(), + overflow: Value::Constant(LabelOverflow::WordWrap), displayed: String::new(), prepared_text: WindowLocal::default(), } } + /// Sets the behavior when more text than can fit on a single line is + /// displayed. + #[must_use] + pub fn overflow(mut self, overflow: impl IntoValue) -> Self { + self.overflow = overflow.into_value(); + self + } + fn prepared_text( &mut self, context: &mut GraphicsContext<'_, '_, '_, '_>, color: Color, - width: Px, + mut width: Px, ) -> &MeasuredText { + let overflow = self.overflow.get_tracking_invalidate(context); + if overflow == LabelOverflow::Clip { + width = Px::MAX; + } let check_generation = self.display.generation(); context.apply_current_font_settings(); let current_families = context.current_family_list(); @@ -88,7 +107,7 @@ where impl Widget for Label where - T: std::fmt::Debug + DynamicDisplay + Send + 'static, + T: Debug + DynamicDisplay + Send + 'static, { fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_>) { self.display.invalidate_when_changed(context); @@ -158,6 +177,18 @@ impl MakeWidgetWithTag for &'_ String { } } +/// The overflow behavior for a [`Label`]. +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +#[non_exhaustive] +pub enum LabelOverflow { + /// Any text that cannot be drawn on a single line will be clipped to the + /// bounds of the label. + Clip, + /// Wraps text at the boundaries between words and whitespace while + /// attaching punctuation to the non-wrapped word when possible. + WordWrap, +} + #[derive(Debug)] struct LabelCacheKey { text: MeasuredText, @@ -209,3 +240,65 @@ impl Display for DynamicDisplayer<'_, '_> { self.0.fmt(self.1, f) } } + +/// A type that can be displayed as a [`Label`]. +pub trait Displayable +where + T: Debug + Display + Send + 'static, +{ + /// Returns this value as a displayable reader. + fn into_displayable(self) -> DynamicReader; + + /// Returns `self` being `Display`ed in a [`Label`] widget. + fn into_label(self) -> Label + where + Self: Sized, + T: Debug + Display + Send + 'static, + { + Label::new(self.into_displayable()) + } + + /// Returns `self` being `Display`ed in a [`Label`] widget. + fn to_label(&self) -> Label + where + Self: Clone, + { + self.clone().into_label() + } +} + +impl Displayable for T +where + T: Debug + Display + Send + 'static, +{ + fn into_displayable(self) -> DynamicReader { + Dynamic::new(self).into_reader() + } +} + +impl Displayable for Dynamic +where + T: Debug + Display + Send + 'static, +{ + fn into_displayable(self) -> DynamicReader { + self.into_reader() + } +} + +impl Displayable for DynamicReader +where + T: Debug + Display + Send + 'static, +{ + fn into_displayable(self) -> DynamicReader { + self + } +} + +impl Displayable for Value +where + T: Debug + Display + Send + 'static, +{ + fn into_displayable(self) -> DynamicReader { + self.into_dynamic().into_reader() + } +} diff --git a/src/widgets/scroll.rs b/src/widgets/scroll.rs index bfbec92..a25144a 100644 --- a/src/widgets/scroll.rs +++ b/src/widgets/scroll.rs @@ -715,7 +715,7 @@ impl Widget for ScrollBar { ) } else { Rect::new( - Point::new(UPx::ZERO, control_size.height - self.bar_width), + Point::new(self.info.offset, control_size.height - self.bar_width), Size::new(self.info.size, self.bar_width), ) }; diff --git a/src/widgets/virtual_list.rs b/src/widgets/virtual_list.rs index f228fca..ac5750f 100644 --- a/src/widgets/virtual_list.rs +++ b/src/widgets/virtual_list.rs @@ -4,7 +4,7 @@ use std::ops::Range; use cushy::context::LayoutContext; use cushy::ConstraintLimit; -use figures::{IntoUnsigned, UnscaledUnit}; +use figures::IntoUnsigned; use super::scroll::OwnedWidget; use crate::context::{AsEventContext, EventContext, Trackable}; @@ -12,7 +12,9 @@ use crate::figures::units::{Px, UPx}; use crate::figures::{IntoSigned, Point, Rect, Round, Size, Zero}; use crate::kludgine::app::winit::event::{MouseScrollDelta, TouchPhase}; use crate::kludgine::app::winit::window::CursorIcon; -use crate::value::{Destination, Dynamic, DynamicReader, IntoDynamic, IntoValue, Source, Watcher}; +use crate::value::{ + Destination, Dynamic, DynamicReader, IntoDynamic, IntoValue, MapEachCloned, Source, Watcher, +}; use crate::widget::{ Callback, EventHandling, MakeWidget, MountedWidget, Widget, WidgetInstance, HANDLED, IGNORED, }; @@ -49,6 +51,7 @@ struct VirtualListItem { pub struct VirtualList { make_row: RowMaker, vertical_scroll: OwnedWidget, + horizontal_scroll: OwnedWidget, items: VecDeque, content_size: Dynamic>, contents: Watcher, @@ -88,6 +91,18 @@ impl VirtualList { let item_count = item_count.into_value().into_dynamic().into_reader(); let content_size = Dynamic::new(Size::default()); + let x = scroll.map_each_cloned(|scroll| scroll.x); + x.for_each_cloned({ + let scroll = scroll.clone(); + move |x| { + if let Ok(mut scroll) = scroll.try_lock() { + if scroll.x != x { + scroll.x = x; + } + } + } + }) + .persist(); let y = scroll.map_each_cloned(|scroll| scroll.y); y.for_each_cloned({ let scroll = scroll.clone(); @@ -100,10 +115,12 @@ impl VirtualList { } }) .persist(); - let vertical = ScrollBar::new(content_size.map_each_cloned(|size| size.height), y, true); - let max_scroll = vertical - .max_scroll() - .map_each_cloned(|y| Point::new(UPx::ZERO, y)) + let horizontal = ScrollBar::new(content_size.map_each_cloned(|size| size.width), x, false); + let mut vertical = + ScrollBar::new(content_size.map_each_cloned(|size| size.height), y, true); + vertical.synchronize_visibility_with(&horizontal); + let max_scroll = (&horizontal.max_scroll(), &vertical.max_scroll()) + .map_each_cloned(|(x, y)| Point::new(x, y)) .into_reader(); let contents = Watcher::default(); @@ -114,6 +131,7 @@ impl VirtualList { contents, contents_generation, vertical_scroll: OwnedWidget::new(vertical), + horizontal_scroll: OwnedWidget::new(horizontal), items: VecDeque::new(), control_size: Dynamic::new(Size::default()), content_size, @@ -180,6 +198,52 @@ impl VirtualList { } } + fn layout_scrollbars( + &mut self, + available_space: Size, + new_control_size: Size, + context: &mut LayoutContext<'_, '_, '_, '_>, + ) { + let horizontal = self + .horizontal_scroll + .make_if_needed() + .mounted(&mut context.as_event_context()); + let scrollbar_layout = context.for_other(&horizontal).layout(available_space); + context.set_child_layout( + &horizontal, + Rect::new( + Point::new( + Px::ZERO, + available_space + .height + .fit_measured(new_control_size.height) + .saturating_sub(scrollbar_layout.height) + .into_signed(), + ), + scrollbar_layout.into_signed(), + ), + ); + let vertical = self + .vertical_scroll + .make_if_needed() + .mounted(&mut context.as_event_context()); + let scrollbar_layout = context.for_other(&vertical).layout(available_space); + context.set_child_layout( + &vertical, + Rect::new( + Point::new( + available_space + .width + .fit_measured(new_control_size.width) + .saturating_sub(scrollbar_layout.width) + .into_signed(), + Px::ZERO, + ), + scrollbar_layout.into_signed(), + ), + ); + } + fn layout_rows( &mut self, item_count: usize, @@ -205,29 +269,12 @@ impl VirtualList { item_size.width = new_control_size.width; } - let vertical = self - .vertical_scroll - .make_if_needed() - .mounted(&mut context.as_event_context()); - let scrollbar_layout = context.for_other(&vertical).layout(available_space); - context.set_child_layout( - &vertical, - Rect::new( - Point::new( - available_space - .width - .fit_measured(new_control_size.width) - .saturating_sub(scrollbar_layout.width) - .into_signed(), - Px::ZERO, - ), - scrollbar_layout.into_signed(), - ), - ); + self.layout_scrollbars(available_space, new_control_size, context); let scroll = self.scroll.get_tracking_invalidate(context); + let max_scroll_x = item_size.width.saturating_sub(new_control_size.width); let max_scroll_y = content_height.saturating_sub(new_control_size.height); - let scroll = scroll.min(Point::new(UPx::MAX, max_scroll_y)); + let scroll = scroll.min(Point::new(max_scroll_x, max_scroll_y)); let start_item = (scroll.y.floor() / item_size.height).floor().get() as usize; let end_item = ((scroll.y.ceil() + new_control_size.height) / item_size.height) @@ -271,20 +318,15 @@ impl VirtualList { ); } - // TODO add % to Figures - let mut y = - -UPx::from_unscaled(scroll.y.into_unscaled() % item_size.height.into_unscaled()) - .into_signed(); - let constraint = item_size.map(ConstraintLimit::SizeToFit); + let x = -scroll.x.into_signed(); + let mut y = -(scroll.y % item_size.height).into_signed(); + let constraint = item_size.map(ConstraintLimit::Fill); for item in &self.items { let child_size = context.for_other(&item.mounted).layout(constraint); context.set_child_layout( &item.mounted, - Rect::new( - Point::new(Px::ZERO, y), - item_size.min(child_size).into_signed(), - ), + Rect::new(Point::new(x, y), item_size.min(child_size).into_signed()), ); y += item_size.height.into_signed(); } @@ -354,6 +396,11 @@ impl Widget for VirtualList { .expect_made_mut() .mounted(&mut context.as_event_context()); context.for_other(&vertical).redraw(); + let horizontal = self + .horizontal_scroll + .expect_made_mut() + .mounted(&mut context.as_event_context()); + context.for_other(&horizontal).redraw(); } fn layout( @@ -384,6 +431,12 @@ impl Widget for VirtualList { .expect("a ScrollBar") .mouse_wheel(delta, context) .is_break(); + let mut horizontal = self.horizontal_scroll.expect_made().widget().lock(); + handled |= horizontal + .downcast_mut::() + .expect("a ScrollBar") + .mouse_wheel(delta, context) + .is_break(); } if handled { self.show_scrollbars(context);