use std::mem;
use std::time::Duration;
use figures::units::{Lp, Px, UPx};
use figures::{
FloatConversion, IntoSigned, IntoUnsigned, Point, Rect, Round, ScreenScale, Size, Zero,
};
use intentional::Cast;
use kempt::Set;
use kludgine::app::winit::event::{MouseScrollDelta, TouchPhase};
use kludgine::app::winit::window::CursorIcon;
use kludgine::shapes::{CornerRadii, Shape};
use kludgine::Color;
use crate::animation::{AnimationHandle, AnimationTarget, IntoAnimate, Spawn, ZeroToOne};
use crate::context::{AsEventContext, EventContext, LayoutContext};
use crate::styles::components::{EasingIn, EasingOut, LineHeight, PrimaryColor, SurfaceColor};
use crate::styles::Dimension;
use crate::value::{
Destination, Dynamic, DynamicReader, IntoDynamic, IntoValue, MapEachCloned, Source, Value,
};
use crate::widget::{EventHandling, MakeWidget, Widget, WidgetId, WidgetRef, HANDLED, IGNORED};
use crate::window::DeviceId;
use crate::ConstraintLimit;
#[derive(Debug)]
pub(crate) struct OwnedWidget<W>(OwnedWidgetState<W>);
#[derive(Debug)]
enum OwnedWidgetState<W> {
Unmade(W),
Making,
Made(WidgetRef),
}
impl<W> OwnedWidget<W>
where
W: Widget,
{
pub const fn new(widget: W) -> Self {
Self(OwnedWidgetState::Unmade(widget))
}
pub fn make_if_needed(&mut self) -> &mut WidgetRef {
if matches!(&self.0, OwnedWidgetState::Unmade(_)) {
let OwnedWidgetState::Unmade(widget) =
mem::replace(&mut self.0, OwnedWidgetState::Making)
else {
unreachable!("just matched")
};
self.0 = OwnedWidgetState::Made(WidgetRef::new(widget));
}
self.expect_made_mut()
}
pub fn expect_made(&self) -> &WidgetRef {
let OwnedWidgetState::Made(widget) = &self.0 else {
unreachable!("widget made")
};
widget
}
pub fn expect_made_mut(&mut self) -> &mut WidgetRef {
let OwnedWidgetState::Made(widget) = &mut self.0 else {
unreachable!("widget made")
};
widget
}
pub fn expect_unmade_mut(&mut self) -> &mut W {
let OwnedWidgetState::Unmade(widget) = &mut self.0 else {
unreachable!("widget unmade")
};
widget
}
}
impl<T> Default for OwnedWidget<T>
where
T: Widget + Default,
{
fn default() -> Self {
Self::new(T::default())
}
}
#[derive(Debug)]
pub struct Scroll {
contents: WidgetRef,
content_size: Dynamic<Size<UPx>>,
control_size: Dynamic<Size<UPx>>,
pub scroll: Dynamic<Point<UPx>>,
enabled: Point<bool>,
max_scroll: DynamicReader<Point<UPx>>,
vertical_widget: OwnedWidget<ScrollBar>,
horizontal_widget: OwnedWidget<ScrollBar>,
}
#[derive(Debug)]
struct OpacityAnimationState {
hovering: Set<WidgetId>,
is_hide: bool,
will_hide: bool,
handle: AnimationHandle,
}
impl Scroll {
fn construct(contents: impl MakeWidget, enabled: Point<bool>) -> Self {
let scroll = Dynamic::<Point<UPx>>::default();
let content_size = Dynamic::<Size<UPx>>::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 horizontal = ScrollBar::new(content_size.map_each_cloned(|size| size.width), x, false);
let y = scroll.map_each_cloned(|scroll| scroll.y);
y.for_each_cloned({
let scroll = scroll.clone();
move |y| {
if let Ok(mut scroll) = scroll.try_lock() {
if scroll.y != y {
scroll.y = y;
}
}
}
})
.persist();
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();
Self {
contents: WidgetRef::new(contents),
enabled,
content_size,
control_size: Dynamic::new(Size::default()),
scroll,
max_scroll,
horizontal_widget: OwnedWidget::new(horizontal),
vertical_widget: OwnedWidget::new(vertical),
}
}
pub fn new(contents: impl MakeWidget) -> Self {
Self::construct(contents, Point::new(true, true))
}
pub fn horizontal(contents: impl MakeWidget) -> Self {
Self::construct(contents, Point::new(true, false))
}
pub fn vertical(contents: impl MakeWidget) -> Self {
Self::construct(contents, Point::new(false, true))
}
#[must_use]
pub fn preserve_max_scroll(mut self, preserve: impl IntoValue<bool>) -> Self {
let preserve = preserve.into_value();
self.vertical_widget.expect_unmade_mut().preserve_max_scroll = preserve.clone();
self.horizontal_widget
.expect_unmade_mut()
.preserve_max_scroll = preserve;
self
}
#[must_use]
pub const fn max_scroll(&self) -> &DynamicReader<Point<UPx>> {
&self.max_scroll
}
#[must_use]
pub fn content_size(&self) -> DynamicReader<Size<UPx>> {
self.content_size.create_reader()
}
#[must_use]
pub fn control_size(&self) -> DynamicReader<Size<UPx>> {
self.control_size.create_reader()
}
fn show_scrollbars(&mut self, context: &mut EventContext<'_>) {
let mut horizontal = self.horizontal_widget.expect_made_mut().widget().lock();
horizontal
.downcast_mut::<ScrollBar>()
.expect("a ScrollBar")
.show(context);
}
}
impl Widget for Scroll {
fn unmounted(&mut self, context: &mut EventContext<'_>) {
self.contents.unmount_in(context);
}
fn hit_test(&mut self, _location: Point<Px>, _context: &mut EventContext<'_>) -> bool {
true
}
fn hover(
&mut self,
_location: Point<Px>,
context: &mut EventContext<'_>,
) -> Option<CursorIcon> {
let mut horizontal = self.horizontal_widget.expect_made_mut().widget().lock();
horizontal
.downcast_mut::<ScrollBar>()
.expect("a ScrollBar")
.hover(context);
None
}
fn unhover(&mut self, context: &mut EventContext<'_>) {
let mut horizontal = self.horizontal_widget.expect_made_mut().widget().lock();
horizontal
.downcast_mut::<ScrollBar>()
.expect("a ScrollBar")
.unhover(context);
}
fn redraw(&mut self, context: &mut crate::context::GraphicsContext<'_, '_, '_, '_>) {
let contents = self.contents.mounted(&mut context.as_event_context());
context.for_other(&contents).redraw();
if self.enabled.x {
let horizontal = self
.horizontal_widget
.expect_made_mut()
.mounted(&mut context.as_event_context());
context.for_other(&horizontal).redraw();
}
if self.enabled.y {
let vertical = self
.vertical_widget
.expect_made_mut()
.mounted(&mut context.as_event_context());
context.for_other(&vertical).redraw();
}
}
fn layout(
&mut self,
available_space: Size<ConstraintLimit>,
context: &mut LayoutContext<'_, '_, '_, '_>,
) -> Size<UPx> {
let max_extents = Size::new(
if self.enabled.x {
ConstraintLimit::SizeToFit(UPx::MAX)
} else {
available_space.width
},
if self.enabled.y {
ConstraintLimit::SizeToFit(UPx::MAX)
} else {
available_space.height
},
);
let contents = self.contents.mounted(&mut context.as_event_context());
let new_content_size = context.for_other(&contents).layout(max_extents);
self.content_size.set(new_content_size);
let new_control_size = Size::new(
if self.enabled.x {
constrain_child(available_space.width, new_content_size.width)
} else {
new_content_size.width.into_unsigned()
},
if self.enabled.y {
constrain_child(available_space.height, new_content_size.height)
} else {
new_content_size.height.into_unsigned()
},
);
let horizontal = self
.horizontal_widget
.make_if_needed()
.mounted(&mut context.as_event_context());
let layout = context.for_other(&horizontal).layout(available_space);
context.set_child_layout(
&horizontal,
Rect::new(
Point::new(
Px::ZERO,
max_extents
.height
.fit_measured(new_control_size.height)
.saturating_sub(layout.height)
.into_signed(),
),
layout.into_signed(),
),
);
let vertical = self
.vertical_widget
.make_if_needed()
.mounted(&mut context.as_event_context());
let layout = context.for_other(&vertical).layout(available_space);
context.set_child_layout(
&vertical,
Rect::new(
Point::new(
max_extents
.width
.fit_measured(new_control_size.width)
.saturating_sub(layout.width)
.into_signed(),
Px::ZERO,
),
layout.into_signed(),
),
);
let scroll = self.scroll.get_tracking_invalidate(context);
self.control_size.set(new_control_size);
let region = Rect::new(
-scroll.into_signed(),
new_content_size
.min(Size::new(UPx::MAX, UPx::MAX) - scroll.max(Point::default()))
.into_signed(),
);
context.set_child_layout(&contents, region);
new_control_size
}
fn mouse_wheel(
&mut self,
_device_id: DeviceId,
delta: MouseScrollDelta,
_phase: TouchPhase,
context: &mut EventContext<'_>,
) -> EventHandling {
let mut handled = false;
{
let mut vertical = self.vertical_widget.expect_made().widget().lock();
handled |= vertical
.downcast_mut::<ScrollBar>()
.expect("a ScrollBar")
.mouse_wheel(delta, context)
.is_break();
let mut horizontal = self.horizontal_widget.expect_made().widget().lock();
handled |= horizontal
.downcast_mut::<ScrollBar>()
.expect("a ScrollBar")
.mouse_wheel(delta, context)
.is_break();
}
if handled {
self.show_scrollbars(context);
context.set_needs_redraw();
HANDLED
} else {
IGNORED
}
}
fn summarize(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
fmt.debug_struct("Scroll")
.field("enabled", &self.enabled)
.field("contents", &self.contents)
.finish()
}
}
#[derive(Default, Debug)]
struct DragInfo {
mouse_buttons_down: usize,
start: Px,
start_scroll: UPx,
in_bar: bool,
}
impl DragInfo {
fn update(
&self,
location: Px,
dynamic_scroll: &Dynamic<UPx>,
info: &ScrollbarInfo,
max_scroll: UPx,
control_size: UPx,
) {
let scroll = self.update_bar(
location,
self.start,
max_scroll,
self.start_scroll,
info,
control_size,
);
dynamic_scroll.set(scroll);
}
fn update_bar(
&self,
location: Px,
start: Px,
max_scroll: UPx,
start_scroll: UPx,
bar: &ScrollbarInfo,
control_size: UPx,
) -> UPx {
if self.in_bar {
let dy = location - start;
if dy == 0 {
start_scroll
} else {
(start_scroll.into_signed()
+ Px::from(
dy.into_float() / (control_size - bar.size).into_float()
* bar.amount_hidden.into_float(),
))
.into_unsigned()
.min(max_scroll)
}
} else {
max_scroll
* ((location - bar.size.into_signed() / 2)
.max(Px::ZERO)
.into_float()
/ (control_size - bar.size).into_float())
}
}
}
fn constrain_child(constraint: ConstraintLimit, measured: UPx) -> UPx {
match constraint {
ConstraintLimit::Fill(size) => size.min(measured),
ConstraintLimit::SizeToFit(_) => measured,
}
}
#[derive(Debug, Default)]
struct ScrollbarInfo {
offset: UPx,
amount_hidden: UPx,
size: UPx,
}
fn scrollbar_region(
scroll: UPx,
content_size: UPx,
control_size: UPx,
minimum_thumb_size: UPx,
) -> ScrollbarInfo {
if content_size > control_size {
let amount_hidden = content_size - control_size;
let ratio_visible = control_size.into_float() / content_size.into_float();
let bar_size = (control_size * ratio_visible).max(minimum_thumb_size);
let remaining_area = control_size - bar_size;
let amount_scrolled = scroll.into_float() / amount_hidden.into_float();
let bar_offset = remaining_area * amount_scrolled;
ScrollbarInfo {
offset: bar_offset,
amount_hidden,
size: bar_size,
}
} else {
ScrollbarInfo::default()
}
}
#[derive(Debug)]
pub struct ScrollBar {
content_size: Dynamic<UPx>,
last_content_size: UPx,
scroll: Dynamic<UPx>,
preserve_max_scroll: Value<bool>,
max_scroll: Dynamic<UPx>,
bar_width: UPx,
control_size: UPx,
line_height: UPx,
vertical: bool,
info: ScrollbarInfo,
scrollbar_opacity: Dynamic<ZeroToOne>,
scrollbar_opacity_animation: Dynamic<OpacityAnimationState>,
drag: DragInfo,
}
impl ScrollBar {
pub fn new(
content_size: impl IntoDynamic<UPx>,
scroll_by: impl IntoDynamic<UPx>,
vertical: bool,
) -> Self {
Self {
content_size: content_size.into_dynamic(),
scroll: scroll_by.into_dynamic(),
preserve_max_scroll: Value::Constant(true),
max_scroll: Dynamic::new(UPx::ZERO),
bar_width: UPx::ZERO,
line_height: UPx::ZERO,
control_size: UPx::ZERO,
vertical,
info: ScrollbarInfo::default(),
scrollbar_opacity: Dynamic::default(),
scrollbar_opacity_animation: Dynamic::new(OpacityAnimationState {
handle: AnimationHandle::new(),
will_hide: true,
is_hide: true,
hovering: Set::new(),
}),
drag: DragInfo::default(),
last_content_size: UPx::ZERO,
}
}
#[must_use]
pub fn preserve_max_scroll(mut self, preserve_max_scroll: impl IntoValue<bool>) -> Self {
self.preserve_max_scroll = preserve_max_scroll.into_value();
self
}
#[must_use]
pub fn max_scroll(&self) -> DynamicReader<UPx> {
self.max_scroll.create_reader()
}
#[must_use]
pub fn mouse_wheel(
&mut self,
delta: MouseScrollDelta,
context: &mut EventContext<'_>,
) -> EventHandling {
let amount = match delta {
MouseScrollDelta::LineDelta(x, y) => Point::new(x, y) * self.line_height.into_float(),
MouseScrollDelta::PixelDelta(px) => Point::new(px.x.cast(), px.y.cast()),
};
let amount = if self.vertical { amount.y } else { amount.x };
let mut scroll = self.scroll.lock();
let old_scroll = *scroll;
let new_scroll = Self::constrained_scroll(
(scroll.into_signed() - amount.cast::<Px>()).into_unsigned(),
self.max_scroll.get(),
);
if old_scroll == new_scroll {
IGNORED
} else {
*scroll = new_scroll;
drop(scroll);
self.show(context);
HANDLED
}
}
fn constrained_scroll(scroll: UPx, max_scroll: UPx) -> UPx {
scroll.min(max_scroll)
}
pub fn synchronize_visibility_with(&mut self, other: &ScrollBar) {
self.scrollbar_opacity = other.scrollbar_opacity.clone();
self.scrollbar_opacity_animation = other.scrollbar_opacity_animation.clone();
}
pub fn hover(&mut self, context: &mut EventContext<'_>) {
self.scrollbar_opacity_animation
.lock()
.hovering
.insert(context.widget().id());
self.show(context);
}
pub fn unhover(&mut self, context: &mut EventContext<'_>) {
self.scrollbar_opacity_animation
.lock()
.hovering
.remove(&context.widget().id());
self.hide(context);
}
pub fn show(&mut self, context: &mut EventContext<'_>) {
let mut animation_state = self.scrollbar_opacity_animation.lock();
let should_hide = self.drag.mouse_buttons_down == 0 && animation_state.hovering.is_empty();
if animation_state.is_hide
|| should_hide != animation_state.will_hide
|| animation_state.handle.is_complete()
{
let current_opacity = self.scrollbar_opacity.get();
let transition_time = *current_opacity.one_minus() / 4.;
let animation = self
.scrollbar_opacity
.transition_to(ZeroToOne::ONE)
.over(Duration::from_secs_f32(transition_time))
.with_easing(context.get(&EasingIn));
animation_state.is_hide = false;
animation_state.will_hide = should_hide;
animation_state.handle = if should_hide {
animation
.and_then(Duration::from_secs(1))
.and_then(
self.scrollbar_opacity
.transition_to(ZeroToOne::ZERO)
.over(Duration::from_millis(300))
.with_easing(context.get(&EasingOut)),
)
.spawn()
} else {
animation.spawn()
};
}
}
pub fn hide(&mut self, context: &mut EventContext<'_>) {
let mut animation_state = self.scrollbar_opacity_animation.lock();
if self.drag.mouse_buttons_down == 0
&& !animation_state.will_hide
&& animation_state.hovering.is_empty()
{
animation_state.is_hide = true;
animation_state.will_hide = true;
animation_state.handle = self
.scrollbar_opacity
.transition_to(ZeroToOne::ZERO)
.over(Duration::from_millis(300))
.with_easing(context.get(&EasingOut))
.spawn();
}
}
}
impl Widget for ScrollBar {
fn redraw(&mut self, context: &mut crate::context::GraphicsContext<'_, '_, '_, '_>) {
let scroll = self.scroll.get_tracking_redraw(context);
let content_size = self.content_size.get_tracking_redraw(context);
let control_size = context.gfx.region().size.into_unsigned();
let scrolled_to_end = scroll == self.info.amount_hidden;
self.control_size = if self.vertical {
control_size.height
} else {
control_size.width
};
self.info = scrollbar_region(scroll, content_size, self.control_size, self.bar_width);
let mut constrained = Self::constrained_scroll(scroll, self.info.amount_hidden);
if scrolled_to_end
&& self.last_content_size != 0
&& self.last_content_size != content_size
&& self.preserve_max_scroll.get()
{
constrained = self.info.amount_hidden;
}
self.last_content_size = content_size;
self.scroll.set(constrained);
self.max_scroll.set(self.info.amount_hidden);
let opacity = self.scrollbar_opacity.get_tracking_redraw(context);
if context.enabled() && self.info.amount_hidden > 0 && opacity > 0. {
let rect = if self.vertical {
Rect::new(
Point::new(control_size.width - self.bar_width, self.info.offset),
Size::new(self.bar_width, self.info.size),
)
} else {
Rect::new(
Point::new(self.info.offset, control_size.height - self.bar_width),
Size::new(self.info.size, self.bar_width),
)
}
.into_signed(); let radii = context
.get(&ScrollBarThumbCornerRadius)
.into_px(context.gfx.scale())
.map(Px::ceil);
let color = context.get(&ScrollBarThumbColor);
let color = color.with_alpha_f32(color.alpha_f32() * *opacity);
let outline_size = context
.get(&ScrollBarThumbOutlineThickness)
.into_px(context.gfx.scale())
.ceil();
let outline_color = context.get(&ScrollBarThumbOutlineColor);
let outline_color = outline_color.with_alpha_f32(outline_color.alpha_f32() * *opacity);
if radii.is_zero() {
context
.gfx
.draw_shape(&Shape::filled_rect(rect.inset(outline_size / 2), color));
if outline_size > 0 {
context.gfx.draw_shape(&Shape::stroked_rect(
rect.inset(outline_size / 2),
outline_color,
));
}
} else {
context.gfx.draw_shape(&Shape::filled_round_rect(
rect.inset(outline_size / 2),
radii,
color,
));
if outline_size > 0 {
context.gfx.draw_shape(&Shape::stroked_round_rect(
rect.inset(outline_size / 2),
radii,
outline_color,
));
}
}
}
}
fn hit_test(&mut self, _location: Point<Px>, context: &mut EventContext<'_>) -> bool {
self.max_scroll.get() > 0 && context.enabled()
}
fn hover(
&mut self,
_location: Point<Px>,
context: &mut EventContext<'_>,
) -> Option<CursorIcon> {
self.scrollbar_opacity_animation
.lock()
.hovering
.insert(context.widget().id());
self.show(context);
None
}
fn unhover(&mut self, context: &mut EventContext<'_>) {
self.scrollbar_opacity_animation
.lock()
.hovering
.remove(&context.widget().id());
self.hide(context);
}
fn layout(
&mut self,
available_space: Size<ConstraintLimit>,
context: &mut LayoutContext<'_, '_, '_, '_>,
) -> Size<UPx> {
self.bar_width = context
.get(&ScrollBarThickness)
.into_upx(context.gfx.scale())
.ceil();
self.line_height = context.get(&LineHeight).into_upx(context.gfx.scale());
if self.vertical {
Size::new(self.bar_width, available_space.height.max())
} else {
Size::new(available_space.width.max(), self.bar_width)
}
}
fn mouse_wheel(
&mut self,
_device_id: DeviceId,
delta: MouseScrollDelta,
_phase: TouchPhase,
context: &mut EventContext<'_>,
) -> EventHandling {
self.mouse_wheel(delta, context)
}
fn mouse_down(
&mut self,
location: Point<Px>,
_device_id: DeviceId,
_button: kludgine::app::winit::event::MouseButton,
context: &mut EventContext<'_>,
) -> EventHandling {
if self.max_scroll.get().is_zero() || !context.enabled() {
return IGNORED;
}
self.drag.start = if self.vertical {
location.y
} else {
location.x
};
self.drag.start_scroll = self.scroll.get();
let relative = self.drag.start - self.info.offset.into_signed();
self.drag.in_bar = relative >= 0 && relative < self.info.size;
if !self.drag.in_bar {
self.drag.update(
self.drag.start,
&self.scroll,
&self.info,
self.max_scroll.get(),
self.control_size,
);
}
self.drag.mouse_buttons_down += 1;
self.show(context);
HANDLED
}
fn mouse_drag(
&mut self,
location: Point<Px>,
_device_id: DeviceId,
_button: kludgine::app::winit::event::MouseButton,
_context: &mut EventContext<'_>,
) {
let offset = if self.vertical {
location.y
} else {
location.x
};
self.drag.update(
offset,
&self.scroll,
&self.info,
self.max_scroll.get(),
self.control_size,
);
}
fn mouse_up(
&mut self,
location: Option<Point<Px>>,
_device_id: DeviceId,
_button: kludgine::app::winit::event::MouseButton,
context: &mut EventContext<'_>,
) {
self.drag.mouse_buttons_down -= 1;
if self.drag.mouse_buttons_down == 0 {
if location.map_or(false, |location| {
let offset = if self.vertical {
location.y
} else {
location.x
};
offset >= 0 && offset < self.control_size
}) {
self.scrollbar_opacity_animation.lock().handle.clear();
self.show(context);
} else {
self.hide(context);
}
}
}
}
define_components! {
Scroll {
ScrollBarThickness(Dimension, "size", Dimension::Lp(Lp::points(7)))
ScrollBarThumbColor(Color, "thumb_color", @PrimaryColor)
ScrollBarThumbOutlineColor(Color, "thumb_outline_color", @SurfaceColor)
ScrollBarThumbOutlineThickness(Dimension, "thumb_outline_size", Dimension::Lp(Lp::points(1)))
ScrollBarThumbCornerRadius(CornerRadii<Dimension>, "corner_radius", |context| CornerRadii::from(context.get(&ScrollBarThickness)))
}
}