use crate::input::{InputCoordinator, Sense, WidgetKind};
use crate::input::core::coordinator::LayerId;
use crate::render::RenderContext;
use crate::types::{CompositeId, Rect};
#[derive(Debug, Clone, Copy, Default)]
pub struct BodyScrollState {
pub offset_x: f64,
pub offset_y: f64,
pub content_w: f64,
pub content_h: f64,
}
impl BodyScrollState {
pub fn clamp(&mut self, body_w: f64, body_h: f64) {
let max_x = (self.content_w - body_w).max(0.0);
let max_y = (self.content_h - body_h).max(0.0);
if self.offset_x > max_x { self.offset_x = max_x; }
if self.offset_y > max_y { self.offset_y = max_y; }
if self.offset_x < 0.0 { self.offset_x = 0.0; }
if self.offset_y < 0.0 { self.offset_y = 0.0; }
}
pub fn overflows(&self, body_w: f64, body_h: f64) -> Overflow {
Overflow {
horizontal: self.content_w > body_w + 0.5,
vertical: self.content_h > body_h + 0.5,
}
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct Overflow {
pub horizontal: bool,
pub vertical: bool,
}
impl Overflow {
pub fn any(self) -> bool { self.horizontal || self.vertical }
}
pub const CHEVRON_STRIP: f64 = 26.0;
pub const CHEVRON_STEP_PX: f64 = 80.0;
pub fn register_chevrons_helper(
coord: &mut InputCoordinator,
host_id: &CompositeId,
body: Rect,
state: &BodyScrollState,
layer: &LayerId,
) {
if body.width <= 0.0 || body.height <= 0.0 { return; }
let _ = layer; let o = state.overflows(body.width, body.height);
if !o.any() { return; }
let inset_x = if o.horizontal { CHEVRON_STRIP } else { 0.0 };
let inset_y = if o.vertical { CHEVRON_STRIP } else { 0.0 };
if o.vertical {
let w = (body.width - inset_x * 2.0).max(0.0);
let up = Rect::new(body.x + inset_x, body.y, w, CHEVRON_STRIP);
let dn = Rect::new(body.x + inset_x, body.y + body.height - CHEVRON_STRIP, w, CHEVRON_STRIP);
coord.register_child(host_id, format!("{}:chevron_up", host_id.0.0),
WidgetKind::Button, up, Sense::CLICK | Sense::HOVER);
coord.register_child(host_id, format!("{}:chevron_down", host_id.0.0),
WidgetKind::Button, dn, Sense::CLICK | Sense::HOVER);
}
if o.horizontal {
let h = (body.height - inset_y * 2.0).max(0.0);
let lf = Rect::new(body.x, body.y + inset_y, CHEVRON_STRIP, h);
let rt = Rect::new(body.x + body.width - CHEVRON_STRIP, body.y + inset_y, CHEVRON_STRIP, h);
coord.register_child(host_id, format!("{}:chevron_left", host_id.0.0),
WidgetKind::Button, lf, Sense::CLICK | Sense::HOVER);
coord.register_child(host_id, format!("{}:chevron_right", host_id.0.0),
WidgetKind::Button, rt, Sense::CLICK | Sense::HOVER);
}
}
pub fn draw_chevrons_helper(
ctx: &mut dyn RenderContext,
body: Rect,
state: &BodyScrollState,
bg_color: &str,
arrow_color: &str,
) {
use crate::ui::widgets::atomic::chevron::{
draw_chevron,
settings::ChevronSettings,
types::{ChevronDirection, ChevronUseCase, ChevronView, ChevronVisualKind,
HitAreaPolicy, PlacementPolicy, VisibilityPolicy},
};
if body.width <= 0.0 || body.height <= 0.0 { return; }
let o = state.overflows(body.width, body.height);
if !o.any() { return; }
let chev_settings = ChevronSettings::default();
let _ = arrow_color;
if o.vertical {
let max_v = (state.content_h - body.height).max(0.0);
let has_back = state.offset_y > 0.5;
let has_fwd = state.offset_y < max_v - 0.5;
let up = Rect::new(body.x, body.y, body.width, CHEVRON_STRIP);
let dn = Rect::new(body.x, body.y + body.height - CHEVRON_STRIP, body.width, CHEVRON_STRIP);
ctx.set_fill_color(bg_color);
ctx.fill_rect(up.x, up.y, up.width, up.height);
ctx.fill_rect(dn.x, dn.y, dn.width, dn.height);
let v_up = ChevronView { direction: ChevronDirection::Up, use_case: ChevronUseCase::PixelScrollStep,
visibility: VisibilityPolicy::WhenOverflow { has_more: has_back },
placement: PlacementPolicy::Overlay, hit_area: HitAreaPolicy::Visual,
visual_kind: ChevronVisualKind::Stroked, ..Default::default() };
let v_dn = ChevronView { direction: ChevronDirection::Down, use_case: ChevronUseCase::PixelScrollStep,
visibility: VisibilityPolicy::WhenOverflow { has_more: has_fwd },
placement: PlacementPolicy::Overlay, hit_area: HitAreaPolicy::Visual,
visual_kind: ChevronVisualKind::Stroked, ..Default::default() };
draw_chevron(ctx, up, &v_up, &chev_settings);
draw_chevron(ctx, dn, &v_dn, &chev_settings);
}
if o.horizontal {
let max_h = (state.content_w - body.width).max(0.0);
let has_back = state.offset_x > 0.5;
let has_fwd = state.offset_x < max_h - 0.5;
let lf = Rect::new(body.x, body.y, CHEVRON_STRIP, body.height);
let rt = Rect::new(body.x + body.width - CHEVRON_STRIP, body.y, CHEVRON_STRIP, body.height);
ctx.set_fill_color(bg_color);
ctx.fill_rect(lf.x, lf.y, lf.width, lf.height);
ctx.fill_rect(rt.x, rt.y, rt.width, rt.height);
let v_lf = ChevronView { direction: ChevronDirection::Left, use_case: ChevronUseCase::PixelScrollStep,
visibility: VisibilityPolicy::WhenOverflow { has_more: has_back },
placement: PlacementPolicy::Overlay, hit_area: HitAreaPolicy::Visual,
visual_kind: ChevronVisualKind::Stroked, ..Default::default() };
let v_rt = ChevronView { direction: ChevronDirection::Right, use_case: ChevronUseCase::PixelScrollStep,
visibility: VisibilityPolicy::WhenOverflow { has_more: has_fwd },
placement: PlacementPolicy::Overlay, hit_area: HitAreaPolicy::Visual,
visual_kind: ChevronVisualKind::Stroked, ..Default::default() };
draw_chevron(ctx, lf, &v_lf, &chev_settings);
draw_chevron(ctx, rt, &v_rt, &chev_settings);
}
}
pub fn step_chevron(state: &mut BodyScrollState, axis: ChevronAxis, body_w: f64, body_h: f64) {
match axis {
ChevronAxis::Up => state.offset_y -= CHEVRON_STEP_PX,
ChevronAxis::Down => state.offset_y += CHEVRON_STEP_PX,
ChevronAxis::Left => state.offset_x -= CHEVRON_STEP_PX,
ChevronAxis::Right => state.offset_x += CHEVRON_STEP_PX,
}
state.clamp(body_w, body_h);
}
#[derive(Debug, Clone, Copy)]
pub enum ChevronAxis { Up, Down, Left, Right }
pub const SCROLLBAR_THICKNESS: f64 = 8.0;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ScrollAxis { Vertical, Horizontal }
pub fn register_scrollbar_helper(
coord: &mut InputCoordinator,
host_id: &CompositeId,
body: Rect,
state: &BodyScrollState,
axis: ScrollAxis,
layer: &LayerId,
) -> Option<Rect> {
if body.width <= 0.0 || body.height <= 0.0 { return None; }
let _ = layer;
let o = state.overflows(body.width, body.height);
let track = match axis {
ScrollAxis::Vertical => {
if !o.vertical { return None; }
Rect::new(body.x + body.width - SCROLLBAR_THICKNESS, body.y, SCROLLBAR_THICKNESS, body.height)
}
ScrollAxis::Horizontal => {
if !o.horizontal { return None; }
Rect::new(body.x, body.y + body.height - SCROLLBAR_THICKNESS, body.width, SCROLLBAR_THICKNESS)
}
};
coord.register_child(host_id, format!("{}:scrollbar_track", host_id.0.0),
WidgetKind::ScrollbarTrack, track, Sense::CLICK);
coord.register_child(host_id, format!("{}:scrollbar_handle", host_id.0.0),
WidgetKind::ScrollbarHandle, track, Sense::DRAG | Sense::HOVER);
Some(track)
}
pub fn draw_scrollbar_helper(
ctx: &mut dyn RenderContext,
body: Rect,
state: &BodyScrollState,
axis: ScrollAxis,
track_color: &str,
thumb_color: &str,
) {
if body.width <= 0.0 || body.height <= 0.0 { return; }
let o = state.overflows(body.width, body.height);
match axis {
ScrollAxis::Vertical => {
if !o.vertical { return; }
let track_x = body.x + body.width - SCROLLBAR_THICKNESS;
let track_y = body.y;
let track_h = body.height;
let max_off = (state.content_h - body.height).max(0.0);
let visible = (body.height / state.content_h).clamp(0.0, 1.0);
let thumb_h = (track_h * visible).max(20.0);
let thumb_y = track_y + (track_h - thumb_h) * (state.offset_y / max_off.max(1.0));
ctx.set_fill_color(track_color);
ctx.fill_rect(track_x, track_y, SCROLLBAR_THICKNESS, track_h);
ctx.set_fill_color(thumb_color);
ctx.fill_rect(track_x, thumb_y, SCROLLBAR_THICKNESS, thumb_h);
}
ScrollAxis::Horizontal => {
if !o.horizontal { return; }
let track_x = body.x;
let track_y = body.y + body.height - SCROLLBAR_THICKNESS;
let track_w = body.width;
let max_off = (state.content_w - body.width).max(0.0);
let visible = (body.width / state.content_w).clamp(0.0, 1.0);
let thumb_w = (track_w * visible).max(20.0);
let thumb_x = track_x + (track_w - thumb_w) * (state.offset_x / max_off.max(1.0));
ctx.set_fill_color(track_color);
ctx.fill_rect(track_x, track_y, track_w, SCROLLBAR_THICKNESS);
ctx.set_fill_color(thumb_color);
ctx.fill_rect(thumb_x, track_y, thumb_w, SCROLLBAR_THICKNESS);
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct CompressFactor {
pub sx: f64,
pub sy: f64,
}
impl Default for CompressFactor {
fn default() -> Self { Self { sx: 1.0, sy: 1.0 } }
}
impl CompressFactor {
pub fn one() -> Self { Self { sx: 1.0, sy: 1.0 } }
pub fn compresses(&self) -> bool { self.sx < 1.0 || self.sy < 1.0 }
}
pub fn compute_compress_factor(content_w: f64, content_h: f64, body: Rect, min_factor: f64) -> CompressFactor {
if body.width <= 0.0 || body.height <= 0.0 || content_w <= 0.0 || content_h <= 0.0 {
return CompressFactor::one();
}
let sx = if content_w > body.width { (body.width / content_w).max(min_factor) } else { 1.0 };
let sy = if content_h > body.height { (body.height / content_h).max(min_factor) } else { 1.0 };
CompressFactor { sx, sy }
}