use std::any::Any;
use std::cell::RefCell;
use std::path::Path;
use crate::core::{EventCtx, Widget};
use crate::event::Event;
use crate::geometry::{Color, Rect, Size};
use crate::render::image::{Fit, Image, VisualState, PLACEHOLDER_SIZE};
use crate::render::{Canvas, Paint};
use crate::style::Style;
use crate::text::TextEngine;
const PLACEHOLDER_BG: Color = Color::rgb(0xEE, 0xEE, 0xEE);
const PLACEHOLDER_BORDER: Color = Color::rgb(0xCC, 0xCC, 0xCC);
struct Layer {
raw: Image,
tinted: RefCell<Option<Image>>,
}
impl Layer {
fn new(raw: Image) -> Self {
Self {
raw,
tinted: RefCell::new(None),
}
}
fn resolve(&self, tint: Option<Color>) -> Image {
match tint {
None => self.raw.clone(),
Some(c) => self
.tinted
.borrow_mut()
.get_or_insert_with(|| self.raw.tinted(c))
.clone(),
}
}
}
pub struct ImageContent {
base: Option<Layer>,
overrides: Vec<(VisualState, Layer)>,
fit: Fit,
tint: Option<Color>,
}
impl ImageContent {
pub fn new(image: Option<Image>) -> Self {
Self {
base: image.map(Layer::new),
overrides: Vec::new(),
fit: Fit::default(),
tint: None,
}
}
pub fn from_bytes(bytes: &[u8]) -> Self {
Self::new(Image::from_bytes(bytes).ok())
}
pub fn from_file(path: impl AsRef<Path>) -> Self {
Self::new(Image::from_file(path).ok())
}
#[cfg(feature = "svg")]
pub fn from_svg_bytes(bytes: &[u8], target_width: Option<u32>) -> Self {
Self::new(Image::from_svg_bytes(bytes, target_width).ok())
}
pub fn from_rgba(w: u32, h: u32, rgba: &[u8]) -> Self {
Self::new(Image::from_rgba(w, h, rgba).ok())
}
pub fn fit(mut self, fit: Fit) -> Self {
self.fit = fit;
self
}
pub fn tint(mut self, color: Color) -> Self {
self.set_tint(color);
self
}
pub fn on_state(mut self, state: VisualState, image: Image) -> Self {
self.overrides.retain(|(s, _)| *s != state);
self.overrides.push((state, Layer::new(image)));
self
}
pub fn set_tint(&mut self, color: Color) {
self.tint = Some(color);
if let Some(l) = &self.base {
*l.tinted.borrow_mut() = None;
}
for (_, l) in &self.overrides {
*l.tinted.borrow_mut() = None;
}
}
pub fn set_fit(&mut self, fit: Fit) {
self.fit = fit;
}
pub fn fit_mode(&self) -> Fit {
self.fit
}
pub fn is_loaded(&self) -> bool {
self.base.is_some()
}
fn layer_for(&self, state: VisualState) -> Option<&Layer> {
self.overrides
.iter()
.find(|(s, _)| *s == state)
.map(|(_, l)| l)
.or(self.base.as_ref())
}
pub fn intrinsic_size(&self) -> Size {
match &self.base {
Some(l) => l.raw.size(),
None => Size::new(PLACEHOLDER_SIZE, PLACEHOLDER_SIZE),
}
}
pub fn paint_into(
&self,
dst: Rect,
canvas: &mut dyn Canvas,
style: &Style,
state: VisualState,
) {
if dst.is_empty() {
return;
}
let radius = style.corner_radius;
match self.layer_for(state) {
Some(layer) => {
let img = layer.resolve(self.tint);
canvas.draw_image(&img, dst, self.fit, radius, state.opacity());
}
None => {
let (x, y, w, h) = (dst.x as f32, dst.y as f32, dst.w as f32, dst.h as f32);
canvas.fill_round_rect(x, y, w, h, radius, &Paint::fill(PLACEHOLDER_BG));
canvas.stroke_round_rect(x, y, w, h, radius, 1.0, &Paint::fill(PLACEHOLDER_BORDER));
}
}
}
}
pub struct ImageView {
content: ImageContent,
}
impl ImageView {
pub fn new(image: Option<Image>) -> Self {
Self {
content: ImageContent::new(image),
}
}
pub fn from_content(content: ImageContent) -> Self {
Self { content }
}
pub fn set_fit(&mut self, fit: Fit) {
self.content.set_fit(fit);
}
pub fn set_tint(&mut self, color: Color) {
self.content.set_tint(color);
}
}
impl Widget for ImageView {
fn measure(&self, _avail: Size, _style: &Style, _text: &mut dyn TextEngine) -> Size {
self.content.intrinsic_size()
}
fn paint(
&self,
_bounds: Rect,
content: Rect,
_focused: bool,
_enabled: bool,
canvas: &mut dyn Canvas,
style: &Style,
) {
self.content
.paint_into(content, canvas, style, VisualState::Normal);
}
fn on_event(&mut self, _ctx: &mut EventCtx, _ev: &Event) -> bool {
false
}
fn as_any_mut(&mut self) -> Option<&mut dyn Any> {
Some(self)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::render::SkiaCanvas;
use tiny_skia::Pixmap;
#[test]
fn loaded_content_reports_pixel_size() {
let img = Image::from_rgba(6, 8, &[0u8; 6 * 8 * 4]).unwrap();
let c = ImageContent::new(Some(img));
assert!(c.is_loaded());
assert_eq!(c.intrinsic_size(), Size::new(6, 8));
}
#[test]
fn missing_content_uses_placeholder_size() {
let c = ImageContent::new(None);
assert!(!c.is_loaded());
assert_eq!(
c.intrinsic_size(),
Size::new(PLACEHOLDER_SIZE, PLACEHOLDER_SIZE)
);
}
#[test]
fn state_override_picks_dedicated_image() {
let base = Image::from_rgba(4, 4, &[10u8; 4 * 4 * 4]).unwrap();
let disabled = Image::from_rgba(8, 8, &[20u8; 8 * 8 * 4]).unwrap();
let c = ImageContent::new(Some(base)).on_state(VisualState::Disabled, disabled);
assert_eq!(
c.layer_for(VisualState::Disabled).unwrap().raw.size(),
Size::new(8, 8)
);
assert_eq!(
c.layer_for(VisualState::Hover).unwrap().raw.size(),
Size::new(4, 4)
);
}
#[test]
fn placeholder_paints_visible_box() {
let mut pm = Pixmap::new(60, 60).unwrap();
pm.fill(tiny_skia::Color::WHITE);
let c = ImageContent::new(None);
{
let mut canvas = SkiaCanvas::new(&mut pm);
c.paint_into(
Rect::new(10, 10, 40, 40),
&mut canvas,
&Style::default(),
VisualState::Normal,
);
}
let p = pm.pixel(30, 30).unwrap();
assert!(
p.red() < 250 && p.green() < 250 && p.blue() < 250,
"占位框应可见,实得 ({},{},{})",
p.red(),
p.green(),
p.blue()
);
}
#[test]
fn disabled_state_dims_image() {
let mut pm = Pixmap::new(40, 40).unwrap();
pm.fill(tiny_skia::Color::WHITE);
let img = Image::from_rgba(4, 4, &[255u8, 0, 0, 255].repeat(4 * 4)).unwrap();
let c = ImageContent::new(Some(img)).fit(Fit::Fill);
{
let mut canvas = SkiaCanvas::new(&mut pm);
c.paint_into(
Rect::new(5, 5, 30, 30),
&mut canvas,
&Style::default(),
VisualState::Disabled,
);
}
let p = pm.pixel(20, 20).unwrap();
assert!(
p.green() > 120 && p.blue() > 120,
"禁用应置灰混白,实得 g={} b={}",
p.green(),
p.blue()
);
}
#[test]
fn button_icon_widens_measure() {
use crate::text::NullTextEngine;
use crate::ui::Button;
let style = Style::default();
let mut te = NullTextEngine;
let plain = Button::new("OK".into());
let w0 = plain.measure(Size::ZERO, &style, &mut te).w;
let mut iconed = Button::new("OK".into());
iconed.set_icon(ImageContent::new(Image::from_rgba(4, 4, &[0u8; 64]).ok()));
let w1 = iconed.measure(Size::ZERO, &style, &mut te).w;
assert!(w1 > w0, "带图标按钮应更宽:w0={w0}, w1={w1}");
}
}