mod scrollbar;
use std::time::Instant;
use derive_setters::Setters;
use tessera_ui::{
Color, ComputedData, Constraint, CursorEventContent, DimensionValue, Dp, MeasurementError,
Modifier, Px, PxPosition, State,
layout::{LayoutInput, LayoutOutput, LayoutSpec, RenderInput},
remember, tessera,
};
use crate::{
alignment::Alignment,
boxed::{BoxedArgs, boxed},
modifier::ModifierExt,
pos_misc::is_position_in_component,
scrollable::scrollbar::{ScrollBarArgs, ScrollBarState, scrollbar_h, scrollbar_v},
};
#[derive(Debug, Setters, Clone)]
pub struct ScrollableArgs {
pub modifier: Modifier,
pub vertical: bool,
pub horizontal: bool,
pub scroll_smoothing: f32,
pub scrollbar_behavior: ScrollBarBehavior,
pub scrollbar_track_color: Color,
pub scrollbar_thumb_color: Color,
pub scrollbar_thumb_hover_color: Color,
pub scrollbar_layout: ScrollBarLayout,
}
#[derive(Debug, Clone)]
pub enum ScrollBarBehavior {
AlwaysVisible,
AutoHide,
Hidden,
}
#[derive(Debug, Clone)]
pub enum ScrollBarLayout {
Alongside,
Overlay,
}
impl Default for ScrollableArgs {
fn default() -> Self {
Self {
modifier: Modifier::new().fill_max_size(),
vertical: true,
horizontal: false,
scroll_smoothing: 0.05,
scrollbar_behavior: ScrollBarBehavior::AlwaysVisible,
scrollbar_track_color: Color::new(0.0, 0.0, 0.0, 0.1),
scrollbar_thumb_color: Color::new(0.0, 0.0, 0.0, 0.3),
scrollbar_thumb_hover_color: Color::new(0.0, 0.0, 0.0, 0.5),
scrollbar_layout: ScrollBarLayout::Alongside,
}
}
}
#[derive(Clone)]
pub struct ScrollableController {
child_position: PxPosition,
target_position: PxPosition,
child_size: ComputedData,
visible_size: ComputedData,
override_child_size: Option<ComputedData>,
last_frame_time: Option<Instant>,
scrollbar_state_v: ScrollBarState,
scrollbar_state_h: ScrollBarState,
}
impl Default for ScrollableController {
fn default() -> Self {
Self::new()
}
}
impl ScrollableController {
pub fn new() -> Self {
Self {
child_position: PxPosition::ZERO,
target_position: PxPosition::ZERO,
child_size: ComputedData::ZERO,
visible_size: ComputedData::ZERO,
override_child_size: None,
last_frame_time: None,
scrollbar_state_v: ScrollBarState::default(),
scrollbar_state_h: ScrollBarState::default(),
}
}
pub fn child_position(&self) -> PxPosition {
self.child_position
}
pub fn visible_size(&self) -> ComputedData {
self.visible_size
}
fn child_size(&self) -> ComputedData {
self.child_size
}
pub fn override_child_size(&mut self, size: ComputedData) {
self.override_child_size = Some(size);
}
fn target_position(&self) -> PxPosition {
self.target_position
}
fn set_target_position(&mut self, target: PxPosition) {
self.target_position = target;
}
pub fn set_scroll_position(&mut self, position: PxPosition) {
self.child_position = position;
self.target_position = position;
}
fn update_scroll_position(&mut self, smoothing: f32) -> bool {
let current_time = Instant::now();
let delta_time = if let Some(last_time) = self.last_frame_time {
current_time.duration_since(last_time).as_secs_f32()
} else {
0.016 };
self.last_frame_time = Some(current_time);
let diff_x = self.target_position.x.to_f32() - self.child_position.x.to_f32();
let diff_y = self.target_position.y.to_f32() - self.child_position.y.to_f32();
if diff_x.abs() < 1.0 && diff_y.abs() < 1.0 {
if self.child_position != self.target_position {
self.child_position = self.target_position;
return true;
}
return false;
}
let mut movement_factor = (1.0 - smoothing) * delta_time * 60.0;
if movement_factor > 1.0 {
movement_factor = 1.0;
}
let old_position = self.child_position;
self.child_position = PxPosition {
x: Px::saturating_from_f32(self.child_position.x.to_f32() + diff_x * movement_factor),
y: Px::saturating_from_f32(self.child_position.y.to_f32() + diff_y * movement_factor),
};
old_position != self.child_position
}
pub(crate) fn scrollbar_state_v(&self) -> ScrollBarState {
self.scrollbar_state_v.clone()
}
pub(crate) fn scrollbar_state_h(&self) -> ScrollBarState {
self.scrollbar_state_h.clone()
}
}
#[derive(Clone, PartialEq)]
struct ScrollableAlongsideLayout {
vertical: bool,
horizontal: bool,
}
impl LayoutSpec for ScrollableAlongsideLayout {
fn measure(
&self,
input: &LayoutInput<'_>,
output: &mut LayoutOutput<'_>,
) -> Result<ComputedData, MeasurementError> {
let mut final_size = ComputedData::ZERO;
let mut content_constraint = Constraint::new(
input.parent_constraint().width(),
input.parent_constraint().height(),
);
if self.vertical {
let scrollbar_node_id = input.children_ids()[1];
let size = input.measure_child_in_parent_constraint(scrollbar_node_id)?;
content_constraint.width -= size.width;
final_size.width += size.width;
}
if self.horizontal {
let scrollbar_node_id = if self.vertical {
input.children_ids()[2]
} else {
input.children_ids()[1]
};
let size = input.measure_child_in_parent_constraint(scrollbar_node_id)?;
content_constraint.height -= size.height;
final_size.height += size.height;
}
let content_node_id = input.children_ids()[0];
let content_measurement = input.measure_child(content_node_id, &content_constraint)?;
final_size.width += content_measurement.width;
final_size.height += content_measurement.height;
output.place_child(content_node_id, PxPosition::ZERO);
if self.vertical {
output.place_child(
input.children_ids()[1],
PxPosition::new(content_measurement.width, Px::ZERO),
);
}
if self.horizontal {
let scrollbar_node_id = if self.vertical {
input.children_ids()[2]
} else {
input.children_ids()[1]
};
output.place_child(
scrollbar_node_id,
PxPosition::new(Px::ZERO, content_measurement.height),
);
}
Ok(final_size)
}
}
#[derive(Clone)]
struct ScrollableInnerLayout {
controller: State<ScrollableController>,
vertical: bool,
horizontal: bool,
child_position: PxPosition,
has_override: bool,
}
impl PartialEq for ScrollableInnerLayout {
fn eq(&self, other: &Self) -> bool {
self.vertical == other.vertical
&& self.horizontal == other.horizontal
&& self.child_position == other.child_position
&& self.has_override == other.has_override
}
}
impl LayoutSpec for ScrollableInnerLayout {
fn measure(
&self,
input: &LayoutInput<'_>,
output: &mut LayoutOutput<'_>,
) -> Result<ComputedData, MeasurementError> {
let merged_constraint = Constraint::new(
input.parent_constraint().width(),
input.parent_constraint().height(),
);
let mut child_constraint = merged_constraint;
if self.vertical {
child_constraint.height = DimensionValue::Wrap {
min: None,
max: None,
};
}
if self.horizontal {
child_constraint.width = DimensionValue::Wrap {
min: None,
max: None,
};
}
let child_node_id = input.children_ids()[0];
let child_measurement = input.measure_child(child_node_id, &child_constraint)?;
let current_child_position = self.child_position;
self.controller.with_mut(|c| {
if let Some(override_size) = c.override_child_size.take() {
c.child_size = override_size;
} else {
c.child_size = child_measurement;
}
});
output.place_child(child_node_id, current_child_position);
let mut width = resolve_dimension(merged_constraint.width, child_measurement.width);
let mut height = resolve_dimension(merged_constraint.height, child_measurement.height);
if let Some(parent_max_width) = input.parent_constraint().width().get_max() {
width = width.min(parent_max_width);
}
if let Some(parent_max_height) = input.parent_constraint().height().get_max() {
height = height.min(parent_max_height);
}
let computed_data = ComputedData { width, height };
self.controller.with_mut(|c| c.visible_size = computed_data);
Ok(computed_data)
}
fn record(&self, input: &RenderInput<'_>) {
input.metadata_mut().clips_children = true;
}
}
#[tessera]
pub fn scrollable(args: impl Into<ScrollableArgs>, child: impl FnOnce() + Send + Sync + 'static) {
let controller = remember(ScrollableController::new);
scrollable_with_controller(args, controller, child);
}
#[tessera]
pub fn scrollable_with_controller(
args: impl Into<ScrollableArgs>,
controller: State<ScrollableController>,
child: impl FnOnce() + Send + Sync + 'static,
) {
let args: ScrollableArgs = args.into();
let modifier = args.modifier;
let scrollbar_args_v = ScrollBarArgs {
total: controller.with(|c| c.child_size().height),
visible: controller.with(|c| c.visible_size().height),
offset: controller.with(|c| c.child_position().y),
thickness: Dp(8.0), state: controller,
scrollbar_behavior: args.scrollbar_behavior.clone(),
track_color: args.scrollbar_track_color,
thumb_color: args.scrollbar_thumb_color,
thumb_hover_color: args.scrollbar_thumb_hover_color,
};
let scrollbar_args_h = ScrollBarArgs {
total: controller.with(|c| c.child_size().width),
visible: controller.with(|c| c.visible_size().width),
offset: controller.with(|c| c.child_position().x),
thickness: Dp(8.0), state: controller,
scrollbar_behavior: args.scrollbar_behavior.clone(),
track_color: args.scrollbar_track_color,
thumb_color: args.scrollbar_thumb_color,
thumb_hover_color: args.scrollbar_thumb_hover_color,
};
match args.scrollbar_layout {
ScrollBarLayout::Alongside => {
modifier.run(move || {
scrollable_with_alongside_scrollbar(
controller,
args,
scrollbar_args_v,
scrollbar_args_h,
child,
);
});
}
ScrollBarLayout::Overlay => {
modifier.run(move || {
scrollable_with_overlay_scrollbar(
controller,
args,
scrollbar_args_v,
scrollbar_args_h,
child,
);
});
}
}
}
#[tessera]
fn scrollable_with_alongside_scrollbar(
controller: State<ScrollableController>,
args: ScrollableArgs,
scrollbar_args_v: ScrollBarArgs,
scrollbar_args_h: ScrollBarArgs,
child: impl FnOnce() + Send + Sync + 'static,
) {
let scrollbar_v_state = controller.with(|c| c.scrollbar_state_v());
let scrollbar_h_state = controller.with(|c| c.scrollbar_state_h());
scrollable_inner(
args.clone(),
controller,
scrollbar_v_state.clone(),
scrollbar_h_state.clone(),
child,
);
if args.vertical {
scrollbar_v(scrollbar_args_v, scrollbar_v_state);
}
if args.horizontal {
scrollbar_h(scrollbar_args_h, scrollbar_h_state);
}
layout(ScrollableAlongsideLayout {
vertical: args.vertical,
horizontal: args.horizontal,
});
}
#[tessera]
fn scrollable_with_overlay_scrollbar(
controller: State<ScrollableController>,
args: ScrollableArgs,
scrollbar_args_v: ScrollBarArgs,
scrollbar_args_h: ScrollBarArgs,
child: impl FnOnce() + Send + Sync + 'static,
) {
boxed(
BoxedArgs::default()
.modifier(Modifier::new().fill_max_size())
.alignment(Alignment::BottomEnd),
|scope| {
scope.child({
let args = args.clone();
let scrollbar_v_state = controller.with(|c| c.scrollbar_state_v());
let scrollbar_h_state = controller.with(|c| c.scrollbar_state_h());
move || {
scrollable_inner(
args,
controller,
scrollbar_v_state,
scrollbar_h_state,
child,
);
}
});
scope.child({
let scrollbar_args_v = scrollbar_args_v.clone();
let args = args.clone();
let scrollbar_v_state = controller.with(|c| c.scrollbar_state_v());
move || {
if args.vertical {
scrollbar_v(scrollbar_args_v, scrollbar_v_state);
}
}
});
scope.child({
let scrollbar_args_h = scrollbar_args_h.clone();
let args = args.clone();
let scrollbar_h_state = controller.with(|c| c.scrollbar_state_h());
move || {
if args.horizontal {
scrollbar_h(scrollbar_args_h, scrollbar_h_state);
}
}
});
},
);
}
fn clamp_wrap(min: Option<Px>, max: Option<Px>, measure: Px) -> Px {
min.unwrap_or(Px(0))
.max(measure)
.min(max.unwrap_or(Px::MAX))
}
fn fill_value(min: Option<Px>, max: Option<Px>, measure: Px) -> Px {
max.expect("Seems that you are trying to fill an infinite dimension, which is not allowed")
.max(measure)
.max(min.unwrap_or(Px(0)))
}
fn resolve_dimension(dim: DimensionValue, measure: Px) -> Px {
match dim {
DimensionValue::Fixed(v) => v,
DimensionValue::Wrap { min, max } => clamp_wrap(min, max, measure),
DimensionValue::Fill { min, max } => fill_value(min, max, measure),
}
}
#[tessera]
fn scrollable_inner(
args: ScrollableArgs,
controller: State<ScrollableController>,
scrollbar_state_v: ScrollBarState,
scrollbar_state_h: ScrollBarState,
child: impl FnOnce(),
) {
controller.with_mut(|c| c.update_scroll_position(args.scroll_smoothing));
let child_position = controller.with(|c| c.child_position());
let has_override = controller.with(|c| c.override_child_size.is_some());
layout(ScrollableInnerLayout {
controller,
vertical: args.vertical,
horizontal: args.horizontal,
child_position,
has_override,
});
input_handler(move |input| {
let size = input.computed_data;
let cursor_pos_option = input.cursor_position_rel;
let is_cursor_in_component = cursor_pos_option
.map(|pos| is_position_in_component(size, pos))
.unwrap_or(false);
if is_cursor_in_component {
for event in input
.cursor_events
.iter()
.filter_map(|event| match &event.content {
CursorEventContent::Scroll(event) => Some(event),
_ => None,
})
{
controller.with_mut(|c| {
let scroll_delta_x = event.delta_x;
let scroll_delta_y = event.delta_y;
let current_target = c.target_position;
let new_target = current_target.saturating_offset(
Px::saturating_from_f32(scroll_delta_x),
Px::saturating_from_f32(scroll_delta_y),
);
let child_size = c.child_size;
let constrained_target = constrain_position(
new_target,
&child_size,
&input.computed_data,
args.vertical,
args.horizontal,
);
c.set_target_position(constrained_target);
});
if matches!(args.scrollbar_behavior, ScrollBarBehavior::AutoHide) {
if args.vertical {
let mut scrollbar_state = scrollbar_state_v.write();
scrollbar_state.last_scroll_activity = Some(std::time::Instant::now());
scrollbar_state.should_be_visible = true;
}
if args.horizontal {
let mut scrollbar_state = scrollbar_state_h.write();
scrollbar_state.last_scroll_activity = Some(std::time::Instant::now());
scrollbar_state.should_be_visible = true;
}
}
}
let target = controller.with(|c| c.target_position());
let child_size = controller.with(|c| c.child_size());
let constrained_position = constrain_position(
target,
&child_size,
&input.computed_data,
args.vertical,
args.horizontal,
);
controller.with_mut(|c| c.set_target_position(constrained_position));
input.cursor_events.clear();
}
});
child();
}
fn constrain_axis(pos: Px, child_len: Px, container_len: Px) -> Px {
if child_len <= container_len {
return Px::ZERO;
}
if pos > Px::ZERO {
Px::ZERO
} else if pos.saturating_add(child_len) < container_len {
container_len.saturating_sub(child_len)
} else {
pos
}
}
fn constrain_position(
position: PxPosition,
child_size: &ComputedData,
container_size: &ComputedData,
vertical_scrollable: bool,
horizontal_scrollable: bool,
) -> PxPosition {
let x = if horizontal_scrollable {
constrain_axis(position.x, child_size.width, container_size.width)
} else {
Px::ZERO
};
let y = if vertical_scrollable {
constrain_axis(position.y, child_size.height, container_size.height)
} else {
Px::ZERO
};
PxPosition { x, y }
}