use std::cell::Cell;
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::signal::Signal;
use crate::spec::Align;
use crate::style::Style;
use crate::text::TextEngine;
use crate::ui::ImageContent;
pub const ROW_H: i32 = 36;
const PAD_X: i32 = 12;
const ICON_GAP: i32 = 8;
pub struct ListRow {
label: String,
icon: Option<ImageContent>,
group: Signal<usize>,
index: usize,
hover: bool,
bg_amt: Cell<Transition<f32>>,
bg_color: Cell<Color>,
sel: Cell<Transition<f32>>,
}
impl ListRow {
pub fn new(label: String, group: Signal<usize>, index: usize) -> Self {
let on = if group.get() == index { 1.0 } else { 0.0 };
Self {
label,
icon: None,
group,
index,
hover: false,
bg_amt: Cell::new(Transition::new(on)),
bg_color: Cell::new(Color::TRANSPARENT),
sel: Cell::new(Transition::new(on)),
}
}
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 ListRow {
fn measure(&self, avail: Size, _style: &Style, _text: &mut dyn TextEngine) -> Size {
Size::new(avail.w.max(0), ROW_H)
}
fn paint(
&self,
bounds: Rect,
_content: Rect,
focused: bool,
enabled: bool,
canvas: &mut dyn Canvas,
style: &Style,
) {
let th = crate::theme::current();
let (pal, lt) = (&th.palette, &th.list);
let sel = self.selected();
let (x, y, w, h) = (
bounds.x as f32,
bounds.y as f32,
bounds.w as f32,
bounds.h as f32,
);
let want = if sel {
Some(lt.selected_bg(pal))
} else if self.hover {
Some(lt.hover_bg(pal))
} else {
None
};
if let Some(c) = want {
self.bg_color.set(c);
}
let mut bg = self.bg_amt.get();
let bg_target = if want.is_some() { 1.0 } else { 0.0 };
if bg.target() != bg_target {
bg.retarget(bg_target, th.anim.fast(), Easing::EaseOut);
}
let bg_amt = bg.animate();
self.bg_amt.set(bg);
if bg_amt > 0.0 {
canvas.fill_rect(
x,
y,
w,
h,
&Paint::fill(self.bg_color.get().scale_alpha(bg_amt)),
);
}
let mut selt = self.sel.get();
let sel_target = if sel && enabled { 1.0 } else { 0.0 };
if selt.target() != sel_target {
selt.retarget(sel_target, th.anim.normal(), Easing::EaseOut);
}
let sel_amt = selt.animate();
self.sel.set(selt);
if sel_amt > 0.0 {
canvas.fill_rect(x, y, 3.0, h, &Paint::fill(pal.accent.scale_alpha(sel_amt)));
}
let vstate = if !enabled {
VisualState::Disabled
} else {
self.visual_state()
};
let mut text_x = bounds.x + PAD_X;
if let Some(icon) = &self.icon {
let side = (bounds.h - 14).max(0);
let iy = bounds.y + (bounds.h - side) / 2;
let istyle = Style {
corner_radius: 0.0,
..style.clone()
};
icon.paint_into(
Rect::new(bounds.x + PAD_X, iy, side, side),
canvas,
&istyle,
vstate,
);
text_x = bounds.x + PAD_X + side + ICON_GAP;
}
let color = if !enabled {
pal.text_disabled
} else if sel {
lt.selected_text(pal)
} else {
lt.text(pal)
};
let tw = (bounds.right() - PAD_X - text_x).max(0);
let tr = Rect::new(text_x, bounds.y, tw, bounds.h);
canvas.draw_text(
&self.label,
tr,
color,
Align::Start,
style.font_family.as_deref(),
style.font_size,
);
let _ = focused;
}
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::render::SkiaCanvas;
use crate::signal::signal;
use tiny_skia::Pixmap;
#[test]
fn row_with_icon_paints_icon_area() {
let group = signal(0);
let red = Image::from_rgba(4, 4, &[255u8, 0, 0, 255].repeat(4 * 4)).unwrap();
let row = ListRow::new("Inbox".into(), group, 0)
.with_icon(ImageContent::new(Some(red)).fit(Fit::Fill));
let mut pm = Pixmap::new(200, ROW_H as u32).unwrap();
pm.fill(tiny_skia::Color::WHITE);
{
let mut c = SkiaCanvas::new(&mut pm);
row.paint(
Rect::new(0, 0, 200, ROW_H),
Rect::new(0, 0, 200, ROW_H),
false,
true,
&mut c,
&Style::default(),
);
}
let side = ROW_H - 14;
let p = pm
.pixel((PAD_X + side / 2) as u32, (ROW_H / 2) as u32)
.unwrap();
assert!(
p.red() > 180 && p.green() < 90,
"行图标应绘制红色,实得 ({},{},{})",
p.red(),
p.green(),
p.blue()
);
}
#[test]
fn row_visual_state_tracks_selection() {
let group = signal(1);
let selected = ListRow::new("A".into(), group, 1);
assert_eq!(selected.visual_state(), VisualState::Selected);
let other = ListRow::new("B".into(), group, 2);
assert_eq!(other.visual_state(), VisualState::Normal);
}
}