use std::ops::{Deref, DerefMut};
use std::sync::{Arc, Mutex, Weak};
use blinc_animation::{AnimationScheduler, Spring, SpringConfig, SpringId};
use blinc_core::{Brush, Shadow};
use crate::div::{Div, ElementBuilder, ElementTypeId};
use crate::element::RenderProps;
use crate::event_handler::{EventContext, EventHandlers};
use crate::selector::ScrollRef;
use crate::stateful::{scroll_events, ScrollState, StateTransitions};
use crate::tree::{LayoutNodeId, LayoutTree};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ScrollDirection {
#[default]
Vertical,
Horizontal,
Both,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ScrollbarVisibility {
Always,
Hover,
#[default]
Auto,
Never,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ScrollbarState {
#[default]
Idle,
TrackHovered,
ThumbHovered,
Dragging,
Scrolling,
FadingOut,
}
impl ScrollbarState {
pub fn is_visible(&self) -> bool {
!matches!(self, ScrollbarState::Idle)
}
pub fn is_interacting(&self) -> bool {
matches!(
self,
ScrollbarState::ThumbHovered | ScrollbarState::Dragging
)
}
pub fn opacity(&self) -> f32 {
match self {
ScrollbarState::Idle => 0.0,
ScrollbarState::FadingOut => 0.3, ScrollbarState::TrackHovered => 0.6,
ScrollbarState::Scrolling => 0.7,
ScrollbarState::ThumbHovered => 0.9,
ScrollbarState::Dragging => 1.0,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ScrollbarSize {
Thin,
#[default]
Normal,
Wide,
}
impl ScrollbarSize {
pub fn width(&self) -> f32 {
match self {
ScrollbarSize::Thin => 4.0,
ScrollbarSize::Normal => 6.0,
ScrollbarSize::Wide => 10.0,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct ScrollbarConfig {
pub visibility: ScrollbarVisibility,
pub size: ScrollbarSize,
pub custom_width: Option<f32>,
pub thumb_color: [f32; 4],
pub thumb_hover_color: [f32; 4],
pub track_color: [f32; 4],
pub corner_radius: f32,
pub edge_padding: f32,
pub auto_dismiss_delay: f32,
pub min_thumb_length: f32,
}
impl Default for ScrollbarConfig {
fn default() -> Self {
Self {
visibility: ScrollbarVisibility::Auto,
size: ScrollbarSize::Normal,
custom_width: None,
thumb_color: [0.5, 0.5, 0.5, 0.5],
thumb_hover_color: [0.6, 0.6, 0.6, 0.8],
track_color: [0.5, 0.5, 0.5, 0.1],
corner_radius: 0.5, edge_padding: 2.0,
auto_dismiss_delay: 1.5, min_thumb_length: 30.0,
}
}
}
impl ScrollbarConfig {
pub fn always_visible() -> Self {
Self {
visibility: ScrollbarVisibility::Always,
..Default::default()
}
}
pub fn show_on_hover() -> Self {
Self {
visibility: ScrollbarVisibility::Hover,
..Default::default()
}
}
pub fn hidden() -> Self {
Self {
visibility: ScrollbarVisibility::Never,
..Default::default()
}
}
pub fn width(&self) -> f32 {
self.custom_width.unwrap_or_else(|| self.size.width())
}
}
#[derive(Debug, Clone, Copy)]
pub struct ScrollConfig {
pub bounce_enabled: bool,
pub bounce_spring: SpringConfig,
pub deceleration: f32,
pub velocity_threshold: f32,
pub max_overscroll: f32,
pub direction: ScrollDirection,
pub scrollbar: ScrollbarConfig,
}
impl Default for ScrollConfig {
fn default() -> Self {
Self {
bounce_enabled: true,
bounce_spring: SpringConfig::new(3000.0, 110.0, 1.0),
deceleration: 1500.0, velocity_threshold: 10.0, max_overscroll: 0.3, direction: ScrollDirection::Vertical,
scrollbar: ScrollbarConfig::default(),
}
}
}
impl ScrollConfig {
pub fn no_bounce() -> Self {
Self {
bounce_enabled: false,
..Default::default()
}
}
pub fn stiff_bounce() -> Self {
Self {
bounce_spring: SpringConfig::stiff(),
..Default::default()
}
}
pub fn gentle_bounce() -> Self {
Self {
bounce_spring: SpringConfig::gentle(),
..Default::default()
}
}
}
pub struct ScrollPhysics {
pub offset_y: f32,
pub velocity_y: f32,
pub offset_x: f32,
pub velocity_x: f32,
spring_y: Option<SpringId>,
spring_x: Option<SpringId>,
pub state: ScrollState,
pub content_height: f32,
pub viewport_height: f32,
pub content_width: f32,
pub viewport_width: f32,
pub config: ScrollConfig,
scheduler: Weak<Mutex<AnimationScheduler>>,
pub scrollbar_state: ScrollbarState,
pub scrollbar_opacity: f32,
scrollbar_target_opacity: f32,
pub area_hovered: bool,
pub idle_time: f32,
pub thumb_drag_start_y: f32,
pub thumb_drag_start_x: f32,
pub thumb_drag_start_scroll_y: f32,
pub thumb_drag_start_scroll_x: f32,
scrollbar_opacity_spring: Option<SpringId>,
last_scroll_time: Option<f64>,
}
impl Default for ScrollPhysics {
fn default() -> Self {
Self {
offset_y: 0.0,
velocity_y: 0.0,
offset_x: 0.0,
velocity_x: 0.0,
spring_y: None,
spring_x: None,
state: ScrollState::Idle,
content_height: 0.0,
viewport_height: 0.0,
content_width: 0.0,
viewport_width: 0.0,
config: ScrollConfig::default(),
scheduler: Weak::new(),
scrollbar_state: ScrollbarState::Idle,
scrollbar_opacity: 0.0,
scrollbar_target_opacity: 0.0,
area_hovered: false,
idle_time: 0.0,
thumb_drag_start_y: 0.0,
thumb_drag_start_x: 0.0,
thumb_drag_start_scroll_y: 0.0,
thumb_drag_start_scroll_x: 0.0,
scrollbar_opacity_spring: None,
last_scroll_time: None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ScrollbarHitResult {
None,
VerticalTrack,
VerticalThumb,
HorizontalTrack,
HorizontalThumb,
}
impl ScrollPhysics {
pub fn new(config: ScrollConfig) -> Self {
Self {
config,
..Default::default()
}
}
pub fn with_scheduler(
config: ScrollConfig,
scheduler: &Arc<Mutex<AnimationScheduler>>,
) -> Self {
Self {
config,
scheduler: Arc::downgrade(scheduler),
..Default::default()
}
}
pub fn set_scheduler(&mut self, scheduler: &Arc<Mutex<AnimationScheduler>>) {
self.scheduler = Arc::downgrade(scheduler);
}
pub fn min_offset_y(&self) -> f32 {
0.0
}
pub fn max_offset_y(&self) -> f32 {
let scrollable = self.content_height - self.viewport_height;
if scrollable > 0.0 {
-scrollable
} else {
0.0
}
}
pub fn min_offset_x(&self) -> f32 {
0.0
}
pub fn max_offset_x(&self) -> f32 {
let scrollable = self.content_width - self.viewport_width;
if scrollable > 0.0 {
-scrollable
} else {
0.0
}
}
pub fn is_overscrolling_y(&self) -> bool {
self.offset_y > self.min_offset_y() || self.offset_y < self.max_offset_y()
}
pub fn is_overscrolling_x(&self) -> bool {
self.offset_x > self.min_offset_x() || self.offset_x < self.max_offset_x()
}
pub fn is_overscrolling(&self) -> bool {
match self.config.direction {
ScrollDirection::Vertical => self.is_overscrolling_y(),
ScrollDirection::Horizontal => self.is_overscrolling_x(),
ScrollDirection::Both => self.is_overscrolling_y() || self.is_overscrolling_x(),
}
}
pub fn overscroll_amount_y(&self) -> f32 {
if self.offset_y > self.min_offset_y() {
self.offset_y - self.min_offset_y()
} else if self.offset_y < self.max_offset_y() {
self.offset_y - self.max_offset_y()
} else {
0.0
}
}
pub fn overscroll_amount_x(&self) -> f32 {
if self.offset_x > self.min_offset_x() {
self.offset_x - self.min_offset_x()
} else if self.offset_x < self.max_offset_x() {
self.offset_x - self.max_offset_x()
} else {
0.0
}
}
pub fn apply_scroll_delta(&mut self, delta_x: f32, delta_y: f32) {
if self.state == ScrollState::Bouncing {
return;
}
if let Some(new_state) = self.state.on_event(blinc_core::events::event_types::SCROLL) {
self.state = new_state;
}
let old_offset_y = self.offset_y;
if matches!(
self.config.direction,
ScrollDirection::Vertical | ScrollDirection::Both
) {
let overscroll = self.overscroll_amount_y();
let pushing_further =
(overscroll > 0.0 && delta_y > 0.0) || (overscroll < 0.0 && delta_y < 0.0);
let pulling_back =
(overscroll > 0.0 && delta_y < 0.0) || (overscroll < 0.0 && delta_y > 0.0);
if self.is_overscrolling_y() && self.config.bounce_enabled && pulling_back {
} else if self.is_overscrolling_y() && self.config.bounce_enabled && pushing_further {
let overscroll_amount = overscroll.abs();
let max_over = self.viewport_height * self.config.max_overscroll;
let stretch_ratio = (overscroll_amount / max_over).min(1.0);
let resistance = 0.55 - (stretch_ratio * 0.45);
self.offset_y += delta_y * resistance;
} else {
self.offset_y += delta_y;
}
if !self.config.bounce_enabled {
self.offset_y = self
.offset_y
.clamp(self.max_offset_y(), self.min_offset_y());
} else {
let max_over = self.viewport_height * self.config.max_overscroll;
self.offset_y = self
.offset_y
.clamp(self.max_offset_y() - max_over, max_over);
}
tracing::trace!(
"scroll_phys delta_y={:.1} offset: {:.1} -> {:.1}, bounds=({:.0}, {:.0}), content={:.0}, viewport={:.0}",
delta_y, old_offset_y, self.offset_y, self.max_offset_y(), self.min_offset_y(),
self.content_height, self.viewport_height
);
}
if matches!(
self.config.direction,
ScrollDirection::Horizontal | ScrollDirection::Both
) {
let overscroll = self.overscroll_amount_x();
let pushing_further =
(overscroll > 0.0 && delta_x > 0.0) || (overscroll < 0.0 && delta_x < 0.0);
let pulling_back =
(overscroll > 0.0 && delta_x < 0.0) || (overscroll < 0.0 && delta_x > 0.0);
if self.is_overscrolling_x() && self.config.bounce_enabled && pulling_back {
} else if self.is_overscrolling_x() && self.config.bounce_enabled && pushing_further {
let overscroll_amount = overscroll.abs();
let max_over = self.viewport_width * self.config.max_overscroll;
let stretch_ratio = (overscroll_amount / max_over).min(1.0);
let resistance = 0.55 - (stretch_ratio * 0.45);
self.offset_x += delta_x * resistance;
} else {
self.offset_x += delta_x;
}
if !self.config.bounce_enabled {
self.offset_x = self
.offset_x
.clamp(self.max_offset_x(), self.min_offset_x());
} else {
let max_over = self.viewport_width * self.config.max_overscroll;
self.offset_x = self
.offset_x
.clamp(self.max_offset_x() - max_over, max_over);
}
}
}
pub fn apply_touch_scroll_delta(&mut self, delta_x: f32, delta_y: f32, current_time: f64) {
if let Some(last_time) = self.last_scroll_time {
let dt_seconds = ((current_time - last_time) / 1000.0) as f32;
if dt_seconds > 0.0 && dt_seconds < 0.5 {
let alpha = 0.3; let instant_vx = delta_x / dt_seconds;
let instant_vy = delta_y / dt_seconds;
self.velocity_x = self.velocity_x * (1.0 - alpha) + instant_vx * alpha;
self.velocity_y = self.velocity_y * (1.0 - alpha) + instant_vy * alpha;
}
} else {
self.velocity_x = delta_x * 60.0;
self.velocity_y = delta_y * 60.0;
}
self.last_scroll_time = Some(current_time);
self.apply_scroll_delta(delta_x, delta_y);
}
pub fn on_scroll_end(&mut self) {
if let Some(new_state) = self
.state
.on_event(blinc_core::events::event_types::SCROLL_END)
{
self.state = new_state;
}
self.last_scroll_time = None;
if self.is_overscrolling() && self.config.bounce_enabled {
self.start_bounce();
return;
}
let has_velocity = self.velocity_x.abs() > self.config.velocity_threshold
|| self.velocity_y.abs() > self.config.velocity_threshold;
if has_velocity {
if let Some(new_state) = self
.state
.on_event(blinc_core::events::event_types::SCROLL_END)
{
self.state = new_state;
}
}
}
pub fn on_gesture_end(&mut self) {
if self.is_overscrolling() && self.config.bounce_enabled {
self.start_bounce();
}
}
fn cancel_springs(&mut self) {
if let Some(scheduler) = self.scheduler.upgrade() {
let scheduler = scheduler.lock().unwrap();
if let Some(id) = self.spring_y.take() {
scheduler.remove_spring(id);
}
if let Some(id) = self.spring_x.take() {
scheduler.remove_spring(id);
}
} else {
self.spring_y = None;
self.spring_x = None;
}
}
fn start_bounce(&mut self) {
if self.state == ScrollState::Bouncing {
return;
}
let Some(scheduler_arc) = self.scheduler.upgrade() else {
if self.is_overscrolling_y() {
self.offset_y = if self.offset_y > self.min_offset_y() {
self.min_offset_y()
} else {
self.max_offset_y()
};
}
if self.is_overscrolling_x() {
self.offset_x = if self.offset_x > self.min_offset_x() {
self.min_offset_x()
} else {
self.max_offset_x()
};
}
return;
};
let scheduler = scheduler_arc.lock().unwrap();
if self.is_overscrolling_y()
&& matches!(
self.config.direction,
ScrollDirection::Vertical | ScrollDirection::Both
)
{
let target = if self.offset_y > self.min_offset_y() {
self.min_offset_y()
} else {
self.max_offset_y()
};
let mut spring = Spring::new(self.config.bounce_spring, self.offset_y);
spring.set_target(target);
let spring_id = scheduler.add_spring(spring);
self.spring_y = Some(spring_id);
}
if self.is_overscrolling_x()
&& matches!(
self.config.direction,
ScrollDirection::Horizontal | ScrollDirection::Both
)
{
let target = if self.offset_x > self.min_offset_x() {
self.min_offset_x()
} else {
self.max_offset_x()
};
let mut spring = Spring::new(self.config.bounce_spring, self.offset_x);
spring.set_target(target);
let spring_id = scheduler.add_spring(spring);
self.spring_x = Some(spring_id);
}
drop(scheduler);
if let Some(new_state) = self.state.on_event(scroll_events::HIT_EDGE) {
self.state = new_state;
}
}
pub fn tick(&mut self, dt: f32) -> bool {
match self.state {
ScrollState::Idle => false,
ScrollState::Scrolling => {
true
}
ScrollState::Decelerating => {
let dx = self.velocity_x * dt;
let dy = self.velocity_y * dt;
let new_offset_y = self.offset_y + dy;
let new_offset_x = self.offset_x + dx;
let decel = self.config.deceleration * dt;
if self.velocity_x > 0.0 {
self.velocity_x = (self.velocity_x - decel).max(0.0);
} else if self.velocity_x < 0.0 {
self.velocity_x = (self.velocity_x + decel).min(0.0);
}
if self.velocity_y > 0.0 {
self.velocity_y = (self.velocity_y - decel).max(0.0);
} else if self.velocity_y < 0.0 {
self.velocity_y = (self.velocity_y + decel).min(0.0);
}
let hit_edge_y =
new_offset_y > self.min_offset_y() || new_offset_y < self.max_offset_y();
let hit_edge_x =
new_offset_x > self.min_offset_x() || new_offset_x < self.max_offset_x();
if hit_edge_y || hit_edge_x {
self.offset_y = new_offset_y.clamp(self.max_offset_y(), self.min_offset_y());
self.offset_x = new_offset_x.clamp(self.max_offset_x(), self.min_offset_x());
self.velocity_x = 0.0;
self.velocity_y = 0.0;
if let Some(new_state) = self.state.on_event(scroll_events::SETTLED) {
self.state = new_state;
}
return false;
}
self.offset_y = new_offset_y;
self.offset_x = new_offset_x;
let stopped = self.velocity_x.abs() < self.config.velocity_threshold
&& self.velocity_y.abs() < self.config.velocity_threshold;
if stopped {
self.velocity_x = 0.0;
self.velocity_y = 0.0;
if let Some(new_state) = self.state.on_event(scroll_events::SETTLED) {
self.state = new_state;
}
return false;
}
true }
ScrollState::Bouncing => {
let Some(scheduler_arc) = self.scheduler.upgrade() else {
self.offset_y = self
.offset_y
.clamp(self.max_offset_y(), self.min_offset_y());
self.offset_x = self
.offset_x
.clamp(self.max_offset_x(), self.min_offset_x());
self.state = ScrollState::Idle;
return false;
};
let scheduler = scheduler_arc.lock().unwrap();
let mut still_bouncing = false;
if let Some(spring_id) = self.spring_y {
if let Some(spring) = scheduler.get_spring(spring_id) {
self.offset_y = spring.value();
if spring.is_settled() {
self.offset_y = spring.target();
} else {
still_bouncing = true;
}
}
}
if let Some(spring_id) = self.spring_x {
if let Some(spring) = scheduler.get_spring(spring_id) {
self.offset_x = spring.value();
if spring.is_settled() {
self.offset_x = spring.target();
} else {
still_bouncing = true;
}
}
}
drop(scheduler);
if !still_bouncing {
self.cancel_springs();
if let Some(new_state) = self.state.on_event(scroll_events::SETTLED) {
self.state = new_state;
}
return false;
}
true
}
}
}
pub fn is_animating(&self) -> bool {
self.state.is_active()
}
pub fn set_direction(&mut self, direction: ScrollDirection) {
self.config.direction = direction;
self.offset_x = 0.0;
self.offset_y = 0.0;
self.velocity_x = 0.0;
self.velocity_y = 0.0;
self.cancel_springs();
self.state = ScrollState::Idle;
}
pub fn scroll_to_animated(&mut self, target_x: f32, target_y: f32) {
self.cancel_springs();
let Some(scheduler_arc) = self.scheduler.upgrade() else {
self.offset_x = target_x;
self.offset_y = target_y;
return;
};
let mut scheduler = scheduler_arc.lock().unwrap();
let scroll_spring_config = SpringConfig::new(400.0, 30.0, 1.0);
if matches!(
self.config.direction,
ScrollDirection::Vertical | ScrollDirection::Both
) && (self.offset_y - target_y).abs() > 0.5
{
let mut spring = Spring::new(scroll_spring_config, self.offset_y);
spring.set_target(target_y);
let spring_id = scheduler.add_spring(spring);
self.spring_y = Some(spring_id);
} else if matches!(
self.config.direction,
ScrollDirection::Vertical | ScrollDirection::Both
) {
self.offset_y = target_y;
}
if matches!(
self.config.direction,
ScrollDirection::Horizontal | ScrollDirection::Both
) && (self.offset_x - target_x).abs() > 0.5
{
let mut spring = Spring::new(scroll_spring_config, self.offset_x);
spring.set_target(target_x);
let spring_id = scheduler.add_spring(spring);
self.spring_x = Some(spring_id);
} else if matches!(
self.config.direction,
ScrollDirection::Horizontal | ScrollDirection::Both
) {
self.offset_x = target_x;
}
drop(scheduler);
if self.spring_x.is_some() || self.spring_y.is_some() {
self.state = ScrollState::Bouncing;
}
}
pub fn on_area_hover_enter(&mut self) {
self.area_hovered = true;
self.update_scrollbar_visibility();
}
pub fn on_area_hover_leave(&mut self) {
self.area_hovered = false;
if self.scrollbar_state != ScrollbarState::Dragging {
self.update_scrollbar_visibility();
}
}
pub fn on_scrollbar_track_hover(&mut self) {
if self.scrollbar_state != ScrollbarState::Dragging {
self.scrollbar_state = ScrollbarState::TrackHovered;
self.update_scrollbar_visibility();
}
}
pub fn on_scrollbar_thumb_hover(&mut self) {
if self.scrollbar_state != ScrollbarState::Dragging {
self.scrollbar_state = ScrollbarState::ThumbHovered;
self.update_scrollbar_visibility();
}
}
pub fn on_scrollbar_hover_leave(&mut self) {
if self.scrollbar_state != ScrollbarState::Dragging {
self.scrollbar_state = if self.area_hovered {
ScrollbarState::Scrolling
} else {
ScrollbarState::Idle
};
self.update_scrollbar_visibility();
}
}
pub fn on_scrollbar_drag_start(&mut self, mouse_x: f32, mouse_y: f32) {
self.scrollbar_state = ScrollbarState::Dragging;
self.thumb_drag_start_x = mouse_x;
self.thumb_drag_start_y = mouse_y;
self.thumb_drag_start_scroll_x = self.offset_x;
self.thumb_drag_start_scroll_y = self.offset_y;
self.update_scrollbar_visibility();
}
pub fn on_scrollbar_drag(&mut self, mouse_x: f32, mouse_y: f32) -> (f32, f32) {
let delta_x = mouse_x - self.thumb_drag_start_x;
let delta_y = mouse_y - self.thumb_drag_start_y;
let (thumb_travel_x, scroll_range_x) = self.thumb_travel_x();
let (thumb_travel_y, scroll_range_y) = self.thumb_travel_y();
let scroll_delta_x = if thumb_travel_x > 0.0 {
(delta_x / thumb_travel_x) * scroll_range_x
} else {
0.0
};
let scroll_delta_y = if thumb_travel_y > 0.0 {
(delta_y / thumb_travel_y) * scroll_range_y
} else {
0.0
};
let new_x = (self.thumb_drag_start_scroll_x - scroll_delta_x)
.clamp(self.max_offset_x(), self.min_offset_x());
let new_y = (self.thumb_drag_start_scroll_y - scroll_delta_y)
.clamp(self.max_offset_y(), self.min_offset_y());
(new_x, new_y)
}
pub fn on_scrollbar_drag_end(&mut self) {
self.scrollbar_state = if self.area_hovered {
ScrollbarState::Scrolling
} else {
ScrollbarState::FadingOut
};
self.idle_time = 0.0;
self.update_scrollbar_visibility();
}
pub fn on_scroll_activity(&mut self) {
self.idle_time = 0.0;
if self.scrollbar_state == ScrollbarState::Idle
|| self.scrollbar_state == ScrollbarState::FadingOut
{
self.scrollbar_state = ScrollbarState::Scrolling;
}
self.update_scrollbar_visibility();
}
fn update_scrollbar_visibility(&mut self) {
let target = match self.config.scrollbar.visibility {
ScrollbarVisibility::Always => 1.0,
ScrollbarVisibility::Never => 0.0,
ScrollbarVisibility::Hover => {
if self.area_hovered || self.scrollbar_state.is_interacting() {
self.scrollbar_state.opacity()
} else {
0.0
}
}
ScrollbarVisibility::Auto => {
if self.scrollbar_state == ScrollbarState::Idle {
0.0
} else {
self.scrollbar_state.opacity()
}
}
};
self.scrollbar_target_opacity = target;
if let Some(scheduler_arc) = self.scheduler.upgrade() {
let mut scheduler = scheduler_arc.lock().unwrap();
if let Some(spring_id) = self.scrollbar_opacity_spring.take() {
scheduler.remove_spring(spring_id);
}
if (self.scrollbar_opacity - target).abs() > 0.01 {
let spring_config = SpringConfig::new(300.0, 25.0, 1.0); let mut spring = Spring::new(spring_config, self.scrollbar_opacity);
spring.set_target(target);
let spring_id = scheduler.add_spring(spring);
self.scrollbar_opacity_spring = Some(spring_id);
} else {
self.scrollbar_opacity = target;
}
} else {
self.scrollbar_opacity = target;
}
}
pub fn tick_scrollbar(&mut self, dt: f32) -> bool {
let mut animating = false;
if (self.state == ScrollState::Scrolling || self.state == ScrollState::Bouncing)
&& (self.scrollbar_state == ScrollbarState::Idle
|| self.scrollbar_state == ScrollbarState::FadingOut)
{
self.scrollbar_state = ScrollbarState::Scrolling;
self.idle_time = 0.0;
self.update_scrollbar_visibility();
}
if self.scrollbar_state == ScrollbarState::Scrolling
|| self.scrollbar_state == ScrollbarState::FadingOut
{
self.idle_time += dt;
let should_fade = match self.config.scrollbar.visibility {
ScrollbarVisibility::Auto => {
self.idle_time >= self.config.scrollbar.auto_dismiss_delay
&& !self.scrollbar_state.is_interacting()
}
ScrollbarVisibility::Hover => {
self.idle_time >= self.config.scrollbar.auto_dismiss_delay
&& !self.area_hovered
&& !self.scrollbar_state.is_interacting()
}
_ => false,
};
if should_fade && self.scrollbar_state != ScrollbarState::FadingOut {
self.scrollbar_state = ScrollbarState::FadingOut;
self.update_scrollbar_visibility();
}
if self.scrollbar_state == ScrollbarState::FadingOut && self.scrollbar_opacity < 0.01 {
self.scrollbar_state = ScrollbarState::Idle;
self.scrollbar_opacity = 0.0;
}
}
if let Some(scheduler_arc) = self.scheduler.upgrade() {
let scheduler = scheduler_arc.lock().unwrap();
if let Some(spring_id) = self.scrollbar_opacity_spring {
if let Some(spring) = scheduler.get_spring(spring_id) {
self.scrollbar_opacity = spring.value();
if !spring.is_settled() {
animating = true;
} else {
self.scrollbar_opacity = spring.target();
}
}
}
}
animating
}
pub fn thumb_dimensions_y(&self) -> (f32, f32) {
let viewport = self.viewport_height;
let content = self.content_height.max(viewport);
let scroll_ratio = viewport / content;
let thumb_height = (scroll_ratio * viewport)
.max(self.config.scrollbar.min_thumb_length)
.min(viewport - self.config.scrollbar.edge_padding * 2.0);
let max_scroll = (content - viewport).max(0.0);
let scroll_progress = if max_scroll > 0.0 {
(-self.offset_y / max_scroll).clamp(0.0, 1.0)
} else {
0.0
};
let track_height = viewport - self.config.scrollbar.edge_padding * 2.0;
let max_thumb_travel = track_height - thumb_height;
let thumb_y = self.config.scrollbar.edge_padding + (scroll_progress * max_thumb_travel);
(thumb_height, thumb_y)
}
pub fn thumb_dimensions_x(&self) -> (f32, f32) {
let viewport = self.viewport_width;
let content = self.content_width.max(viewport);
let scroll_ratio = viewport / content;
let thumb_width = (scroll_ratio * viewport)
.max(self.config.scrollbar.min_thumb_length)
.min(viewport - self.config.scrollbar.edge_padding * 2.0);
let max_scroll = (content - viewport).max(0.0);
let scroll_progress = if max_scroll > 0.0 {
(-self.offset_x / max_scroll).clamp(0.0, 1.0)
} else {
0.0
};
let track_width = viewport - self.config.scrollbar.edge_padding * 2.0;
let max_thumb_travel = track_width - thumb_width;
let thumb_x = self.config.scrollbar.edge_padding + (scroll_progress * max_thumb_travel);
(thumb_width, thumb_x)
}
fn thumb_travel_y(&self) -> (f32, f32) {
let viewport = self.viewport_height;
let content = self.content_height.max(viewport);
let (thumb_height, _) = self.thumb_dimensions_y();
let track_height = viewport - self.config.scrollbar.edge_padding * 2.0;
let thumb_travel = track_height - thumb_height;
let scroll_range = (content - viewport).max(0.0);
(thumb_travel, scroll_range)
}
fn thumb_travel_x(&self) -> (f32, f32) {
let viewport = self.viewport_width;
let content = self.content_width.max(viewport);
let (thumb_width, _) = self.thumb_dimensions_x();
let track_width = viewport - self.config.scrollbar.edge_padding * 2.0;
let thumb_travel = track_width - thumb_width;
let scroll_range = (content - viewport).max(0.0);
(thumb_travel, scroll_range)
}
pub fn can_scroll_y(&self) -> bool {
self.content_height > self.viewport_height
}
pub fn can_scroll_x(&self) -> bool {
self.content_width > self.viewport_width
}
pub fn hit_test_scrollbar(&self, local_x: f32, local_y: f32) -> ScrollbarHitResult {
let config = &self.config.scrollbar;
let scrollbar_width = config.width();
let edge_padding = config.edge_padding;
if self.can_scroll_y()
&& matches!(
self.config.direction,
ScrollDirection::Vertical | ScrollDirection::Both
)
{
let track_x = self.viewport_width - scrollbar_width - edge_padding;
let track_y = edge_padding;
let track_height = self.viewport_height - edge_padding * 2.0;
if local_x >= track_x
&& local_x <= track_x + scrollbar_width
&& local_y >= track_y
&& local_y <= track_y + track_height
{
let (thumb_height, thumb_y) = self.thumb_dimensions_y();
if local_y >= thumb_y && local_y <= thumb_y + thumb_height {
return ScrollbarHitResult::VerticalThumb;
}
return ScrollbarHitResult::VerticalTrack;
}
}
if self.can_scroll_x()
&& matches!(
self.config.direction,
ScrollDirection::Horizontal | ScrollDirection::Both
)
{
let track_x = edge_padding;
let track_y = self.viewport_height - scrollbar_width - edge_padding;
let track_width = self.viewport_width - edge_padding * 2.0;
if local_x >= track_x
&& local_x <= track_x + track_width
&& local_y >= track_y
&& local_y <= track_y + scrollbar_width
{
let (thumb_width, thumb_x) = self.thumb_dimensions_x();
if local_x >= thumb_x && local_x <= thumb_x + thumb_width {
return ScrollbarHitResult::HorizontalThumb;
}
return ScrollbarHitResult::HorizontalTrack;
}
}
ScrollbarHitResult::None
}
pub fn on_scrollbar_pointer_down(&mut self, local_x: f32, local_y: f32) -> bool {
let hit = self.hit_test_scrollbar(local_x, local_y);
match hit {
ScrollbarHitResult::VerticalThumb | ScrollbarHitResult::HorizontalThumb => {
self.on_scrollbar_drag_start(local_x, local_y);
true
}
ScrollbarHitResult::VerticalTrack => {
let (thumb_height, _) = self.thumb_dimensions_y();
let track_height = self.viewport_height - self.config.scrollbar.edge_padding * 2.0;
let click_ratio =
(local_y - self.config.scrollbar.edge_padding - thumb_height / 2.0)
/ (track_height - thumb_height);
let click_ratio = click_ratio.clamp(0.0, 1.0);
let max_scroll = (self.content_height - self.viewport_height).max(0.0);
self.offset_y = -click_ratio * max_scroll;
self.on_scroll_activity();
true
}
ScrollbarHitResult::HorizontalTrack => {
let (thumb_width, _) = self.thumb_dimensions_x();
let track_width = self.viewport_width - self.config.scrollbar.edge_padding * 2.0;
let click_ratio =
(local_x - self.config.scrollbar.edge_padding - thumb_width / 2.0)
/ (track_width - thumb_width);
let click_ratio = click_ratio.clamp(0.0, 1.0);
let max_scroll = (self.content_width - self.viewport_width).max(0.0);
self.offset_x = -click_ratio * max_scroll;
self.on_scroll_activity();
true
}
ScrollbarHitResult::None => false,
}
}
pub fn on_scrollbar_pointer_move(&mut self, local_x: f32, local_y: f32) -> Option<(f32, f32)> {
if self.scrollbar_state == ScrollbarState::Dragging {
let (new_x, new_y) = self.on_scrollbar_drag(local_x, local_y);
self.offset_x = new_x;
self.offset_y = new_y;
Some((new_x, new_y))
} else {
let hit = self.hit_test_scrollbar(local_x, local_y);
match hit {
ScrollbarHitResult::VerticalThumb | ScrollbarHitResult::HorizontalThumb => {
self.on_scrollbar_thumb_hover();
}
ScrollbarHitResult::VerticalTrack | ScrollbarHitResult::HorizontalTrack => {
self.on_scrollbar_track_hover();
}
ScrollbarHitResult::None => {
if self.scrollbar_state == ScrollbarState::ThumbHovered
|| self.scrollbar_state == ScrollbarState::TrackHovered
{
self.on_scrollbar_hover_leave();
}
}
}
None
}
}
pub fn on_scrollbar_pointer_up(&mut self) {
if self.scrollbar_state == ScrollbarState::Dragging {
self.on_scrollbar_drag_end();
}
}
pub fn scrollbar_render_info(&self) -> ScrollbarRenderInfo {
let (thumb_height, thumb_y) = self.thumb_dimensions_y();
let (thumb_width, thumb_x) = self.thumb_dimensions_x();
ScrollbarRenderInfo {
state: self.scrollbar_state,
opacity: self.scrollbar_opacity,
config: self.config.scrollbar,
show_vertical: self.can_scroll_y()
&& matches!(
self.config.direction,
ScrollDirection::Vertical | ScrollDirection::Both
),
vertical_thumb_height: thumb_height,
vertical_thumb_y: thumb_y,
show_horizontal: self.can_scroll_x()
&& matches!(
self.config.direction,
ScrollDirection::Horizontal | ScrollDirection::Both
),
horizontal_thumb_width: thumb_width,
horizontal_thumb_x: thumb_x,
}
}
}
pub type SharedScrollPhysics = Arc<Mutex<ScrollPhysics>>;
#[derive(Debug, Clone, Copy)]
pub struct ScrollbarRenderInfo {
pub state: ScrollbarState,
pub opacity: f32,
pub config: ScrollbarConfig,
pub show_vertical: bool,
pub vertical_thumb_height: f32,
pub vertical_thumb_y: f32,
pub show_horizontal: bool,
pub horizontal_thumb_width: f32,
pub horizontal_thumb_x: f32,
}
impl Default for ScrollbarRenderInfo {
fn default() -> Self {
Self {
state: ScrollbarState::Idle,
opacity: 0.0,
config: ScrollbarConfig::default(),
show_vertical: false,
vertical_thumb_height: 30.0,
vertical_thumb_y: 0.0,
show_horizontal: false,
horizontal_thumb_width: 30.0,
horizontal_thumb_x: 0.0,
}
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct ScrollRenderInfo {
pub offset_x: f32,
pub offset_y: f32,
pub viewport_width: f32,
pub viewport_height: f32,
pub content_width: f32,
pub content_height: f32,
pub is_animating: bool,
pub direction: ScrollDirection,
}
pub struct Scroll {
inner: Div,
content: Option<Box<dyn ElementBuilder>>,
physics: SharedScrollPhysics,
handlers: EventHandlers,
scroll_ref: Option<ScrollRef>,
}
impl Deref for Scroll {
type Target = Div;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl DerefMut for Scroll {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.inner
}
}
impl Default for Scroll {
fn default() -> Self {
Self::new()
}
}
impl Scroll {
pub fn new() -> Self {
let physics = Arc::new(Mutex::new(ScrollPhysics::default()));
let handlers = Self::create_internal_handlers(Arc::clone(&physics));
Self {
inner: Div::new()
.overflow_scroll_style_only()
.items_start()
.justify_start()
.content_start(),
content: None,
physics,
handlers,
scroll_ref: None,
}
}
pub fn with_config(config: ScrollConfig) -> Self {
let physics = Arc::new(Mutex::new(ScrollPhysics::new(config)));
let handlers = Self::create_internal_handlers(Arc::clone(&physics));
Self {
inner: Div::new()
.overflow_scroll_style_only()
.items_start()
.justify_start()
.content_start(),
content: None,
physics,
handlers,
scroll_ref: None,
}
}
pub fn with_physics(physics: SharedScrollPhysics) -> Self {
let handlers = Self::create_internal_handlers(Arc::clone(&physics));
Self {
inner: Div::new()
.overflow_scroll_style_only()
.items_start()
.justify_start()
.content_start(),
content: None,
physics,
handlers,
scroll_ref: None,
}
}
pub fn create_internal_handlers(physics: SharedScrollPhysics) -> EventHandlers {
let mut handlers = EventHandlers::new();
handlers.on_scroll({
let physics = Arc::clone(&physics);
move |ctx| {
let mut p = physics.lock().unwrap();
if let Some(time) = ctx.scroll_time {
p.apply_touch_scroll_delta(ctx.scroll_delta_x, ctx.scroll_delta_y, time);
} else {
p.apply_scroll_delta(ctx.scroll_delta_x, ctx.scroll_delta_y);
}
p.on_scroll_activity();
}
});
handlers.on_hover_enter({
let physics = Arc::clone(&physics);
move |_ctx| {
physics.lock().unwrap().on_area_hover_enter();
}
});
handlers.on_hover_leave({
let physics = Arc::clone(&physics);
move |_ctx| {
physics.lock().unwrap().on_area_hover_leave();
}
});
handlers.on_mouse_down({
let physics = Arc::clone(&physics);
move |ctx| {
let mut p = physics.lock().unwrap();
p.on_scrollbar_pointer_down(ctx.local_x, ctx.local_y);
}
});
handlers.on_drag({
let physics = Arc::clone(&physics);
move |ctx| {
let mut p = physics.lock().unwrap();
if p.scrollbar_state == ScrollbarState::Dragging {
p.on_scrollbar_pointer_move(ctx.local_x, ctx.local_y);
}
}
});
handlers.on_drag_end({
let physics = Arc::clone(&physics);
move |_ctx| {
let mut p = physics.lock().unwrap();
p.on_scrollbar_pointer_up();
}
});
handlers
}
pub fn physics(&self) -> SharedScrollPhysics {
Arc::clone(&self.physics)
}
pub fn offset_y(&self) -> f32 {
self.physics.lock().unwrap().offset_y
}
pub fn state(&self) -> ScrollState {
self.physics.lock().unwrap().state
}
pub fn bounce(self, enabled: bool) -> Self {
self.physics.lock().unwrap().config.bounce_enabled = enabled;
self
}
pub fn no_bounce(self) -> Self {
self.bounce(false)
}
pub fn deceleration(self, decel: f32) -> Self {
self.physics.lock().unwrap().config.deceleration = decel.max(0.0);
self
}
pub fn spring(self, config: SpringConfig) -> Self {
self.physics.lock().unwrap().config.bounce_spring = config;
self
}
pub fn direction(mut self, direction: ScrollDirection) -> Self {
self.physics.lock().unwrap().config.direction = direction;
use taffy::Overflow;
match direction {
ScrollDirection::Vertical => {
self.inner = std::mem::take(&mut self.inner)
.overflow_x(Overflow::Clip)
.overflow_y(Overflow::Scroll);
}
ScrollDirection::Horizontal => {
self.inner = std::mem::take(&mut self.inner)
.overflow_x(Overflow::Scroll)
.overflow_y(Overflow::Clip);
}
ScrollDirection::Both => {
self.inner = std::mem::take(&mut self.inner).overflow_scroll();
}
}
self
}
pub fn vertical(self) -> Self {
self.direction(ScrollDirection::Vertical)
}
pub fn horizontal(self) -> Self {
self.direction(ScrollDirection::Horizontal)
}
pub fn both_directions(self) -> Self {
self.direction(ScrollDirection::Both)
}
pub fn scrollbar_visibility(self, visibility: ScrollbarVisibility) -> Self {
let mut physics = self.physics.lock().unwrap();
physics.config.scrollbar.visibility = visibility;
physics.update_scrollbar_visibility();
drop(physics);
self
}
pub fn scrollbar_always(self) -> Self {
self.scrollbar_visibility(ScrollbarVisibility::Always)
}
pub fn scrollbar_on_hover(self) -> Self {
self.scrollbar_visibility(ScrollbarVisibility::Hover)
}
pub fn scrollbar_auto(self) -> Self {
self.scrollbar_visibility(ScrollbarVisibility::Auto)
}
pub fn scrollbar_hidden(self) -> Self {
self.scrollbar_visibility(ScrollbarVisibility::Never)
}
pub fn scrollbar_size(self, size: ScrollbarSize) -> Self {
self.physics.lock().unwrap().config.scrollbar.size = size;
self
}
pub fn scrollbar_thin(self) -> Self {
self.scrollbar_size(ScrollbarSize::Thin)
}
pub fn scrollbar_wide(self) -> Self {
self.scrollbar_size(ScrollbarSize::Wide)
}
pub fn scrollbar_width(self, width: f32) -> Self {
self.physics.lock().unwrap().config.scrollbar.custom_width = Some(width);
self
}
pub fn scrollbar_thumb_color(self, r: f32, g: f32, b: f32, a: f32) -> Self {
self.physics.lock().unwrap().config.scrollbar.thumb_color = [r, g, b, a];
self
}
pub fn scrollbar_track_color(self, r: f32, g: f32, b: f32, a: f32) -> Self {
self.physics.lock().unwrap().config.scrollbar.track_color = [r, g, b, a];
self
}
pub fn scrollbar_dismiss_delay(self, seconds: f32) -> Self {
self.physics
.lock()
.unwrap()
.config
.scrollbar
.auto_dismiss_delay = seconds;
self
}
pub fn scrollbar_info(&self) -> ScrollbarRenderInfo {
self.physics.lock().unwrap().scrollbar_render_info()
}
pub fn id(mut self, id: impl Into<String>) -> Self {
self.inner = std::mem::take(&mut self.inner).id(id);
self
}
pub fn bind(mut self, scroll_ref: &ScrollRef) -> Self {
self.scroll_ref = Some(scroll_ref.clone());
self
}
pub fn scroll_ref(&self) -> Option<&ScrollRef> {
self.scroll_ref.as_ref()
}
pub fn content(mut self, child: impl ElementBuilder + 'static) -> Self {
self.content = Some(Box::new(child));
self
}
pub fn set_content_height(&self, height: f32) {
self.physics.lock().unwrap().content_height = height;
}
pub fn apply_scroll_delta(&self, delta_x: f32, delta_y: f32) {
self.physics
.lock()
.unwrap()
.apply_scroll_delta(delta_x, delta_y);
}
pub fn on_scroll_gesture_end(&self) {
self.physics.lock().unwrap().on_scroll_end();
}
pub fn tick(&self, dt: f32) -> bool {
self.physics.lock().unwrap().tick(dt)
}
pub fn w(mut self, px: f32) -> Self {
self.inner = std::mem::take(&mut self.inner).w(px);
self.physics.lock().unwrap().viewport_width = px;
self
}
pub fn h(mut self, px: f32) -> Self {
self.inner = std::mem::take(&mut self.inner).h(px);
self.physics.lock().unwrap().viewport_height = px;
self
}
pub fn size(mut self, w: f32, h: f32) -> Self {
self.inner = std::mem::take(&mut self.inner).size(w, h);
{
let mut physics = self.physics.lock().unwrap();
physics.viewport_width = w;
physics.viewport_height = h;
}
self
}
pub fn w_full(mut self) -> Self {
self.inner = std::mem::take(&mut self.inner).w_full();
self
}
pub fn h_full(mut self) -> Self {
self.inner = std::mem::take(&mut self.inner).h_full();
self
}
pub fn w_fit(mut self) -> Self {
self.inner = std::mem::take(&mut self.inner).w_fit();
self
}
pub fn h_fit(mut self) -> Self {
self.inner = std::mem::take(&mut self.inner).h_fit();
self
}
pub fn p(mut self, px: f32) -> Self {
self.inner = std::mem::take(&mut self.inner).p(px);
self
}
pub fn px(mut self, px: f32) -> Self {
self.inner = std::mem::take(&mut self.inner).px(px);
self
}
pub fn py(mut self, px: f32) -> Self {
self.inner = std::mem::take(&mut self.inner).py(px);
self
}
pub fn m(mut self, px: f32) -> Self {
self.inner = std::mem::take(&mut self.inner).m(px);
self
}
pub fn mx(mut self, px: f32) -> Self {
self.inner = std::mem::take(&mut self.inner).mx(px);
self
}
pub fn my(mut self, px: f32) -> Self {
self.inner = std::mem::take(&mut self.inner).my(px);
self
}
pub fn gap(mut self, px: f32) -> Self {
self.inner = std::mem::take(&mut self.inner).gap(px);
self
}
pub fn flex_row(mut self) -> Self {
self.inner = std::mem::take(&mut self.inner).flex_row();
self
}
pub fn flex_col(mut self) -> Self {
self.inner = std::mem::take(&mut self.inner).flex_col();
self
}
pub fn flex_grow(mut self) -> Self {
self.inner = std::mem::take(&mut self.inner).flex_grow();
self
}
pub fn items_center(mut self) -> Self {
self.inner = std::mem::take(&mut self.inner).items_center();
self
}
pub fn items_start(mut self) -> Self {
self.inner = std::mem::take(&mut self.inner).items_start();
self
}
pub fn items_end(mut self) -> Self {
self.inner = std::mem::take(&mut self.inner).items_end();
self
}
pub fn justify_center(mut self) -> Self {
self.inner = std::mem::take(&mut self.inner).justify_center();
self
}
pub fn justify_start(mut self) -> Self {
self.inner = std::mem::take(&mut self.inner).justify_start();
self
}
pub fn justify_end(mut self) -> Self {
self.inner = std::mem::take(&mut self.inner).justify_end();
self
}
pub fn justify_between(mut self) -> Self {
self.inner = std::mem::take(&mut self.inner).justify_between();
self
}
pub fn bg(mut self, color: impl Into<Brush>) -> Self {
self.inner = std::mem::take(&mut self.inner).background(color);
self
}
pub fn rounded(mut self, radius: f32) -> Self {
self.inner = std::mem::take(&mut self.inner).rounded(radius);
self
}
pub fn border(mut self, width: f32, color: blinc_core::Color) -> Self {
self.inner = std::mem::take(&mut self.inner).border(width, color);
self
}
pub fn border_color(mut self, color: blinc_core::Color) -> Self {
self.inner = std::mem::take(&mut self.inner).border_color(color);
self
}
pub fn border_width(mut self, width: f32) -> Self {
self.inner = std::mem::take(&mut self.inner).border_width(width);
self
}
pub fn shadow(mut self, shadow: Shadow) -> Self {
self.inner = std::mem::take(&mut self.inner).shadow(shadow);
self
}
pub fn shadow_sm(mut self) -> Self {
self.inner = std::mem::take(&mut self.inner).shadow_sm();
self
}
pub fn shadow_md(mut self) -> Self {
self.inner = std::mem::take(&mut self.inner).shadow_md();
self
}
pub fn shadow_lg(mut self) -> Self {
self.inner = std::mem::take(&mut self.inner).shadow_lg();
self
}
pub fn transform(mut self, transform: blinc_core::Transform) -> Self {
self.inner = std::mem::take(&mut self.inner).transform(transform);
self
}
pub fn opacity(mut self, opacity: f32) -> Self {
self.inner = std::mem::take(&mut self.inner).opacity(opacity);
self
}
pub fn overflow_clip(mut self) -> Self {
self.inner = std::mem::take(&mut self.inner).overflow_clip();
self
}
pub fn overflow_visible(mut self) -> Self {
self.inner = std::mem::take(&mut self.inner).overflow_visible();
self
}
pub fn overflow_fade(mut self, distance: f32) -> Self {
self.inner = std::mem::take(&mut self.inner).overflow_fade(distance);
self
}
pub fn overflow_fade_x(mut self, distance: f32) -> Self {
self.inner = std::mem::take(&mut self.inner).overflow_fade_x(distance);
self
}
pub fn overflow_fade_y(mut self, distance: f32) -> Self {
self.inner = std::mem::take(&mut self.inner).overflow_fade_y(distance);
self
}
pub fn overflow_fade_edges(mut self, top: f32, right: f32, bottom: f32, left: f32) -> Self {
self.inner = std::mem::take(&mut self.inner).overflow_fade_edges(top, right, bottom, left);
self
}
pub fn child(self, child: impl ElementBuilder + 'static) -> Self {
self.content(child)
}
pub fn on_scroll<F>(mut self, handler: F) -> Self
where
F: Fn(&EventContext) + Send + Sync + 'static,
{
self.handlers.on_scroll(handler);
self
}
pub fn on_click<F>(mut self, handler: F) -> Self
where
F: Fn(&EventContext) + Send + Sync + 'static,
{
self.inner = std::mem::take(&mut self.inner).on_click(handler);
self
}
pub fn on_hover_enter<F>(mut self, handler: F) -> Self
where
F: Fn(&EventContext) + Send + Sync + 'static,
{
self.inner = std::mem::take(&mut self.inner).on_hover_enter(handler);
self
}
pub fn on_hover_leave<F>(mut self, handler: F) -> Self
where
F: Fn(&EventContext) + Send + Sync + 'static,
{
self.inner = std::mem::take(&mut self.inner).on_hover_leave(handler);
self
}
pub fn on_mouse_down<F>(mut self, handler: F) -> Self
where
F: Fn(&EventContext) + Send + Sync + 'static,
{
self.inner = std::mem::take(&mut self.inner).on_mouse_down(handler);
self
}
pub fn on_mouse_up<F>(mut self, handler: F) -> Self
where
F: Fn(&EventContext) + Send + Sync + 'static,
{
self.inner = std::mem::take(&mut self.inner).on_mouse_up(handler);
self
}
pub fn on_focus<F>(mut self, handler: F) -> Self
where
F: Fn(&EventContext) + Send + Sync + 'static,
{
self.inner = std::mem::take(&mut self.inner).on_focus(handler);
self
}
pub fn on_blur<F>(mut self, handler: F) -> Self
where
F: Fn(&EventContext) + Send + Sync + 'static,
{
self.inner = std::mem::take(&mut self.inner).on_blur(handler);
self
}
pub fn on_key_down<F>(mut self, handler: F) -> Self
where
F: Fn(&EventContext) + Send + Sync + 'static,
{
self.inner = std::mem::take(&mut self.inner).on_key_down(handler);
self
}
pub fn on_key_up<F>(mut self, handler: F) -> Self
where
F: Fn(&EventContext) + Send + Sync + 'static,
{
self.inner = std::mem::take(&mut self.inner).on_key_up(handler);
self
}
}
impl ElementBuilder for Scroll {
fn build(&self, tree: &mut LayoutTree) -> LayoutNodeId {
let viewport_id = self.inner.build(tree);
if let Some(ref child) = self.content {
let child_id = child.build(tree);
tree.add_child(viewport_id, child_id);
}
viewport_id
}
fn render_props(&self) -> RenderProps {
let mut props = self.inner.render_props();
props.clips_content = true;
props
}
fn children_builders(&self) -> &[Box<dyn ElementBuilder>] {
if let Some(ref child) = self.content {
std::slice::from_ref(child)
} else {
&[]
}
}
fn element_type_id(&self) -> ElementTypeId {
ElementTypeId::Div
}
fn semantic_type_name(&self) -> Option<&'static str> {
Some("scroll")
}
fn event_handlers(&self) -> Option<&EventHandlers> {
if self.handlers.is_empty() {
None
} else {
Some(&self.handlers)
}
}
fn scroll_info(&self) -> Option<ScrollRenderInfo> {
let physics = self.physics.lock().unwrap();
Some(ScrollRenderInfo {
offset_x: physics.offset_x,
offset_y: physics.offset_y,
viewport_width: physics.viewport_width,
viewport_height: physics.viewport_height,
content_width: physics.content_width,
content_height: physics.content_height,
is_animating: physics.is_animating(),
direction: physics.config.direction,
})
}
fn scroll_physics(&self) -> Option<SharedScrollPhysics> {
Some(Arc::clone(&self.physics))
}
fn layout_style(&self) -> Option<&taffy::Style> {
self.inner.layout_style()
}
fn element_id(&self) -> Option<&str> {
self.inner.element_id()
}
fn bound_scroll_ref(&self) -> Option<&ScrollRef> {
self.scroll_ref.as_ref()
}
}
pub fn scroll() -> Scroll {
Scroll::new()
}
pub fn scroll_no_bounce() -> Scroll {
Scroll::with_config(ScrollConfig::no_bounce())
}
#[cfg(test)]
#[allow(clippy::field_reassign_with_default)]
mod tests {
use super::*;
#[test]
fn test_scroll_physics_basic() {
let mut physics = ScrollPhysics::default();
physics.viewport_height = 400.0;
physics.content_height = 1000.0;
assert_eq!(physics.min_offset_y(), 0.0);
assert_eq!(physics.max_offset_y(), -600.0);
physics.apply_scroll_delta(0.0, -50.0);
assert_eq!(physics.offset_y, -50.0);
assert_eq!(physics.state, ScrollState::Scrolling);
}
#[test]
fn test_scroll_physics_overscroll() {
let mut physics = ScrollPhysics::default();
physics.viewport_height = 400.0;
physics.content_height = 1000.0;
physics.apply_scroll_delta(0.0, 50.0);
assert!(physics.is_overscrolling_y());
assert!(physics.overscroll_amount_y() > 0.0);
}
#[test]
fn test_scroll_physics_bounce() {
let scheduler = Arc::new(Mutex::new(AnimationScheduler::new()));
let mut physics = ScrollPhysics::with_scheduler(ScrollConfig::default(), &scheduler);
physics.viewport_height = 400.0;
physics.content_height = 1000.0;
physics.offset_y = 50.0;
physics.state = ScrollState::Scrolling;
physics.on_scroll_end();
assert_eq!(physics.state, ScrollState::Bouncing);
assert!(physics.spring_y.is_some());
use crate::stateful::{scroll_events, StateTransitions};
physics.offset_y = 0.0; if let Some(new_state) = physics.state.on_event(scroll_events::SETTLED) {
physics.state = new_state;
}
assert_eq!(physics.state, ScrollState::Idle);
assert!((physics.offset_y - 0.0).abs() < 1.0);
}
#[test]
fn test_scroll_physics_no_bounce() {
let config = ScrollConfig::no_bounce();
let mut physics = ScrollPhysics::new(config);
physics.viewport_height = 400.0;
physics.content_height = 1000.0;
physics.apply_scroll_delta(0.0, 100.0);
assert_eq!(physics.offset_y, 0.0);
}
#[test]
fn test_scroll_settling() {
let mut physics = ScrollPhysics::default();
physics.viewport_height = 400.0;
physics.content_height = 1000.0;
physics.apply_scroll_delta(0.0, -50.0);
assert_eq!(physics.state, ScrollState::Scrolling);
assert_eq!(physics.offset_y, -50.0);
physics.on_scroll_end();
assert_eq!(physics.state, ScrollState::Decelerating);
let still_animating = physics.tick(1.0 / 60.0);
assert!(!still_animating);
use crate::stateful::{scroll_events, StateTransitions};
if let Some(new_state) = physics.state.on_event(scroll_events::SETTLED) {
physics.state = new_state;
}
assert_eq!(physics.state, ScrollState::Idle);
}
#[test]
fn test_scroll_element_builder() {
use crate::text::text;
let s = scroll().h(400.0).rounded(8.0).child(text("Hello"));
let mut tree = LayoutTree::new();
let _node = s.build(&mut tree);
assert!(s.scroll_info().is_some());
}
#[test]
fn test_scroll_child_starts_at_origin() {
use crate::div::div;
use taffy::{AvailableSpace, Size};
let s = scroll().w(400.0).h(300.0).child(div().w(200.0).h(100.0));
let mut tree = LayoutTree::new();
let root_id = s.build(&mut tree);
tree.compute_layout(
root_id,
Size {
width: AvailableSpace::Definite(400.0),
height: AvailableSpace::Definite(300.0),
},
);
let children = tree.children(root_id);
assert!(!children.is_empty(), "Scroll should have a child");
let child_id = children[0];
let child_layout = tree.get_layout(child_id).expect("Child should have layout");
assert_eq!(
child_layout.location.x, 0.0,
"Child x should be at origin, got {}",
child_layout.location.x
);
assert_eq!(
child_layout.location.y, 0.0,
"Child y should be at origin, got {}",
child_layout.location.y
);
}
#[test]
fn test_scroll_with_items_center_still_starts_at_origin() {
use crate::div::div;
use taffy::{AvailableSpace, Size};
let s = scroll()
.w(400.0)
.h(300.0)
.items_center() .child(div().w(200.0).h(100.0));
let mut tree = LayoutTree::new();
let root_id = s.build(&mut tree);
tree.compute_layout(
root_id,
Size {
width: AvailableSpace::Definite(400.0),
height: AvailableSpace::Definite(300.0),
},
);
let children = tree.children(root_id);
let child_id = children[0];
let child_layout = tree.get_layout(child_id).expect("Child should have layout");
println!(
"With items_center: child location = ({}, {})",
child_layout.location.x, child_layout.location.y
);
}
#[test]
fn test_scroll_with_justify_center_offsets_content() {
use crate::div::div;
use taffy::{AvailableSpace, Size};
let s = scroll()
.w(400.0)
.h(300.0)
.justify_center() .child(div().w(200.0).h(100.0));
let mut tree = LayoutTree::new();
let root_id = s.build(&mut tree);
tree.compute_layout(
root_id,
Size {
width: AvailableSpace::Definite(400.0),
height: AvailableSpace::Definite(300.0),
},
);
let children = tree.children(root_id);
let child_id = children[0];
let child_layout = tree.get_layout(child_id).expect("Child should have layout");
assert_eq!(
child_layout.location.x, 100.0,
"justify_center should center child on x axis (main axis for flex-row)"
);
assert_eq!(
child_layout.location.y, 0.0,
"y should be 0 (cross axis not affected)"
);
}
#[test]
fn test_horizontal_scroll_content_position() {
use crate::div::div;
use taffy::{AvailableSpace, Size};
let s = scroll()
.direction(ScrollDirection::Horizontal)
.w(400.0)
.h(300.0)
.items_start() .child(div().flex_row().gap(20.0).children(vec![
div().w(280.0).h(280.0),
div().w(280.0).h(280.0),
div().w(280.0).h(280.0),
]));
let mut tree = LayoutTree::new();
let root_id = s.build(&mut tree);
tree.compute_layout(
root_id,
Size {
width: AvailableSpace::Definite(400.0),
height: AvailableSpace::Definite(300.0),
},
);
let children = tree.children(root_id);
assert!(!children.is_empty(), "Scroll should have content");
let content_id = children[0];
let content_layout = tree
.get_layout(content_id)
.expect("Content should have layout");
println!(
"Content container: location=({}, {}), size=({}, {})",
content_layout.location.x,
content_layout.location.y,
content_layout.size.width,
content_layout.size.height
);
assert_eq!(
content_layout.location.x, 0.0,
"Horizontal scroll content should start at x=0, got {}",
content_layout.location.x
);
assert_eq!(
content_layout.location.y, 0.0,
"Content should start at y=0, got {}",
content_layout.location.y
);
let content_children = tree.children(content_id);
if !content_children.is_empty() {
let first_card = content_children[0];
let first_card_layout = tree.get_layout(first_card).expect("First card layout");
println!(
"First card: location=({}, {})",
first_card_layout.location.x, first_card_layout.location.y
);
assert_eq!(
first_card_layout.location.x, 0.0,
"First card should be at x=0, got {}",
first_card_layout.location.x
);
}
}
#[test]
fn test_scrollbar_state_visibility() {
assert!(!ScrollbarState::Idle.is_visible());
assert!(ScrollbarState::TrackHovered.is_visible());
assert!(ScrollbarState::ThumbHovered.is_visible());
assert!(ScrollbarState::Dragging.is_visible());
assert!(ScrollbarState::Scrolling.is_visible());
assert!(ScrollbarState::FadingOut.is_visible());
}
#[test]
fn test_scrollbar_state_interacting() {
assert!(!ScrollbarState::Idle.is_interacting());
assert!(!ScrollbarState::TrackHovered.is_interacting());
assert!(ScrollbarState::ThumbHovered.is_interacting());
assert!(ScrollbarState::Dragging.is_interacting());
assert!(!ScrollbarState::Scrolling.is_interacting());
assert!(!ScrollbarState::FadingOut.is_interacting());
}
#[test]
fn test_scrollbar_state_opacity() {
assert_eq!(ScrollbarState::Idle.opacity(), 0.0);
assert!(ScrollbarState::Dragging.opacity() > ScrollbarState::ThumbHovered.opacity());
assert!(ScrollbarState::ThumbHovered.opacity() > ScrollbarState::Scrolling.opacity());
assert!(ScrollbarState::FadingOut.opacity() > 0.0);
}
#[test]
fn test_scrollbar_size_presets() {
assert_eq!(ScrollbarSize::Thin.width(), 4.0);
assert_eq!(ScrollbarSize::Normal.width(), 6.0);
assert_eq!(ScrollbarSize::Wide.width(), 10.0);
}
#[test]
fn test_scrollbar_config_default() {
let config = ScrollbarConfig::default();
assert_eq!(config.visibility, ScrollbarVisibility::Auto);
assert_eq!(config.size, ScrollbarSize::Normal);
assert!(config.custom_width.is_none());
assert!(config.auto_dismiss_delay > 0.0);
assert!(config.min_thumb_length > 0.0);
}
#[test]
fn test_scrollbar_config_presets() {
let always = ScrollbarConfig::always_visible();
assert_eq!(always.visibility, ScrollbarVisibility::Always);
let hover = ScrollbarConfig::show_on_hover();
assert_eq!(hover.visibility, ScrollbarVisibility::Hover);
let hidden = ScrollbarConfig::hidden();
assert_eq!(hidden.visibility, ScrollbarVisibility::Never);
}
#[test]
fn test_scrollbar_config_width() {
let mut config = ScrollbarConfig::default();
assert_eq!(config.width(), 6.0);
config.size = ScrollbarSize::Thin;
assert_eq!(config.width(), 4.0);
config.custom_width = Some(12.0);
assert_eq!(config.width(), 12.0); }
#[test]
fn test_scrollbar_thumb_dimensions() {
let mut physics = ScrollPhysics::default();
physics.viewport_height = 400.0;
physics.content_height = 1000.0;
physics.viewport_width = 300.0;
physics.content_width = 600.0;
let (thumb_height, thumb_y) = physics.thumb_dimensions_y();
assert!(thumb_height >= physics.config.scrollbar.min_thumb_length);
assert!(thumb_height <= physics.viewport_height);
assert!(thumb_y >= 0.0);
let (thumb_width, thumb_x) = physics.thumb_dimensions_x();
assert!(thumb_width >= physics.config.scrollbar.min_thumb_length);
assert!(thumb_width <= physics.viewport_width);
assert!(thumb_x >= 0.0);
}
#[test]
fn test_scrollbar_thumb_position_updates() {
let mut physics = ScrollPhysics::default();
physics.viewport_height = 400.0;
physics.content_height = 1000.0;
physics.offset_y = 0.0;
let (_, thumb_y_at_top) = physics.thumb_dimensions_y();
physics.offset_y = -300.0; let (_, thumb_y_at_middle) = physics.thumb_dimensions_y();
physics.offset_y = -600.0;
let (_, thumb_y_at_bottom) = physics.thumb_dimensions_y();
assert!(thumb_y_at_middle > thumb_y_at_top);
assert!(thumb_y_at_bottom > thumb_y_at_middle);
}
#[test]
fn test_scrollbar_state_transitions() {
let mut physics = ScrollPhysics::default();
physics.viewport_height = 400.0;
physics.content_height = 1000.0;
assert_eq!(physics.scrollbar_state, ScrollbarState::Idle);
assert_eq!(physics.scrollbar_opacity, 0.0);
physics.on_area_hover_enter();
assert!(physics.area_hovered);
physics.on_scrollbar_thumb_hover();
assert_eq!(physics.scrollbar_state, ScrollbarState::ThumbHovered);
physics.on_scrollbar_drag_start(100.0, 100.0);
assert_eq!(physics.scrollbar_state, ScrollbarState::Dragging);
assert_eq!(physics.thumb_drag_start_y, 100.0);
physics.on_scrollbar_drag_end();
assert_ne!(physics.scrollbar_state, ScrollbarState::Dragging);
physics.on_area_hover_leave();
assert!(!physics.area_hovered);
}
#[test]
fn test_scrollbar_can_scroll() {
let mut physics = ScrollPhysics::default();
physics.viewport_height = 400.0;
physics.content_height = 300.0;
assert!(!physics.can_scroll_y());
physics.content_height = 1000.0;
assert!(physics.can_scroll_y());
physics.viewport_width = 300.0;
physics.content_width = 200.0;
assert!(!physics.can_scroll_x());
physics.content_width = 600.0;
assert!(physics.can_scroll_x());
}
#[test]
fn test_scrollbar_render_info() {
let mut physics = ScrollPhysics::default();
physics.viewport_height = 400.0;
physics.content_height = 1000.0;
physics.viewport_width = 300.0;
physics.content_width = 300.0;
let info = physics.scrollbar_render_info();
assert_eq!(info.state, ScrollbarState::Idle);
assert!(info.show_vertical); assert!(!info.show_horizontal); assert!(info.vertical_thumb_height > 0.0);
}
#[test]
fn test_scroll_builder_scrollbar_config() {
let s = scroll()
.h(400.0)
.scrollbar_always()
.scrollbar_thin()
.scrollbar_dismiss_delay(2.0);
let physics = s.physics.lock().unwrap();
assert_eq!(
physics.config.scrollbar.visibility,
ScrollbarVisibility::Always
);
assert_eq!(physics.config.scrollbar.size, ScrollbarSize::Thin);
assert_eq!(physics.config.scrollbar.auto_dismiss_delay, 2.0);
}
}