use blinc_app::prelude::*;
use blinc_app::windowed::{WindowedApp, WindowedContext};
use blinc_core::State;
use blinc_layout::prelude::{ButtonState, NoState, Scroll, ScrollPhysics, SharedScrollPhysics};
use std::sync::{Arc, Mutex};
fn main() -> Result<()> {
tracing_subscriber::fmt()
.with_max_level(tracing::Level::INFO)
.init();
let config = WindowConfig {
title: "Blinc Scroll Example".to_string(),
width: 800,
height: 600,
resizable: true,
..Default::default()
};
WindowedApp::run(config, |ctx| build_ui(ctx))
}
fn build_ui(ctx: &WindowedContext) -> impl ElementBuilder {
let direction_state = ctx.use_state_keyed("scroll_direction", || ScrollDirection::Vertical);
let current_direction = direction_state.get();
let physics_state = ctx.use_state_keyed("scroll_physics", || {
Arc::new(Mutex::new(ScrollPhysics::default())) as SharedScrollPhysics
});
let physics = physics_state.get();
{
let mut p = physics.lock().unwrap();
if p.config.direction != current_direction {
p.set_direction(current_direction);
}
}
div()
.w(ctx.width)
.h(ctx.height)
.bg(Color::rgba(0.1, 0.1, 0.15, 1.0))
.flex_col()
.items_center()
.p(8.0)
.gap(5.0)
.child(
h2("Scroll Container Demo")
.weight(FontWeight::Bold)
.color(Color::WHITE),
)
.child(
h3("Scroll with mouse wheel or trackpad - bounce physics at edges!")
.color(Color::rgba(1.0, 1.0, 1.0, 0.7)),
)
.child(build_direction_toggle(ctx, &direction_state))
.child(build_scroll_container(ctx, &direction_state, physics))
}
fn build_direction_toggle(
_ctx: &WindowedContext,
current: &State<ScrollDirection>,
) -> impl ElementBuilder {
let direction_signal_id = current.signal_id();
let direction_for_label = current.clone();
let direction_state_for_click = current.clone();
div()
.flex_row()
.gap(4.0)
.items_center()
.child(span("Direction:").color(Color::rgba(1.0, 1.0, 1.0, 0.8)))
.child(
stateful::<ButtonState>()
.initial(ButtonState::Idle)
.deps([direction_signal_id])
.on_state(move |ctx| {
let bg = match ctx.state() {
ButtonState::Idle => Color::rgba(0.3, 0.5, 1.0, 0.8),
ButtonState::Hovered => Color::rgba(0.4, 0.6, 1.0, 0.9),
ButtonState::Pressed => Color::rgba(0.2, 0.4, 0.9, 1.0),
ButtonState::Disabled => Color::rgba(0.3, 0.3, 0.35, 0.5),
};
let label = match direction_for_label.get() {
ScrollDirection::Vertical => "Vertical",
ScrollDirection::Horizontal => "Horizontal",
ScrollDirection::Both => "Both",
};
div()
.bg(bg)
.px(4.0)
.py(2.0)
.rounded(8.0)
.justify_center()
.items_center()
.child(
text(label)
.weight(FontWeight::SemiBold)
.color(Color::WHITE)
.no_wrap(),
)
})
.on_click(move |_| {
let current = direction_state_for_click.get();
let next = match current {
ScrollDirection::Vertical => ScrollDirection::Horizontal,
ScrollDirection::Horizontal => ScrollDirection::Both,
ScrollDirection::Both => ScrollDirection::Vertical,
};
direction_state_for_click.set(next);
tracing::info!("Switched to {:?} scroll", next);
}),
)
}
fn build_scroll_container(
ctx: &WindowedContext,
direction: &State<ScrollDirection>,
physics: SharedScrollPhysics,
) -> impl ElementBuilder {
let viewport_width = ctx.width - 80.0;
let viewport_height = ctx.height - 100.0;
{
let mut p = physics.lock().unwrap();
p.viewport_width = viewport_width;
p.viewport_height = viewport_height;
}
let direction_clone = direction.clone();
let direction_signal_id = direction.signal_id();
stateful::<NoState>()
.deps([direction_signal_id])
.on_state(move |_ctx| {
let direction = direction_clone.get();
div().child(
Scroll::with_physics(physics.clone())
.w(viewport_width)
.h(viewport_height)
.rounded(24.0)
.bg(Color::rgba(0.15, 0.15, 0.2, 1.0))
.direction(direction)
.on_scroll(|e| {
tracing::info!(
"Scroll delta: ({:.1}, {:.1})",
e.scroll_delta_x,
e.scroll_delta_y
);
})
.child(build_scroll_content(direction)),
)
})
}
fn build_scroll_content(direction: ScrollDirection) -> impl ElementBuilder {
let is_horizontal = matches!(direction, ScrollDirection::Horizontal);
let container = div().p(20.0).gap(16.0);
let container = if is_horizontal {
container.flex_row().h_full()
} else {
container.w_full().flex_col()
};
container
.child(content_card(
"Glass Cards",
"These glass cards demonstrate that blur effects clip properly inside the scroll container.",
Color::rgba(0.4, 0.6, 1.0, 0.3),
is_horizontal,
))
.child(content_card(
"Bounce Physics",
"Scroll past the edges to see webkit-style spring bounce animation bring it back.",
Color::rgba(1.0, 0.4, 0.6, 0.3),
is_horizontal,
))
.child(content_card(
"Momentum Scrolling",
"Release while scrolling to see momentum-based deceleration.",
Color::rgba(0.4, 1.0, 0.6, 0.3),
is_horizontal,
))
.child(simple_card("Card 4", "More content to scroll through...", is_horizontal))
.child(simple_card("Card 5", "Keep scrolling!", is_horizontal))
.child(content_card(
"State Machine",
"The scroll uses a FSM with states: Idle, Scrolling, Decelerating, Bouncing.",
Color::rgba(0.8, 0.4, 1.0, 0.3),
is_horizontal,
))
.child(simple_card("Card 7", "Almost there...", is_horizontal))
.child(simple_card("Card 8", "A bit more content.", is_horizontal))
.child(content_card(
"Configurable",
"Bounce can be disabled, spring stiffness adjusted, and friction tuned.",
Color::rgba(1.0, 0.8, 0.2, 0.3),
is_horizontal,
))
.child(simple_card("Card 10", "Getting close to the end!", is_horizontal))
.child(simple_card("Card 11", "One more card...", is_horizontal))
.child(content_card(
"End of Content",
"You've reached the end! Scroll back or bounce.",
Color::rgba(0.2, 0.8, 0.8, 0.3),
is_horizontal,
))
}
fn content_card(
title: &str,
description: &str,
accent: Color,
is_horizontal: bool,
) -> impl ElementBuilder {
let card = div().glass().rounded(16.0).p(20.0).flex_col().gap(8.0);
let card = if is_horizontal {
card.w(280.0).h_full().flex_shrink_0()
} else {
card.w_full()
};
card
.child(div().w_full().h(4.0).bg(accent).rounded(2.0))
.child(
text(title)
.size(24.0)
.weight(FontWeight::Bold)
.color(Color::WHITE),
)
.child(
text(description)
.size(16.0)
.color(Color::rgba(1.0, 1.0, 1.0, 0.8)),
)
}
fn simple_card(title: &str, description: &str, is_horizontal: bool) -> impl ElementBuilder {
let card = div()
.bg(Color::rgba(0.2, 0.2, 0.25, 1.0))
.rounded(12.0)
.p(16.0)
.flex_col()
.gap(4.0);
let card = if is_horizontal {
card.w(200.0).h_full().flex_shrink_0()
} else {
card.w_full()
};
card.child(
text(title)
.size(20.0)
.weight(FontWeight::SemiBold)
.color(Color::WHITE),
)
.child(
text(description)
.size(14.0)
.color(Color::rgba(1.0, 1.0, 1.0, 0.6)),
)
}