use std::collections::HashMap;
use crate::anim::Anim;
use crate::clipboard::{Clipboard, MemoryClipboard};
use crate::id::WidgetId;
use crate::input::EditorState;
const SCROLLBAR_HOLD_SECS: f64 = 0.8;
const SCROLLBAR_FADE_SECS: f64 = 0.3;
#[derive(Debug, Clone, Copy, Default)]
struct Scroll {
offset_y: f32,
last_change: f64,
seen: u64,
}
pub struct FrameState {
now: f64,
scroll: HashMap<WidgetId, Scroll>,
pub reduced_motion: bool,
pub(crate) hovered: HashMap<WidgetId, f64>,
pub(crate) active: Option<WidgetId>,
pub(crate) focus: Option<WidgetId>,
pub(crate) focus_visible: bool,
pub(crate) pointer: Option<(f32, f32)>,
pub(crate) anims: HashMap<WidgetId, Anim>,
pub(crate) editors: HashMap<WidgetId, EditorState>,
pub(crate) overlays: Vec<WidgetId>,
pub(crate) overlay_opened: HashMap<WidgetId, f64>,
pub(crate) clipboard: Box<dyn Clipboard>,
pub(crate) frame_no: u64,
}
impl Default for FrameState {
fn default() -> Self {
Self {
now: 0.0,
scroll: HashMap::new(),
reduced_motion: false,
hovered: HashMap::new(),
active: None,
focus: None,
focus_visible: false,
pointer: None,
anims: HashMap::new(),
editors: HashMap::new(),
overlays: Vec::new(),
overlay_opened: HashMap::new(),
clipboard: Box::new(MemoryClipboard::default()),
frame_no: 0,
}
}
}
impl FrameState {
pub fn new() -> Self {
Self::default()
}
pub fn tick(&mut self, now_seconds: f64) {
self.now = now_seconds;
}
pub fn now(&self) -> f64 {
self.now
}
pub fn is_hovered(&self, id: WidgetId) -> bool {
self.hovered.contains_key(&id)
}
pub(crate) fn hovered_for(&self, id: WidgetId) -> Option<f64> {
self.hovered.get(&id).map(|t| self.now - t)
}
pub fn overlay_open(&self, id: WidgetId) -> bool {
self.overlays.contains(&id)
}
pub(crate) fn open_overlay(&mut self, id: WidgetId) {
if !self.overlays.contains(&id) {
self.overlays.push(id);
self.overlay_opened.insert(id, self.now);
}
}
pub(crate) fn close_overlay(&mut self, id: WidgetId) {
self.overlays.retain(|o| *o != id);
self.overlay_opened.remove(&id);
}
pub fn is_active(&self, id: WidgetId) -> bool {
self.active == Some(id)
}
pub fn focused(&self) -> Option<WidgetId> {
self.focus
}
pub fn set_focus(&mut self, id: Option<WidgetId>) {
self.focus = id;
self.focus_visible = id.is_some();
}
pub fn set_clipboard(&mut self, clipboard: Box<dyn Clipboard>) {
self.clipboard = clipboard;
}
pub fn scroll_by(&mut self, id: WidgetId, dy: f32) {
let entry = self.scroll.entry(id).or_default();
entry.offset_y += dy;
entry.last_change = self.now;
}
pub fn scroll_offset(&self, id: WidgetId) -> f32 {
self.scroll.get(&id).map_or(0.0, |s| s.offset_y)
}
pub(crate) fn clamp_scroll(&mut self, id: WidgetId, max: f32) -> f32 {
let frame_no = self.frame_no;
match self.scroll.get_mut(&id) {
Some(s) => {
s.offset_y = s.offset_y.clamp(0.0, max.max(0.0));
s.seen = frame_no;
s.offset_y
}
None => 0.0,
}
}
pub(crate) fn gc_scroll(&mut self, frame_no: u64) {
self.scroll.retain(|_, s| s.seen == frame_no);
}
pub(crate) fn scrollbar_alpha(&self, id: WidgetId) -> f32 {
let Some(s) = self.scroll.get(&id) else {
return 0.0;
};
let age = self.now - s.last_change;
if age <= SCROLLBAR_HOLD_SECS {
1.0
} else if self.reduced_motion {
0.0
} else {
let t = (age - SCROLLBAR_HOLD_SECS) / SCROLLBAR_FADE_SECS;
#[expect(clippy::cast_possible_truncation, reason = "alpha fits in f32")]
{
(1.0 - t).clamp(0.0, 1.0) as f32
}
}
}
pub(crate) fn scrollbar_animating(&self, id: WidgetId) -> bool {
if self.reduced_motion {
return false;
}
self.scroll.get(&id).is_some_and(|s| {
let age = self.now - s.last_change;
age > 0.0 && age <= SCROLLBAR_HOLD_SECS + SCROLLBAR_FADE_SECS
})
}
}