windui 0.7.0

轻量跨平台桌面 GUI:tiny-skia 渲染 + 平台原生窗口/文字(Windows: Win32+DirectWrite;macOS: Cocoa+CoreText)
Documentation
# 图片支持设计方案(windui)

- 日期:2026-06-17
- 状态:已确认,待实现
- 范围:为 windui 增加图片渲染能力,下沉为可被任意控件复用的内容原语,并预留多格式解码框架。

## 1. 目标与非目标

### 目标
- 新增图片渲染能力,支持三种来源:文件路径、嵌入字节(`include_bytes!`)、原始 RGBA 缓冲。
- 将图片能力**下沉为可复用内容原语 `ImageContent`**,使任意自绘控件可低成本嵌入图片(图标等)。
- 提供独立 `ImageView` 控件作为原语的薄包装。
- 支持多种 Fit 缩放模式(Contain/Cover/Fill/None),默认 Contain。
- 支持圆角裁剪(头像/卡片场景)。
- **预留多格式解码框架**:定义可插拔 `ImageDecoder` trait + 注册表,核心只内置 PNG(零新依赖),将来 JPEG/WebP 等由使用方注册,核心代码与 API 零破坏。

### 非目标(v1 边界,有意收窄)
- 不内置 PNG 之外的解码器(但框架就位,可由使用方扩展)。
- 不含 GIF 动图、SVG、网络加载。
- 控件接入仅做 **Button 图标** 一个示范;ListView 等其它控件本版不接(后续照抄 pattern 即可)。

## 2. 设计原则对齐

- **Widget 纯内容、不访问树**:图片的"解码 + Fit + 测量 + 绘制 + 占位 + 圆角"全部逻辑沉到 Widget 之下的 `ImageContent` 内容原语;`ImageView``Button` 图标共用同一份代码,零重复。
- **轻量零依赖红线**:核心仅用 tiny-skia 原生 PNG 解码,不引入新 crate。
- **逻辑/物理坐标双轨**:图片绘制走与图形同一条 `×scale` 物理化路径,保证高 DPI 不糊。
- **构建期解码、paint 期零解码**:资源在 `Element::image*()` 构建期一次性解码缓存,每帧只做 blit。
- **误用检测文化**`.fit()`/`.corner()` 等图片专属修饰符复用现有 `config_text_input` 的 downcast 误用检测 pattern(链到非图片控件时 debug panic,release 静默忽略)。

## 3. 架构分层

三层,逐层下沉:

```
第 1 层  render::image::Image          解码后的不可变资源(Rc<Pixmap> 缓存)
          + 解码框架(ImageDecoder / 注册表 / PngDecoder)
第 2 层  render::image::ImageContent   可复用内容原语(被嵌入其它控件的那块)
          + Canvas::draw_image 图元(SkiaCanvas 实现)
第 3 层  ui::ImageView                 独立控件(ImageContent 薄包装)
          ui::Button 图标               接入示范(内嵌 Option<ImageContent>)
          Element::image* / .fit / .corner / .icon*   Builder API
```

## 4. 第 1 层:`Image` 资源 + 解码框架

文件:`src/render/image.rs`

### 4.1 资源类型

```rust
pub struct Image {
    pixmap: Rc<Pixmap>,  // 解码后缓存,Rc 共享,paint 期零解码
    w: u32,
    h: u32,
}

impl Image {
    pub fn from_file(path: impl AsRef<Path>) -> Result<Self, ImageError>;   // 读字节 → from_bytes(格式自适配)
    pub fn from_bytes(bytes: &[u8]) -> Result<Self, ImageError>;            // 嗅探格式 + 分发解码器
    pub fn from_png_bytes(bytes: &[u8]) -> Result<Self, ImageError>;        // 便捷直通 PNG(绕过注册表)
    pub fn from_rgba(w: u32, h: u32, rgba: &[u8]) -> Result<Self, ImageError>; // 原始 RGBA8(校验 len==w*h*4)
    pub fn size(&self) -> Size;   // 固有逻辑尺寸(1 图片像素 = 1 逻辑 dp)
}
```

### 4.2 解码框架(预留扩展点)

```rust
/// 解码产物:RGBA8(非预乘),由 Image 内部转 Pixmap(预乘)。
pub struct DecodedImage { pub width: u32, pub height: u32, pub rgba: Vec<u8> }

/// 可插拔解码器。核心内置 PNG;JPEG/WebP 等由使用方注册。
pub trait ImageDecoder {
    fn probe(&self, bytes: &[u8]) -> bool;                       // 按魔数判格式
    fn decode(&self, bytes: &[u8]) -> Result<DecodedImage, ImageError>;
    fn name(&self) -> &'static str;                              // 诊断用
}

struct PngDecoder;  // v1 唯一内置,用 tiny-skia Pixmap::decode_png

/// 注册一个额外解码器(如包 image crate 的 JpegDecoder)。
pub fn register_decoder(d: Box<dyn ImageDecoder>);
```

- **注册表**:照搬 `theme` 的 thread-local 模式(UI 单线程,避免加锁),PNG 预置,保证 `from_png_*` 永远可用。
- **`from_bytes` 分发**:遍历注册表,首个 `probe` 命中的解码器负责 `decode`;无命中返回 `ImageError::UnsupportedFormat`- **扩展示例**(不进核心代码):

```rust
windui::render::image::register_decoder(Box::new(MyJpegDecoder)); // 内部包 image crate
```

### 4.3 Fit 与错误类型

```rust
#[derive(Clone, Copy, PartialEq, Eq, Default)]
pub enum Fit {
    #[default] Contain,  // 等比缩放完整显示(可能留白)
    Cover,               // 等比缩放铺满、裁掉溢出
    Fill,                // 非等比拉伸填满
    None,                // 原始像素 1:1(受 DPI 缩放)
}

#[derive(Debug)]
pub enum ImageError {
    Io(std::io::Error),       // 文件读取失败
    UnsupportedFormat,        // 无解码器能 probe 命中
    Decode(String),           // 解码器内部错误
    InvalidRgba,              // from_rgba 长度与 w*h*4 不符
}
```

## 5. 第 2 层:`ImageContent` 内容原语 + `draw_image` 图元

### 5.1 `ImageContent`(核心——被嵌入其它控件的那块)

不碰树的纯内容结构体:

```rust
pub struct ImageContent {
    image: Option<Image>,   // None = 加载失败 / 空
    fit: Fit,
}

impl ImageContent {
    pub fn new(image: Option<Image>) -> Self;       // 持有解码结果(失败传 None)
    pub fn fit(self, fit: Fit) -> Self;
    pub fn intrinsic_size(&self) -> Size;            // 供控件 measure;None 时返回占位默认尺寸
    pub fn paint_into(&self, dst: Rect, canvas: &mut dyn Canvas, style: &Style); // 供控件 paint
}
```

- **圆角不另设字段**`paint_into` 直接读 `style.corner_radius` 作裁剪半径,与核心层给背景/边框画圆角的同一个值——背景、边框、图片三者一起圆角,杜绝方角戳出圆角边框的视觉撕裂。
- `paint_into`:有图 → `canvas.draw_image(img, dst, fit, style.corner_radius)`;无图 → 画**淡灰底 + 边框占位框**(错误可见,而非静默消失)。

### 5.2 Canvas 新图元

`src/render/mod.rs` 的 `Canvas` trait 新增:

```rust
fn draw_image(&mut self, img: &Image, dst: Rect, fit: Fit, radius: f32);
```

`SkiaCanvas` 实现(`src/render/skia.rs`):
- `Pixmap::draw_pixmap``PixmapPaint { quality: FilterQuality::Bilinear, .. }` 平滑缩放。
-`fit` 计算 src→dst 的 `Transform`(缩放 + 居中平移),叠加全局 `×scale`- **始终裁剪到 dst 框**(Cover 溢出、None 超框都安全收口):复用裁剪栈,与栈顶 mask 求交。
- 圆角:`radius > 0` 时用 `rounded_rect_path` 构造圆角 mask(物理坐标,与现有 clip 同源),与当前裁剪 mask 求交后作为 `draw_pixmap` 的 mask。

> 说明:`Canvas::draw_image` 签名引用 `render::Image` 本地类型,不向控件层泄漏 tiny-skia;而 render 层(skia.rs)本就依赖 tiny-skia,故 `Image` 内包 `Pixmap` 是务实且一致的。

## 6. 第 3 层:控件与 Builder

### 6.1 `ImageView` 独立控件

文件:`src/ui/image.rs`

```rust
pub struct ImageView { content: ImageContent }

impl Widget for ImageView {
    fn measure(&self, _avail, _style, _text) -> Size { self.content.intrinsic_size() }
    fn paint(&self, _bounds, content, _focused, canvas, style) {
        self.content.paint_into(content, canvas, style);
    }
    fn as_any_mut(&mut self) -> Option<&mut dyn Any> { Some(self) } // 供 .fit()/.corner() 配置
}
```

### 6.2 Button 图标接入示范

`Button` 增加可选图标字段,证明"其它控件低成本接图片"的 pattern:

```rust
pub struct Button {
    label: String,
    icon: Option<ImageContent>,   // 新增
    state: BtnState,
    on_click: Option<ClickFn>,
}
```

- `measure`:有图标时在文字宽度基础上加 `图标宽 + 间距`,高度取 `max(文字高, 图标高)`- `paint`:图标画在文字左侧(icon rect + label rect 水平排布,整体居中)。

### 6.3 Builder API

`src/ui/mod.rs`:

```rust
Element::image(path)                         // 文件路径
Element::image_bytes(&[u8])                  // 嵌入字节(嗅探格式)
Element::image_rgba(w, h, &[u8])             // 原始 RGBA
    .fit(Fit::Cover)                         // 缩放模式(默认 Contain)
    .corner(8.0)                             // 复用现有 .corner():写 Style.corner_radius

Element::button("Save").icon_bytes(&[u8])    // Button 图标接入示范
```

- 构造函数内部调 `Image::from_*`,失败时 `ImageContent::new(None)`(占位框可见),不 panic。
- `.fit()`:图片专属修饰符,复用 `config_text_input` 的 downcast 误用检测 pattern(链到非图片控件 debug panic)。
- **圆角直接复用现有 `Element::corner()`**(写 `Style.corner_radius`),语义不变、不重载:图片控件的 `paint_into` 读同一个值作裁剪半径,背景/边框/图片一致圆角。无需新增 `.corner()` 变体或新名字。

## 7. 测量约定

- `intrinsic_size()` 返回图片像素尺寸作逻辑 dp(1 px = 1 dp,再由 DPI 缩放)。
- `Wrap` 尺寸时框 = 固有尺寸,此时 Contain == None。
- 加载失败时返回一个占位默认尺寸(如 48×48 dp),保证布局不塌陷。

## 8. 测试策略

### 单元测试
- 解码内嵌小 PNG(`include_bytes!` 一张 2×2 测试图)→ 验证 `size()`- `from_rgba` 长度校验:正确尺寸成功、错误长度返回 `InvalidRgba`- `from_bytes` 格式嗅探:PNG 命中 PngDecoder;非图片字节返回 `UnsupportedFormat`- 四种 Fit 的 src→dst 变换数学(纯函数,独立可测)。
- 圆角 mask 生成(radius>0 时角落像素被裁掉)。
- 占位 fallback:`ImageContent::new(None)``paint_into` 画出边框、`intrinsic_size` 返回默认尺寸。
- Button 带图标的 `measure`(宽度含图标 + 间距)。

### 截图验证
- `examples/image.rs`:展示三种来源 × 四种 Fit × 圆角 × 占位,配合 `--screenshot <path>` 出图肉眼核对。

### 接入验证(dev-conventions 约定)
- 接入 **showcase 控件 tab**`examples/fullshowcase.rs`),新增图片演示分区。

## 9. 文件改动清单

| 文件 | 改动 |
|---|---|
| `src/render/image.rs` | 新增:`Image` / `DecodedImage` / `ImageDecoder` / `PngDecoder` / 注册表 / `Fit` / `ImageError` |
| `src/render/mod.rs` | `Canvas` trait 新增 `draw_image`;导出 image 模块类型 |
| `src/render/skia.rs` | `SkiaCanvas` 实现 `draw_image`(draw_pixmap + Fit 变换 + 圆角 mask + 裁剪) |
| `src/ui/image.rs` | 新增:`ImageContent` 内容原语 + `ImageView` 控件 |
| `src/ui/mod.rs` | Builder:`image`/`image_bytes`/`image_rgba`/`.fit`/`.corner`/`.icon_bytes`;Button 加 icon 字段 |
| `src/lib.rs` (prelude) | 导出 `Fit``ImageView`/`Image` 等公共类型 |
| `examples/image.rs` | 新增示例 |
| `examples/fullshowcase.rs` | showcase 图片分区 |
| `docs/API_GUIDE.md` / `docs/ROADMAP.md` | 文档同步 |

## 10. 未来扩展(非本版)

- 内置或社区 JPEG/WebP 解码器(通过 `register_decoder`,零核心改动)。
- GIF 动图、SVG 矢量。
- 异步/网络图片加载与占位过渡。
- 更多控件接入图标(ListView 行、CheckBox、Tab 等,照抄 Button pattern)。