use std::sync::{Arc, Mutex, Weak};
use blinc_core::reactive::SignalId;
use crate::tree::LayoutNodeId;
use super::registry::ElementRegistry;
use super::{ScrollBehavior, ScrollOptions};
pub type TriggerCallback = Arc<dyn Fn() + Send + Sync>;
pub type SharedScrollRefInner = Arc<Mutex<ScrollRefInner>>;
#[derive(Debug, Default)]
pub struct ScrollRefInner {
node_id: Option<LayoutNodeId>,
registry: Option<Weak<ElementRegistry>>,
offset: (f32, f32),
content_size: Option<(f32, f32)>,
viewport_size: Option<(f32, f32)>,
pending_scroll: Option<PendingScroll>,
dirty: bool,
}
#[derive(Debug, Clone)]
pub enum PendingScroll {
ToOffset { x: f32, y: f32, smooth: bool },
ByAmount { dx: f32, dy: f32, smooth: bool },
ToElement {
element_id: String,
options: ScrollOptions,
},
ToTop { smooth: bool },
ToBottom { smooth: bool },
}
#[derive(Clone)]
pub struct ScrollRef {
inner: Arc<Mutex<ScrollRefInner>>,
signal_id: SignalId,
trigger: TriggerCallback,
}
impl Default for ScrollRef {
fn default() -> Self {
Self::new()
}
}
impl ScrollRef {
pub fn with_trigger(signal_id: SignalId, trigger: TriggerCallback) -> Self {
Self {
inner: Arc::new(Mutex::new(ScrollRefInner::default())),
signal_id,
trigger,
}
}
pub fn with_inner(
inner: SharedScrollRefInner,
signal_id: SignalId,
trigger: TriggerCallback,
) -> Self {
Self {
inner,
signal_id,
trigger,
}
}
pub fn inner(&self) -> SharedScrollRefInner {
Arc::clone(&self.inner)
}
pub fn new_inner() -> SharedScrollRefInner {
Arc::new(Mutex::new(ScrollRefInner::default()))
}
}
impl std::fmt::Debug for ScrollRef {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ScrollRef")
.field("bound", &self.is_bound())
.finish()
}
}
impl ScrollRef {
pub fn new() -> Self {
let noop_trigger: TriggerCallback = Arc::new(|| {});
Self {
inner: Arc::new(Mutex::new(ScrollRefInner::default())),
signal_id: SignalId::from_raw(0), trigger: noop_trigger,
}
}
pub fn signal_id(&self) -> SignalId {
self.signal_id
}
fn trigger(&self) {
(self.trigger)();
}
pub fn is_bound(&self) -> bool {
self.inner
.lock()
.ok()
.is_some_and(|inner| inner.node_id.is_some())
}
pub(crate) fn bind_to_node(&self, node_id: LayoutNodeId, registry: Weak<ElementRegistry>) {
if let Ok(mut inner) = self.inner.lock() {
inner.node_id = Some(node_id);
inner.registry = Some(registry);
}
}
pub(crate) fn node_id(&self) -> Option<LayoutNodeId> {
self.inner.lock().ok()?.node_id
}
pub(crate) fn update_state(
&self,
offset: (f32, f32),
content_size: (f32, f32),
viewport_size: (f32, f32),
) {
if let Ok(mut inner) = self.inner.lock() {
inner.offset = offset;
inner.content_size = Some(content_size);
inner.viewport_size = Some(viewport_size);
}
}
pub(crate) fn take_pending_scroll(&self) -> Option<PendingScroll> {
self.inner.lock().ok()?.pending_scroll.take()
}
pub(crate) fn take_dirty(&self) -> bool {
if let Ok(mut inner) = self.inner.lock() {
let dirty = inner.dirty;
inner.dirty = false;
dirty
} else {
false
}
}
pub fn scroll_to(&self, element_id: &str) {
self.scroll_to_with_options(element_id, ScrollOptions::default());
}
pub fn scroll_to_with_options(&self, element_id: &str, options: ScrollOptions) {
if let Ok(mut inner) = self.inner.lock() {
inner.pending_scroll = Some(PendingScroll::ToElement {
element_id: element_id.to_string(),
options,
});
inner.dirty = true;
}
self.trigger();
}
pub fn scroll_to_top(&self) {
self.scroll_to_top_with_behavior(ScrollBehavior::Auto);
}
pub fn scroll_to_top_with_behavior(&self, behavior: ScrollBehavior) {
if let Ok(mut inner) = self.inner.lock() {
inner.pending_scroll = Some(PendingScroll::ToTop {
smooth: behavior == ScrollBehavior::Smooth,
});
inner.dirty = true;
}
self.trigger();
}
pub fn scroll_to_bottom(&self) {
self.scroll_to_bottom_with_behavior(ScrollBehavior::Auto);
}
pub fn scroll_to_bottom_with_behavior(&self, behavior: ScrollBehavior) {
if let Ok(mut inner) = self.inner.lock() {
inner.pending_scroll = Some(PendingScroll::ToBottom {
smooth: behavior == ScrollBehavior::Smooth,
});
inner.dirty = true;
}
self.trigger();
}
pub fn scroll_by(&self, dx: f32, dy: f32) {
self.scroll_by_with_behavior(dx, dy, ScrollBehavior::Auto);
}
pub fn scroll_by_with_behavior(&self, dx: f32, dy: f32, behavior: ScrollBehavior) {
if let Ok(mut inner) = self.inner.lock() {
inner.pending_scroll = Some(PendingScroll::ByAmount {
dx,
dy,
smooth: behavior == ScrollBehavior::Smooth,
});
inner.dirty = true;
}
self.trigger();
}
pub fn set_scroll_offset(&self, x: f32, y: f32) {
self.set_scroll_offset_with_behavior(x, y, ScrollBehavior::Auto);
}
pub fn set_scroll_offset_with_behavior(&self, x: f32, y: f32, behavior: ScrollBehavior) {
if let Ok(mut inner) = self.inner.lock() {
inner.pending_scroll = Some(PendingScroll::ToOffset {
x,
y,
smooth: behavior == ScrollBehavior::Smooth,
});
inner.dirty = true;
}
self.trigger();
}
pub fn offset(&self) -> (f32, f32) {
self.inner.lock().ok().map(|i| i.offset).unwrap_or_default()
}
pub fn scroll_x(&self) -> f32 {
self.offset().0
}
pub fn scroll_y(&self) -> f32 {
self.offset().1
}
pub fn content_size(&self) -> Option<(f32, f32)> {
self.inner.lock().ok()?.content_size
}
pub fn viewport_size(&self) -> Option<(f32, f32)> {
self.inner.lock().ok()?.viewport_size
}
pub fn max_scroll(&self) -> Option<(f32, f32)> {
let inner = self.inner.lock().ok()?;
let content = inner.content_size?;
let viewport = inner.viewport_size?;
Some((
(content.0 - viewport.0).max(0.0),
(content.1 - viewport.1).max(0.0),
))
}
pub fn is_at_top(&self) -> bool {
self.scroll_y() <= 0.0
}
pub fn is_at_bottom(&self) -> bool {
if let Some((_, max_y)) = self.max_scroll() {
self.scroll_y() >= max_y - 1.0 } else {
true
}
}
pub fn scroll_progress(&self) -> f32 {
if let Some((_, max_y)) = self.max_scroll() {
if max_y > 0.0 {
(self.scroll_y() / max_y).clamp(0.0, 1.0)
} else {
0.0
}
} else {
0.0
}
}
}
pub fn use_scroll_ref(key: &str) -> ScrollRef {
use blinc_core::BlincContextState;
let ctx = BlincContextState::get();
let state_key = format!("scroll_ref:{}", key);
let (signal_id, inner) = ctx.get_or_create_persisted(&state_key, ScrollRef::new_inner);
let noop_trigger: TriggerCallback = Arc::new(|| {});
ScrollRef::with_inner(inner, signal_id, noop_trigger)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::selector::{ScrollBlock, ScrollInline};
#[test]
fn test_scroll_ref_new() {
let scroll_ref = ScrollRef::new();
assert!(!scroll_ref.is_bound());
}
#[test]
fn test_scroll_to_bottom() {
let scroll_ref = ScrollRef::new();
scroll_ref.scroll_to_bottom();
let pending = scroll_ref.take_pending_scroll();
assert!(matches!(
pending,
Some(PendingScroll::ToBottom { smooth: false })
));
}
#[test]
fn test_scroll_to_element() {
let scroll_ref = ScrollRef::new();
scroll_ref.scroll_to_with_options(
"my-item",
ScrollOptions {
behavior: ScrollBehavior::Smooth,
block: ScrollBlock::Center,
inline: ScrollInline::Nearest,
},
);
let pending = scroll_ref.take_pending_scroll();
assert!(matches!(
pending,
Some(PendingScroll::ToElement {
element_id,
options,
}) if element_id == "my-item" && options.behavior == ScrollBehavior::Smooth
));
}
#[test]
fn test_scroll_offset_query() {
let scroll_ref = ScrollRef::new();
scroll_ref.update_state((100.0, 200.0), (500.0, 1000.0), (300.0, 400.0));
assert_eq!(scroll_ref.offset(), (100.0, 200.0));
assert_eq!(scroll_ref.scroll_x(), 100.0);
assert_eq!(scroll_ref.scroll_y(), 200.0);
assert_eq!(scroll_ref.content_size(), Some((500.0, 1000.0)));
assert_eq!(scroll_ref.viewport_size(), Some((300.0, 400.0)));
assert_eq!(scroll_ref.max_scroll(), Some((200.0, 600.0)));
}
#[test]
fn test_scroll_progress() {
let scroll_ref = ScrollRef::new();
scroll_ref.update_state((0.0, 300.0), (500.0, 1000.0), (300.0, 400.0));
assert!((scroll_ref.scroll_progress() - 0.5).abs() < 0.01);
}
}