Label overflow + Virtual list horizontal scrolling

This commit is contained in:
Jonathan Johnson 2024-11-08 10:12:28 -08:00
parent 1bb5495f53
commit 96a265854b
No known key found for this signature in database
GPG key ID: A66D6A34D6620579
10 changed files with 204 additions and 65 deletions

View file

@ -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

View file

@ -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

View file

@ -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 {

View file

@ -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};

View file

@ -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;

View file

@ -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();

View file

@ -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<T> IntoReadOnly<T> for Owned<T> {
pub trait IntoReader<T> {
/// Returns this value as a reader.
fn into_reader(self) -> DynamicReader<T>;
/// Returns `self` being `Display`ed in a [`Label`] widget.
fn into_label(self) -> Label<T>
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<T>
where
Self: Clone,
T: Debug + Display + Send + 'static,
{
self.clone().into_label()
}
}
impl<T> IntoReader<T> for Dynamic<T> {

View file

@ -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<T> {
/// The contents of the label.
pub display: ReadOnly<T>,
/// The behavior to use when too much text is able to be displayed on a
/// single line.
pub overflow: Value<LabelOverflow>,
displayed: String,
prepared_text: WindowLocal<LabelCacheKey>,
}
impl<T> Label<T>
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<T>) -> 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<LabelOverflow>) -> Self {
self.overflow = overflow.into_value();
self
}
fn prepared_text(
&mut self,
context: &mut GraphicsContext<'_, '_, '_, '_>,
color: Color,
width: Px,
mut width: Px,
) -> &MeasuredText<Px> {
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<T> Widget for Label<T>
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<Px>,
@ -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<T>
where
T: Debug + Display + Send + 'static,
{
/// Returns this value as a displayable reader.
fn into_displayable(self) -> DynamicReader<T>;
/// Returns `self` being `Display`ed in a [`Label`] widget.
fn into_label(self) -> Label<T>
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<T>
where
Self: Clone,
{
self.clone().into_label()
}
}
impl<T> Displayable<T> for T
where
T: Debug + Display + Send + 'static,
{
fn into_displayable(self) -> DynamicReader<T> {
Dynamic::new(self).into_reader()
}
}
impl<T> Displayable<T> for Dynamic<T>
where
T: Debug + Display + Send + 'static,
{
fn into_displayable(self) -> DynamicReader<T> {
self.into_reader()
}
}
impl<T> Displayable<T> for DynamicReader<T>
where
T: Debug + Display + Send + 'static,
{
fn into_displayable(self) -> DynamicReader<T> {
self
}
}
impl<T> Displayable<T> for Value<T>
where
T: Debug + Display + Send + 'static,
{
fn into_displayable(self) -> DynamicReader<T> {
self.into_dynamic().into_reader()
}
}

View file

@ -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),
)
};

View file

@ -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<ScrollBar>,
horizontal_scroll: OwnedWidget<ScrollBar>,
items: VecDeque<VirtualListItem>,
content_size: Dynamic<Size<UPx>>,
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<ConstraintLimit>,
new_control_size: Size<UPx>,
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::<ScrollBar>()
.expect("a ScrollBar")
.mouse_wheel(delta, context)
.is_break();
}
if handled {
self.show_scrollbars(context);