use crate::animation::Easing;
use crate::widget::operation::Outcome;
use crate::widget::{Id, Operation};
use crate::{Rectangle, Vector};
pub trait Scrollable {
fn snap_to(&mut self, offset: RelativeOffset<Option<f32>>);
fn scroll_to(&mut self, offset: AbsoluteOffset<Option<f32>>);
fn scroll_by(&mut self, offset: AbsoluteOffset, bounds: Rectangle, content_bounds: Rectangle);
fn scroll_to_animated(
&mut self,
offset: AbsoluteOffset<Option<f32>>,
_animation: ScrollAnimation,
) {
self.scroll_to(offset);
}
}
#[derive(Debug, Clone, Copy)]
pub struct ScrollAnimation {
pub duration_secs: f32,
pub easing: Easing,
}
impl Default for ScrollAnimation {
fn default() -> Self {
Self {
duration_secs: 0.3,
easing: Easing::EaseOutCubic,
}
}
}
impl ScrollAnimation {
#[must_use]
pub fn new(duration_secs: f32) -> Self {
Self {
duration_secs: duration_secs.max(0.0),
easing: Easing::EaseOutCubic,
}
}
#[must_use]
pub const fn with_easing(mut self, easing: Easing) -> Self {
self.easing = easing;
self
}
}
pub fn snap_to<T>(target: Id, offset: RelativeOffset<Option<f32>>) -> impl Operation<T> {
struct SnapTo {
target: Id,
offset: RelativeOffset<Option<f32>>,
}
impl<T> Operation<T> for SnapTo {
fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation<T>)) {
operate(self);
}
fn scrollable(
&mut self,
id: Option<&Id>,
_bounds: Rectangle,
_content_bounds: Rectangle,
_translation: Vector,
state: &mut dyn Scrollable,
) {
if Some(&self.target) == id {
state.snap_to(self.offset);
}
}
}
SnapTo { target, offset }
}
pub fn scroll_to<T>(target: Id, offset: AbsoluteOffset<Option<f32>>) -> impl Operation<T> {
struct ScrollTo {
target: Id,
offset: AbsoluteOffset<Option<f32>>,
}
impl<T> Operation<T> for ScrollTo {
fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation<T>)) {
operate(self);
}
fn scrollable(
&mut self,
id: Option<&Id>,
_bounds: Rectangle,
_content_bounds: Rectangle,
_translation: Vector,
state: &mut dyn Scrollable,
) {
if Some(&self.target) == id {
state.scroll_to(self.offset);
}
}
}
ScrollTo { target, offset }
}
pub fn scroll_to_animated<T>(
target: Id,
offset: AbsoluteOffset<Option<f32>>,
animation: ScrollAnimation,
) -> impl Operation<T> {
struct ScrollToAnimated {
target: Id,
offset: AbsoluteOffset<Option<f32>>,
animation: ScrollAnimation,
}
impl<T> Operation<T> for ScrollToAnimated {
fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation<T>)) {
operate(self);
}
fn scrollable(
&mut self,
id: Option<&Id>,
_bounds: Rectangle,
_content_bounds: Rectangle,
_translation: Vector,
state: &mut dyn Scrollable,
) {
if Some(&self.target) == id {
state.scroll_to_animated(self.offset, self.animation);
}
}
}
ScrollToAnimated {
target,
offset,
animation,
}
}
pub fn scroll_by<T>(target: Id, offset: AbsoluteOffset) -> impl Operation<T> {
struct ScrollBy {
target: Id,
offset: AbsoluteOffset,
}
impl<T> Operation<T> for ScrollBy {
fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation<T>)) {
operate(self);
}
fn scrollable(
&mut self,
id: Option<&Id>,
bounds: Rectangle,
content_bounds: Rectangle,
_translation: Vector,
state: &mut dyn Scrollable,
) {
if Some(&self.target) == id {
state.scroll_by(self.offset, bounds, content_bounds);
}
}
}
ScrollBy { target, offset }
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct AbsoluteOffset<T = f32> {
pub x: T,
pub y: T,
}
impl From<AbsoluteOffset> for AbsoluteOffset<Option<f32>> {
fn from(offset: AbsoluteOffset) -> Self {
Self {
x: Some(offset.x),
y: Some(offset.y),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct RelativeOffset<T = f32> {
pub x: T,
pub y: T,
}
impl RelativeOffset {
pub const START: Self = Self { x: 0.0, y: 0.0 };
pub const END: Self = Self { x: 1.0, y: 1.0 };
}
impl From<RelativeOffset> for RelativeOffset<Option<f32>> {
fn from(offset: RelativeOffset) -> Self {
Self {
x: Some(offset.x),
y: Some(offset.y),
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct EnsureVisibleConfig {
pub alignment_y: f32,
pub alignment_x: f32,
pub animation: Option<ScrollAnimation>,
}
impl Default for EnsureVisibleConfig {
fn default() -> Self {
Self {
alignment_y: Self::DEFAULT_ALIGNMENT,
alignment_x: Self::DEFAULT_ALIGNMENT,
animation: Some(ScrollAnimation::default()),
}
}
}
impl EnsureVisibleConfig {
pub const DEFAULT_ALIGNMENT: f32 = 0.35;
#[must_use]
pub fn new(alignment_y: f32, alignment_x: f32) -> Self {
Self {
alignment_y: alignment_y.clamp(0.0, 1.0),
alignment_x: alignment_x.clamp(0.0, 1.0),
animation: Some(ScrollAnimation::default()),
}
}
#[must_use]
pub fn vertical(alignment_y: f32) -> Self {
Self {
alignment_y: alignment_y.clamp(0.0, 1.0),
alignment_x: Self::DEFAULT_ALIGNMENT,
animation: Some(ScrollAnimation::default()),
}
}
pub const CENTER: Self = Self {
alignment_y: 0.5,
alignment_x: 0.5,
animation: Some(ScrollAnimation {
duration_secs: 0.3,
easing: Easing::EaseOutCubic,
}),
};
#[must_use]
pub const fn with_animation(mut self, animation: ScrollAnimation) -> Self {
self.animation = Some(animation);
self
}
#[must_use]
pub const fn instant(mut self) -> Self {
self.animation = None;
self
}
}
pub fn ensure_focused_visible_op<T>(config: EnsureVisibleConfig) -> impl Operation<T> {
EnsureFocusedVisibleOp {
config,
scrollable_ctx: None,
scrollable_index: 0,
target: None,
}
}
struct EnsureFocusedVisibleOp {
config: EnsureVisibleConfig,
scrollable_ctx: Option<(usize, ScrollableContext)>,
scrollable_index: usize,
target: Option<(usize, AbsoluteOffset<Option<f32>>)>,
}
#[derive(Clone)]
struct ScrollableContext {
bounds: Rectangle,
content_bounds: Rectangle,
}
impl<T> Operation<T> for EnsureFocusedVisibleOp {
fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation<T>)) {
operate(self);
}
fn scrollable(
&mut self,
_id: Option<&Id>,
bounds: Rectangle,
content_bounds: Rectangle,
_translation: Vector,
_state: &mut dyn Scrollable,
) {
if self.target.is_some() {
return;
}
self.scrollable_ctx = Some((
self.scrollable_index,
ScrollableContext {
bounds,
content_bounds,
},
));
self.scrollable_index += 1;
}
fn focusable(&mut self, _id: Option<&Id>, bounds: Rectangle, state: &mut dyn super::Focusable) {
if self.target.is_some() || !state.is_focused() {
return;
}
let Some((idx, ctx)) = &self.scrollable_ctx else {
return;
};
if let Some(offset) =
compute_scroll_offset(bounds, ctx.bounds, ctx.content_bounds, self.config)
{
self.target = Some((*idx, offset));
}
}
fn finish(&self) -> Outcome<T> {
if let Some((idx, offset)) = &self.target {
Outcome::Chain(Box::new(ApplyScrollAtIndex {
target_index: *idx,
current: 0,
offset: *offset,
animation: self.config.animation,
}))
} else {
Outcome::None
}
}
}
struct ApplyScrollAtIndex {
target_index: usize,
current: usize,
offset: AbsoluteOffset<Option<f32>>,
animation: Option<ScrollAnimation>,
}
impl<T> Operation<T> for ApplyScrollAtIndex {
fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation<T>)) {
operate(self);
}
fn scrollable(
&mut self,
_id: Option<&Id>,
_bounds: Rectangle,
_content_bounds: Rectangle,
_translation: Vector,
state: &mut dyn Scrollable,
) {
if self.current == self.target_index {
if let Some(animation) = self.animation {
state.scroll_to_animated(self.offset, animation);
} else {
state.scroll_to(self.offset);
}
}
self.current += 1;
}
}
pub fn compute_scroll_offset(
focused_bounds: Rectangle,
scrollable_bounds: Rectangle,
content_bounds: Rectangle,
config: EnsureVisibleConfig,
) -> Option<AbsoluteOffset<Option<f32>>> {
let target_y = if content_bounds.height > scrollable_bounds.height {
let focused_y = focused_bounds.y - scrollable_bounds.y;
let focused_center_y = focused_y + focused_bounds.height / 2.0;
let viewport_target_y = scrollable_bounds.height * config.alignment_y;
let desired = focused_center_y - viewport_target_y;
let max_scroll = content_bounds.height - scrollable_bounds.height;
Some(desired.clamp(0.0, max_scroll))
} else {
None
};
let target_x = if content_bounds.width > scrollable_bounds.width {
let focused_x = focused_bounds.x - scrollable_bounds.x;
let focused_center_x = focused_x + focused_bounds.width / 2.0;
let viewport_target_x = scrollable_bounds.width * config.alignment_x;
let desired = focused_center_x - viewport_target_x;
let max_scroll = content_bounds.width - scrollable_bounds.width;
Some(desired.clamp(0.0, max_scroll))
} else {
None
};
if target_x.is_some() || target_y.is_some() {
Some(AbsoluteOffset {
x: target_x,
y: target_y,
})
} else {
None
}
}