mirror of
https://github.com/danbulant/cushy
synced 2026-06-24 09:02:31 +00:00
Label overflow + Virtual list horizontal scrolling
This commit is contained in:
parent
1bb5495f53
commit
96a265854b
10 changed files with 204 additions and 65 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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};
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
20
src/value.rs
20
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<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> {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in a new issue