use std::cell::Cell;
use std::rc::Rc;
use crate::anim::{Easing, Transition};
use crate::core::{EventCtx, Widget};
use crate::event::{Event, Key, PointerKind};
use crate::geometry::{Color, Rect, Size};
use crate::render::image::VisualState;
use crate::render::{Canvas, Paint};
use crate::spec::Align;
use crate::style::Style;
use crate::text::TextEngine;
use crate::ui::ImageContent;
const SCROLLBAR_HIT_W: i32 = 10;
const SCROLLBAR_MIN_THUMB: f32 = 24.0;
const TAB_ICON_GAP: i32 = 6;
#[derive(Default)]
pub struct ScrollWidget {
dragging: bool,
start_y: i32,
start_scroll: i32,
}
impl Widget for ScrollWidget {
fn on_event(&mut self, ctx: &mut EventCtx, ev: &Event) -> bool {
let Event::Pointer(p) = ev else { return false };
match p.kind {
PointerKind::Wheel(delta) => {
ctx.scroll_by(-delta * 48 / 120);
true
}
PointerKind::Down => {
let b = ctx.bounds();
let (scroll_y, content_h, view_h) = ctx.scroll_metrics();
if content_h > view_h && p.pos.x >= b.right() - SCROLLBAR_HIT_W {
self.dragging = true;
self.start_y = p.pos.y;
self.start_scroll = scroll_y;
ctx.capture();
true
} else {
false
}
}
PointerKind::Move if self.dragging => {
let (_, content_h, view_h) = ctx.scroll_metrics();
if view_h > 0 && content_h > view_h {
let max_scroll = content_h - view_h;
let thumb_h =
(view_h as f32 * view_h as f32 / content_h as f32).max(SCROLLBAR_MIN_THUMB);
let travel = (view_h as f32 - thumb_h).max(1.0);
let dy = p.pos.y - self.start_y;
let delta = (dy as f32 * max_scroll as f32 / travel) as i32;
ctx.set_scroll((self.start_scroll + delta).clamp(0, max_scroll));
}
true
}
PointerKind::Up if self.dragging => {
self.dragging = false;
ctx.release_capture();
true
}
_ => false,
}
}
}
pub struct ModalScrim;
impl Widget for ModalScrim {
fn on_event(&mut self, _ctx: &mut EventCtx, ev: &Event) -> bool {
matches!(ev, Event::Pointer(_))
}
}
pub struct TabButton {
label: String,
icon: Option<ImageContent>,
group: Rc<Cell<usize>>,
index: usize,
hover: bool,
color_anim: Cell<Transition<Color>>,
primed: Cell<bool>,
ind: Cell<Transition<f32>>,
}
impl TabButton {
pub fn new(label: String, group: Rc<Cell<usize>>, index: usize) -> Self {
let ind = if group.get() == index { 1.0 } else { 0.0 };
Self {
label,
icon: None,
group,
index,
hover: false,
color_anim: Cell::new(Transition::new(Color::rgba(0, 0, 0, 0))),
primed: Cell::new(false),
ind: Cell::new(Transition::new(ind)),
}
}
pub fn with_icon(mut self, icon: ImageContent) -> Self {
self.icon = Some(icon);
self
}
fn selected(&self) -> bool {
self.group.get() == self.index
}
fn visual_state(&self) -> VisualState {
if self.selected() {
VisualState::Selected
} else if self.hover {
VisualState::Hover
} else {
VisualState::Normal
}
}
}
impl Widget for TabButton {
fn measure(&self, _avail: Size, style: &Style, text: &mut dyn TextEngine) -> Size {
let t = text.measure(&self.label, style.font_family.as_deref(), style.font_size, None);
let icon_extra = if self.icon.is_some() { t.h + TAB_ICON_GAP } else { 0 };
Size::new(t.w + 24 + icon_extra, t.h + 16)
}
fn paint(&self, bounds: Rect, _content: Rect, _focused: bool, enabled: bool, canvas: &mut dyn Canvas, style: &Style) {
let th = crate::theme::current();
let (pal, tab) = (&th.palette, &th.tab);
let sel = self.selected();
let target_color = if !enabled {
pal.text_disabled
} else if sel {
tab.accent(pal)
} else if self.hover {
tab.hover(pal)
} else {
tab.inactive(pal)
};
let mut canim = self.color_anim.get();
if !self.primed.get() {
canim = Transition::new(target_color);
self.primed.set(true);
} else if canim.target() != target_color {
canim.retarget(target_color, th.anim.fast(), Easing::EaseOut);
}
let color = canim.animate();
self.color_anim.set(canim);
let vstate = if !enabled { VisualState::Disabled } else { self.visual_state() };
if let Some(icon) = &self.icon {
let ts = canvas.measure_text(&self.label, style.font_family.as_deref(), style.font_size);
let ih = ts.h;
let total_w = ih + TAB_ICON_GAP + ts.w;
let sx = bounds.x + ((bounds.w - total_w) / 2).max(0);
let iy = bounds.y + ((bounds.h - ih) / 2).max(0);
let istyle = Style { corner_radius: 0.0, ..style.clone() };
icon.paint_into(Rect::new(sx, iy, ih, ih), canvas, &istyle, vstate);
let tr = Rect::new(sx + ih + TAB_ICON_GAP, bounds.y, ts.w + 2, bounds.h);
canvas.draw_text(&self.label, tr, color, Align::Start, style.font_family.as_deref(), style.font_size);
} else {
canvas.draw_text(&self.label, bounds, color, Align::Center, style.font_family.as_deref(), style.font_size);
}
let mut ind = self.ind.get();
let ind_target = if sel && enabled { 1.0 } else { 0.0 };
if ind.target() != ind_target {
ind.retarget(ind_target, th.anim.normal(), Easing::EaseOut);
}
let amount = ind.animate();
self.ind.set(ind);
if amount > 0.0 {
let full = (bounds.w - 16) as f32;
let bw = full * amount;
let cx = bounds.x as f32 + bounds.w as f32 / 2.0;
canvas.fill_round_rect(cx - bw / 2.0, (bounds.y + bounds.h - 3) as f32, bw, 3.0, 1.5, &Paint::fill(tab.accent(pal).scale_alpha(amount)));
}
}
fn on_event(&mut self, ctx: &mut EventCtx, ev: &Event) -> bool {
match ev {
Event::Pointer(p) => match p.kind {
PointerKind::Enter => {
self.hover = true;
ctx.mark_dirty();
true
}
PointerKind::Leave => {
self.hover = false;
ctx.mark_dirty();
true
}
PointerKind::Down => {
ctx.request_focus();
true
}
PointerKind::Up => {
if ctx.bounds().contains(p.pos) {
self.group.set(self.index);
ctx.mark_dirty();
}
true
}
_ => false,
},
Event::Key(k) if k.pressed && (k.key == Key::Enter || k.key == Key::Space) => {
self.group.set(self.index);
ctx.mark_dirty();
true
}
_ => false,
}
}
fn focusable(&self) -> bool {
true
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::render::image::{Fit, Image};
use crate::text::NullTextEngine;
#[test]
fn tab_icon_widens_measure() {
let g = Rc::new(Cell::new(0));
let style = Style::default();
let mut te = NullTextEngine;
let w0 = TabButton::new("Home".into(), g.clone(), 0).measure(Size::ZERO, &style, &mut te).w;
let red = Image::from_rgba(4, 4, &[255u8, 0, 0, 255].repeat(4 * 4)).unwrap();
let iconed =
TabButton::new("Home".into(), g, 0).with_icon(ImageContent::new(Some(red)).fit(Fit::Fill));
let w1 = iconed.measure(Size::ZERO, &style, &mut te).w;
assert!(w1 > w0, "带图标标签应更宽:w0={w0}, w1={w1}");
}
#[test]
fn tab_visual_state_tracks_selection() {
let g = Rc::new(Cell::new(2));
assert_eq!(TabButton::new("A".into(), g.clone(), 2).visual_state(), VisualState::Selected);
assert_eq!(TabButton::new("B".into(), g, 0).visual_state(), VisualState::Normal);
}
}