#[cfg(widgets_unstripped)]
use crate::core::HorizontalAlignment;
use crate::core::{Color, Point, Rect};
#[cfg(widgets_unstripped)]
use crate::event::{Event, EventHandler};
use crate::render::RenderContext;
#[cfg(widgets_unstripped)]
use crate::widget::capability::coercion::{expect_f64, expect_string};
#[cfg(widgets_unstripped)]
use crate::widget::capability::properties_trait::{base_property_get, base_property_set};
#[cfg(widgets_unstripped)]
use crate::widget::capability::types::{CapabilityAccessError, CapabilityValue};
#[cfg(widgets_unstripped)]
use crate::widget::capability::WidgetProperties;
#[cfg(widgets_unstripped)]
use crate::widget::icon_source::IconSource;
#[cfg(widgets_unstripped)]
use crate::widget::{BaseWidget, Draw, Widget, WidgetKind};
#[cfg(widgets_unstripped)]
use crate::{impl_widget_property_hooks, property_names_of};
#[path = "icon_names.rs"]
mod icon_names;
pub use icon_names::IconName;
pub fn draw_icon_at(ctx: &mut RenderContext, rect: Rect, color: Color, name: IconName) {
if rect.width == 0 || rect.height == 0 {
return;
}
#[cfg(feature = "icons")]
if let Some(data) = name.data_opt() {
if let Err(error) = draw_outline_in(ctx, rect, color, &data) {
log::error!("icon '{}' could not be painted: {error:?}", data.name);
}
return;
}
draw_fallback_in(ctx, rect, color, name.as_str());
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IconRef<'a> {
Builtin(IconName),
StaticGeometry(&'a IconData),
}
impl<'a> From<&'a IconData> for IconRef<'a> {
fn from(data: &'a IconData) -> Self {
Self::StaticGeometry(data)
}
}
impl From<IconName> for IconRef<'_> {
fn from(name: IconName) -> Self {
Self::Builtin(name)
}
}
impl From<crate::widget::icon_source::IconSource> for IconRef<'_> {
fn from(source: crate::widget::icon_source::IconSource) -> Self {
match source {
crate::widget::icon_source::IconSource::Builtin(name) => Self::Builtin(name),
crate::widget::icon_source::IconSource::StaticGeometry(data) => {
Self::StaticGeometry(data)
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IconPaintOutcome {
Drawn,
SkippedEmptySlot,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IconPaintError {
UnsupportedSourceForProfile,
ZeroGrid,
InvalidGeometry(crate::render::path::FlattenError),
EmptyGeometry,
Backend(crate::render::CompoundPathError),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct IconSourceMetadata {
pub grid: u16,
pub contour_count: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct IconMetrics {
pub bounds: Rect,
pub source_extent: u32,
}
impl IconMetrics {
pub fn is_empty(&self) -> bool {
self.bounds.width == 0 || self.bounds.height == 0
}
}
fn glyph_box(slot: Rect) -> Rect {
let side = slot.width.min(slot.height);
Rect::new(
slot.x + ((slot.width - side) / 2) as i32,
slot.y + ((slot.height - side) / 2) as i32,
side,
side,
)
}
enum SourceGeometry {
#[cfg(any(feature = "icons", widgets_unstripped))]
Outline(IconData),
Fallback(&'static crate::widget::icon_fallback_data::IconFallback),
}
fn source_geometry(source: IconRef<'_>) -> Result<SourceGeometry, IconPaintError> {
match source {
IconRef::Builtin(name) => {
#[cfg(feature = "icons")]
if let Some(data) = name.data_opt() {
return Ok(SourceGeometry::Outline(data));
}
match crate::widget::icon_fallback_data::ICON_FALLBACK
.iter()
.find(|entry| entry.name == name.as_str())
{
None => Err(IconPaintError::EmptyGeometry),
Some(entry) => Ok(SourceGeometry::Fallback(entry)),
}
}
IconRef::StaticGeometry(data) => {
#[cfg(any(feature = "icons", widgets_unstripped))]
{
Ok(SourceGeometry::Outline(*data))
}
#[cfg(not(any(feature = "icons", widgets_unstripped)))]
{
let _ = data;
Err(IconPaintError::UnsupportedSourceForProfile)
}
}
}
}
pub fn measure_icon(source: IconRef<'_>, slot: Rect) -> Result<IconMetrics, IconPaintError> {
let extent = match source_geometry(source)? {
#[cfg(any(feature = "icons", widgets_unstripped))]
SourceGeometry::Outline(data) => u32::from(data.grid),
SourceGeometry::Fallback(entry) => u32::from(entry.grid),
};
Ok(IconMetrics { bounds: glyph_box(slot), source_extent: extent })
}
pub fn validate_icon(source: IconRef<'_>) -> Result<IconSourceMetadata, IconPaintError> {
match source_geometry(source)? {
#[cfg(any(feature = "icons", widgets_unstripped))]
SourceGeometry::Outline(data) => {
use crate::render::path::{parse, MAX_OUTLINE_CONTOURS};
if data.grid == 0 {
return Err(IconPaintError::ZeroGrid);
}
let mut contour_count = 0usize;
for d in data.paths {
let subpaths = parse(d).map_err(|error| {
IconPaintError::InvalidGeometry(crate::render::path::FlattenError::Parse(error))
})?;
for subpath in &subpaths {
if subpath.segments.is_empty() {
continue;
}
contour_count += 1;
if contour_count > MAX_OUTLINE_CONTOURS {
return Err(IconPaintError::InvalidGeometry(
crate::render::path::FlattenError::TooManyContours,
));
}
}
}
if contour_count == 0 {
return Err(IconPaintError::EmptyGeometry);
}
Ok(IconSourceMetadata { grid: data.grid, contour_count })
}
SourceGeometry::Fallback(entry) => {
if entry.grid == 0 {
return Err(IconPaintError::ZeroGrid);
}
if entry.contours.is_empty() {
return Err(IconPaintError::EmptyGeometry);
}
Ok(IconSourceMetadata { grid: entry.grid, contour_count: entry.contours.len() })
}
}
}
pub fn paint_icon(
ctx: &mut RenderContext,
source: IconRef<'_>,
slot: Rect,
color: Color,
) -> Result<IconPaintOutcome, IconPaintError> {
if slot.width == 0 || slot.height == 0 {
return Ok(IconPaintOutcome::SkippedEmptySlot);
}
match source {
IconRef::Builtin(name) => {
#[cfg(feature = "icons")]
if let Some(data) = name.data_opt() {
draw_outline_in(ctx, slot, color, &data)?;
return Ok(IconPaintOutcome::Drawn);
}
if draw_fallback_in(ctx, slot, color, name.as_str()) {
Ok(IconPaintOutcome::Drawn)
} else {
Err(IconPaintError::EmptyGeometry)
}
}
IconRef::StaticGeometry(data) => {
#[cfg(any(feature = "icons", widgets_unstripped))]
{
draw_outline_in(ctx, slot, color, data)?;
Ok(IconPaintOutcome::Drawn)
}
#[cfg(not(any(feature = "icons", widgets_unstripped)))]
{
let _ = (ctx, data, color);
Err(IconPaintError::UnsupportedSourceForProfile)
}
}
}
}
pub fn draw_icon_named(ctx: &mut RenderContext, rect: Rect, color: Color, name: &str) -> bool {
if rect.width == 0 || rect.height == 0 {
return false;
}
match resolve_named_icon(name) {
#[cfg(any(feature = "icons", widgets_unstripped))]
Some(NamedIcon::Outline(data)) => {
if let Err(error) = draw_outline_in(ctx, rect, color, &data) {
log::error!("icon '{}' could not be painted: {error:?}", data.name);
}
true
}
Some(NamedIcon::Fallback) => {
draw_fallback_in(ctx, rect, color, name);
true
}
None => false,
}
}
pub fn icon_token_is_drawable(name: &str) -> bool {
resolve_named_icon(name).is_some()
}
#[cfg(feature = "image")]
pub fn draw_icon_image(
context: &mut RenderContext,
image: &crate::image::Image,
slot: Rect,
) -> bool {
let crate::image::ImageData::Rgba8(pixels) = &image.inner.data else {
return false;
};
if pixels.is_empty() {
return false;
}
let fitted = crate::widget::icon_source::fit_image_into(image.width(), image.height(), slot);
context.draw_image(fitted.x, fitted.y, fitted.width, fitted.height, pixels);
true
}
enum NamedIcon {
#[cfg(any(feature = "icons", widgets_unstripped))]
Outline(IconData),
Fallback,
}
fn resolve_named_icon(name: &str) -> Option<NamedIcon> {
#[cfg(any(feature = "icons", widgets_unstripped))]
{
#[cfg(feature = "icons")]
if let Some(known) = IconName::from_name(name) {
if let Some(data) = known.data_opt() {
return Some(NamedIcon::Outline(data));
}
}
#[cfg(widgets_unstripped)]
if let Some(data) = crate::widget::display_widgets::icon_data_set::lookup_registered(name) {
return Some(NamedIcon::Outline(data));
}
}
if crate::widget::icon_fallback_data::ICON_FALLBACK.iter().any(|entry| entry.name == name) {
return Some(NamedIcon::Fallback);
}
None
}
pub fn draw_icon_centered(
ctx: &mut RenderContext,
box_rect: Rect,
side: u32,
color: Color,
name: IconName,
) {
let side = side.min(box_rect.width).min(box_rect.height) as i32;
if side <= 0 {
return;
}
let rect = Rect::new(
box_rect.x + (box_rect.width as i32 - side) / 2,
box_rect.y + (box_rect.height as i32 - side) / 2,
side as u32,
side as u32,
);
draw_icon_at(ctx, rect, color, name);
}
#[cfg(any(feature = "icons", widgets_unstripped))]
fn draw_outline_in(
ctx: &mut RenderContext,
rect: Rect,
color: Color,
data: &IconData,
) -> Result<(), IconPaintError> {
#[cfg(all(not(alloc_frugal), any(feature = "icons", widgets_unstripped)))]
use crate::render::path::flatten::flatten_static_paths;
#[cfg(alloc_frugal)]
use crate::render::path::flatten_paths;
use crate::render::path::{IconPlacement, MAX_OUTLINE_CONTOURS, MAX_OUTLINE_POINTS};
let rect = glyph_box(rect);
if rect.width == 0 || rect.height == 0 {
return Err(IconPaintError::EmptyGeometry);
}
let placement = IconPlacement::new(rect.x, rect.y, rect.width as f32, data.grid);
let mut points = [Point::new(0, 0); MAX_OUTLINE_POINTS];
let mut contours = [(0usize, 0usize); MAX_OUTLINE_CONTOURS];
#[cfg(alloc_frugal)]
let count = flatten_paths(data.paths, placement, &mut points, &mut contours);
#[cfg(not(alloc_frugal))]
let count = flatten_static_paths(data.paths, placement, &mut points, &mut contours);
let count = count.map_err(IconPaintError::InvalidGeometry)?;
if count == 0 {
return Err(IconPaintError::EmptyGeometry);
}
let point_count = contours[..count].iter().map(|&(_, end)| end).max().unwrap_or(0);
ctx.fill_path_non_zero(&points[..point_count], &contours[..count], color)
.map_err(IconPaintError::Backend)
}
fn draw_fallback_in(ctx: &mut RenderContext, rect: Rect, color: Color, token: &str) -> bool {
use crate::widget::icon_fallback_data::ICON_FALLBACK;
let Some(fallback) = ICON_FALLBACK.iter().find(|entry| entry.name == token) else {
return false;
};
let rect = glyph_box(rect);
if rect.width == 0 || rect.height == 0 {
return false;
}
let mut points = crate::compat::Vec::new();
let mut contours = crate::compat::Vec::new();
let grid = f32::from(fallback.grid);
let scale = rect.width as f32 / grid;
let origin_x = rect.x as f32;
let origin_y = rect.y as f32;
for contour in fallback.contours {
if contour.len() < 3 {
continue;
}
let start = points.len();
points.extend(contour.iter().map(|&(gx, gy)| {
Point::new(
(origin_x + gx as f32 * scale).round() as i32,
(origin_y + (gy as f32 + grid) * scale).round() as i32,
)
}));
contours.push((start, points.len()));
}
if let Err(error) = ctx.fill_path_non_zero(&points, &contours, color) {
log::error!("fallback icon '{}' could not be submitted: {error:?}", fallback.name);
}
true
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct IconData {
pub name: &'static str,
pub grid: u16,
pub paths: &'static [&'static str],
}
#[cfg(widgets_unstripped)]
pub struct Icon {
base: BaseWidget,
icon_name: String,
icon_source: Option<IconSource>,
size: f32,
color: Option<Color>,
}
#[cfg(widgets_unstripped)]
impl Icon {
pub fn new(geometry: Rect) -> Self {
Self {
base: BaseWidget::new(WidgetKind::Icon, geometry, "Icon"),
icon_name: "check".to_string(),
icon_source: None,
size: 24.0,
color: None,
}
}
pub fn set_icon(&mut self, name: &str) {
self.icon_name = name.to_string();
self.icon_source = None;
self.base.request_redraw();
}
pub fn icon(&self) -> &str {
match self.icon_source {
Some(IconSource::Builtin(name)) => name.as_str(),
Some(IconSource::StaticGeometry(_)) => "",
None => &self.icon_name,
}
}
pub fn set_icon_enum(&mut self, icon: IconName) {
self.set_icon(icon.as_str());
}
pub fn set_icon_source(&mut self, source: IconSource) {
self.icon_name.clear();
self.icon_source = Some(source);
self.base.request_redraw();
}
pub fn icon_source(&self) -> Option<IconSource> {
self.icon_source
}
pub fn clear_icon_source(&mut self) {
self.icon_source = None;
self.icon_name.clear();
self.base.request_redraw();
}
pub fn set_size(&mut self, size: f32) {
self.size = size.max(4.0);
self.base.request_redraw();
}
pub fn size(&self) -> f32 {
self.size
}
pub fn set_color(&mut self, color: Color) {
self.color = Some(color);
self.base.request_redraw();
}
pub fn clear_color(&mut self) {
self.color = None;
self.base.request_redraw();
}
pub fn color(&self) -> Color {
self.resolve_color()
}
pub fn explicit_color(&self) -> Option<Color> {
self.color
}
fn resolve_color(&self) -> Color {
if let Some(explicit) = self.color {
return explicit;
}
if let Some(themed) = self.style().text_color {
return themed;
}
Color::PRIMARY
}
fn icon_rect(&self) -> Rect {
let rect = self.geometry();
let size = self.size as u32;
let x = rect.x + ((rect.width as i32) - size as i32) / 2;
let y = rect.y + ((rect.height as i32) - size as i32) / 2;
Rect::new(x.max(rect.x), y.max(rect.y), size.min(rect.width), size.min(rect.height))
}
fn draw_icon(&self, ctx: &mut RenderContext) {
if let Some(source) = self.icon_source {
if let Err(error) =
paint_icon(ctx, source.into(), self.icon_rect(), self.resolve_color())
{
log::error!("typed icon could not be painted: {error:?}");
}
return;
}
#[cfg(feature = "icons")]
{
if let Some(name) = IconName::from_name(&self.icon_name) {
if let Some(data) = name.data_opt() {
self.draw_outline(ctx, &data);
return;
}
}
}
if let Some(data) =
crate::widget::display_widgets::icon_data_set::lookup_registered(&self.icon_name)
{
self.draw_outline_data(ctx, &data);
return;
}
self.draw_fallback(ctx);
}
fn draw_fallback(&self, ctx: &mut RenderContext) {
let rect = self.icon_rect();
if rect.width == 0 || rect.height == 0 {
return;
}
let token = self.icon_name.as_str();
if !draw_fallback_in(ctx, rect, self.resolve_color(), token) {
self.draw_unknown(ctx);
}
}
fn draw_outline_data(&self, ctx: &mut RenderContext, data: &IconData) {
let rect = self.icon_rect();
if rect.width == 0 || rect.height == 0 {
return;
}
if let Err(error) = draw_outline_in(ctx, rect, self.resolve_color(), data) {
log::error!("icon '{}' could not be painted: {error:?}", data.name);
}
}
#[cfg(feature = "icons")]
fn draw_outline(&self, ctx: &mut RenderContext, data: &IconData) {
self.draw_outline_data(ctx, data);
}
fn draw_unknown(&self, ctx: &mut RenderContext) {
let r = self.icon_rect();
let c = self.resolve_color();
let cx = r.x;
let cy = r.y;
let s = r.width as i32;
ctx.draw_text(
Point::new(cx + s / 4, cy + s * 3 / 4),
"?",
&crate::core::Font::simple("sans-serif", 12.0),
c,
HorizontalAlignment::Left,
);
}
}
#[cfg(widgets_unstripped)]
impl Widget for Icon {
fn base(&self) -> &BaseWidget {
&self.base
}
fn set_state_theme_hook(&mut self) {
crate::style::reapply_active_theme_state(self);
}
fn base_mut(&mut self) -> &mut BaseWidget {
&mut self.base
}
fn size_hint(&self) -> crate::core::Size {
let side = self.size.round().max(1.0) as u32;
crate::core::Size::new(side, side)
}
impl_draw_bridge!();
impl_widget_property_hooks!();
}
#[cfg(widgets_unstripped)]
impl WidgetProperties for Icon {
fn get(&self, name: &str) -> Result<CapabilityValue, CapabilityAccessError> {
match name {
"icon_name" => Ok(CapabilityValue::String(self.icon().to_string())),
"icon_source" => match self.icon_source() {
None => Ok(CapabilityValue::Null),
Some(IconSource::Builtin(name)) => Ok(CapabilityValue::IconSpec(
crate::widget::icon_source::IconSpec::Builtin(name),
)),
Some(IconSource::StaticGeometry(_)) => {
Err(CapabilityAccessError::IconSourceNotPersistable)
}
},
"size" => Ok(CapabilityValue::Float(f64::from(self.size()))),
"color" => Ok(CapabilityValue::String(self.resolve_color().to_hex_rgba())),
_ => base_property_get(self, name),
}
}
fn set(&mut self, name: &str, value: CapabilityValue) -> Result<(), CapabilityAccessError> {
match name {
"icon_name" => {
self.set_icon(&expect_string(value)?);
Ok(())
}
"icon_source" => {
match value {
CapabilityValue::Null => self.clear_icon_source(),
CapabilityValue::IconSpec(crate::widget::icon_source::IconSpec::Builtin(
name,
)) => self.set_icon_source(IconSource::Builtin(name)),
CapabilityValue::IconSpec(crate::widget::icon_source::IconSpec::Resource {
..
}) => return Err(CapabilityAccessError::ResourceResolutionRequired),
_ => return Err(CapabilityAccessError::TypeMismatch),
}
Ok(())
}
"size" => {
self.set_size(expect_f64(value)? as f32);
Ok(())
}
"color" => {
let raw = expect_string(value)?;
let color = Color::parse_hex(&raw).ok_or(CapabilityAccessError::TypeMismatch)?;
self.set_color(color);
Ok(())
}
_ => base_property_set(self, name, value),
}
}
fn property_names(&self) -> &'static [&'static str] {
property_names_of!["icon_name", "icon_source", "size", "color", BASE_PROPERTY_NAMES]
}
fn set_resolved_icon_source(
&mut self,
source: Option<IconSource>,
) -> Result<(), CapabilityAccessError> {
match source {
Some(source) => self.set_icon_source(source),
None => self.clear_icon_source(),
}
Ok(())
}
fn command(&mut self, name: &str) -> Result<(), CapabilityAccessError> {
match name {
"set_icon_name" | "set_size" => Err(CapabilityAccessError::OutOfRange),
_ => Err(CapabilityAccessError::UnknownCommand),
}
}
}
#[cfg(widgets_unstripped)]
impl Draw for Icon {
fn draw(&mut self, context: &mut RenderContext) {
let rect = self.geometry();
if rect.width == 0 || rect.height == 0 {
return;
}
if !self.base.is_enabled() {
let resolved = self.resolve_color();
let gray = ((resolved.r as u16 + resolved.g as u16 + resolved.b as u16) / 3) as u8;
let saved = self.color;
self.color = Some(Color::rgba(gray, gray, gray, resolved.a / 2));
self.draw_icon(context);
self.color = saved;
return;
}
self.draw_icon(context);
}
}
#[cfg(widgets_unstripped)]
impl EventHandler for Icon {
fn handle_event(&mut self, event: &Event) {
self.base.handle_event(event);
}
}
#[cfg(all(test, widgets_unstripped))]
mod tests {
use super::*;
#[test]
fn icon_default_creation() {
let icon = Icon::new(Rect::new(0, 0, 24, 24));
assert_eq!(icon.kind(), WidgetKind::Icon);
assert_eq!(icon.icon(), "check");
assert!((icon.size() - 24.0).abs() < f32::EPSILON);
assert_eq!(icon.color(), Color::PRIMARY);
}
#[test]
fn icon_set_icon_and_color() {
let mut icon = Icon::new(Rect::new(0, 0, 24, 24));
icon.set_icon("heart");
assert_eq!(icon.icon(), "heart");
icon.set_icon_enum(IconName::Star);
assert_eq!(icon.icon(), "star");
icon.set_color(Color::RED);
assert_eq!(icon.color(), Color::RED);
icon.set_size(32.0);
assert!((icon.size() - 32.0).abs() < f32::EPSILON);
}
#[test]
fn colour_resolution_prefers_an_explicit_setter() {
let mut icon = Icon::new(Rect::new(0, 0, 24, 24));
assert_eq!(icon.explicit_color(), None, "a new icon follows the theme");
assert_eq!(icon.color(), Color::PRIMARY, "with no theme, the fallback stands");
icon.set_color(Color::RED);
assert_eq!(icon.explicit_color(), Some(Color::RED));
assert_eq!(icon.color(), Color::RED);
icon.clear_color();
assert_eq!(icon.explicit_color(), None, "clearing returns the icon to the theme");
assert_eq!(icon.color(), Color::PRIMARY);
}
#[test]
fn colour_resolution_follows_the_widget_style() {
use crate::style::WidgetStyle;
let mut icon = Icon::new(Rect::new(0, 0, 24, 24));
let themed = Color::rgba(7, 8, 9, 255);
icon.set_style(WidgetStyle::default().with_text_color(themed));
assert_eq!(icon.color(), themed, "the style's text colour must drive the icon");
icon.set_color(Color::RED);
assert_eq!(icon.color(), Color::RED);
}
#[test]
fn drawing_disabled_does_not_discard_an_explicit_colour() {
use crate::render::{PaintBackend, RenderContext, SoftwarePaintBackend};
use crate::widget::draw::Draw;
let bounds = Rect::new(0, 0, 24, 24);
let mut icon = Icon::new(bounds);
icon.set_icon("heart");
icon.set_color(Color::RED);
let mut surface = SoftwarePaintBackend::new(bounds.size(), 1.0);
surface.begin_frame(Color::WHITE);
{
let mut context = RenderContext::new(&mut surface);
icon.draw(&mut context);
}
assert_eq!(
icon.explicit_color(),
Some(Color::RED),
"a normal draw must leave the caller's colour untouched"
);
icon.set_enabled(false);
surface.begin_frame(Color::WHITE);
{
let mut context = RenderContext::new(&mut surface);
icon.draw(&mut context);
}
assert_eq!(
icon.explicit_color(),
Some(Color::RED),
"a disabled draw must restore the caller's colour, not clear it"
);
icon.set_enabled(true);
assert_eq!(icon.color(), Color::RED, "re-enabling must paint the caller's colour again");
}
#[test]
fn the_color_property_round_trips_and_rejects_garbage() {
use crate::widget::capability::WidgetProperties;
let mut icon = Icon::new(Rect::new(0, 0, 24, 24));
assert!(icon.property_names().contains(&"color"));
icon.set("color", CapabilityValue::String("#11223344".to_string()))
.expect("a hex colour must be accepted");
assert_eq!(icon.color(), Color::rgba(0x11, 0x22, 0x33, 0x44));
assert_eq!(
icon.set("color", CapabilityValue::String("not-a-colour".to_string())),
Err(CapabilityAccessError::TypeMismatch),
"an unparsable colour must be reported"
);
}
#[test]
fn icon_name_enum_roundtrip() {
for name in IconName::ALL {
let s = name.as_str();
let parsed = IconName::from_name(s);
assert!(parsed.is_some(), "Failed to parse icon name: {s}");
assert_eq!(parsed.unwrap(), name);
}
assert!(IconName::ALL.len() > 1, "the enum must declare more than one icon");
}
#[test]
fn icon_svg_output_check() {
let mut icon = Icon::new(Rect::new(0, 0, 24, 24));
icon.set_icon("check");
let svg = crate::widget::svg::render_to_svg(&mut icon);
assert!(svg.starts_with("<svg"));
assert!(svg.ends_with("</svg>"));
}
#[test]
fn static_compound_icon_preserves_holes_and_replaces_legacy_state() {
use crate::render::{PaintBackend, RenderContext, SoftwarePaintBackend};
use crate::widget::draw::Draw;
static RING: IconData =
IconData { name: "ring", grid: 24, paths: &["M2-2H22V-22H2Z M7-7V-17H17V-7Z"] };
let bounds = Rect::new(0, 0, 24, 24);
let mut icon = Icon::new(bounds);
let source = IconSource::static_geometry(&RING);
icon.set_icon_source(source);
assert_eq!(icon.icon_source(), Some(source));
assert!(icon.icon().is_empty(), "static geometry has no legacy token name");
let mut backend = SoftwarePaintBackend::new(bounds.size(), 1.0);
backend.begin_frame(Color::TRANSPARENT);
{
let mut context = RenderContext::new(&mut backend);
icon.draw(&mut context);
}
backend.end_frame();
let pixels = backend.frame_rgba();
let alpha_at = |x: usize, y: usize| pixels[(y * 24 + x) * 4 + 3];
assert_eq!(alpha_at(12, 12), 0, "opposite-wound inner contours must remain transparent");
assert_eq!(alpha_at(3, 12), 255, "the surrounding contour must be filled");
icon.set_icon("check");
assert_eq!(icon.icon_source(), None, "a legacy setter replaces the typed source");
assert_eq!(icon.icon(), "check");
icon.set_icon_source(source);
icon.clear_icon_source();
assert_eq!(icon.icon_source(), None);
assert!(icon.icon().is_empty(), "clearing removes rather than revives a prior source");
let builtin = IconSource::builtin(IconName::Info);
icon.set_icon_source(builtin);
assert_eq!(icon.icon_source(), Some(builtin));
assert_eq!(icon.icon(), "info", "typed built-ins expose their canonical legacy token");
icon.set_icon("warning");
assert_eq!(icon.icon_source(), None, "the legacy setter also replaces a typed built-in");
assert_eq!(icon.icon(), "warning");
}
#[test]
fn the_icon_source_property_round_trips_and_refuses_a_runtime_only_source() {
use crate::widget::icon_source::{IconColorMode, IconSpec};
static RING: IconData =
IconData { name: "ring", grid: 24, paths: &["M2-2H22V-22H2Z M7-7V-17H17V-7Z"] };
let mut icon = Icon::new(Rect::new(0, 0, 24, 24));
assert_eq!(icon.get("icon_source"), Ok(CapabilityValue::Null), "a fresh icon has none");
assert!(icon.property_names().contains(&"icon_source"), "the name must be published");
icon.set("icon_source", CapabilityValue::IconSpec(IconSpec::Builtin(IconName::Info)))
.expect("a built-in token is persistable");
assert_eq!(icon.icon_source(), Some(IconSource::Builtin(IconName::Info)));
assert_eq!(
icon.get("icon_source"),
Ok(CapabilityValue::IconSpec(IconSpec::Builtin(IconName::Info)))
);
assert_eq!(icon.icon(), "info", "one active state: the legacy name reports the token");
assert_eq!(
icon.set(
"icon_source",
CapabilityValue::IconSpec(IconSpec::Resource {
id: "project/icons/a".into(),
color_mode: IconColorMode::Monochrome,
})
),
Err(CapabilityAccessError::ResourceResolutionRequired)
);
icon.set_icon_source(IconSource::static_geometry(&RING));
assert_eq!(icon.get("icon_source"), Err(CapabilityAccessError::IconSourceNotPersistable));
icon.set("icon_source", CapabilityValue::Null).expect("null clears the source");
assert_eq!(icon.icon_source(), None);
assert!(icon.icon().is_empty(), "clearing the typed source clears the legacy name too");
}
#[test]
fn icon_svg_output_star() {
let mut icon = Icon::new(Rect::new(0, 0, 24, 24));
icon.set_icon_enum(IconName::Star);
let svg = crate::widget::svg::render_to_svg(&mut icon);
assert!(svg.starts_with("<svg"));
assert!(svg.ends_with("</svg>"));
}
#[test]
fn icon_svg_output_warning() {
let mut icon = Icon::new(Rect::new(0, 0, 24, 24));
icon.set_icon("warning");
let svg = crate::widget::svg::render_to_svg(&mut icon);
assert!(svg.starts_with("<svg"));
assert!(svg.ends_with("</svg>"));
}
#[test]
fn icon_event_forwarding() {
let mut icon = Icon::new(Rect::new(0, 0, 24, 24));
icon.handle_event(&Event::MouseMove { pos: Point::new(10, 10) });
icon.handle_event(&Event::MousePress { pos: Point::new(10, 10), button: 1, modifiers: 0 });
}
#[test]
fn icon_unknown_fallback() {
let mut icon = Icon::new(Rect::new(0, 0, 24, 24));
icon.set_icon("nonexistent");
let svg = crate::widget::svg::render_to_svg(&mut icon);
assert!(svg.starts_with("<svg"));
assert!(svg.ends_with("</svg>"));
}
#[cfg(all(feature = "icons", not(alloc_frugal)))]
#[test]
fn every_tokened_icon_draws_a_distinct_outline() {
use crate::widget::svg::render_to_svg;
let mut outlines: Vec<String> = Vec::new();
for token in IconName::ALL {
let mut icon = Icon::new(Rect::new(0, 0, 24, 24));
icon.set_icon_enum(token);
let svg = render_to_svg(&mut icon);
assert!(
svg.contains("<path"),
"{token:?} rendered no outline path, so the data did not reach the draw: {svg}"
);
for other in &outlines {
assert_ne!(other, &svg, "{token:?} draws the same picture as another icon");
}
outlines.push(svg);
}
assert_eq!(outlines.len(), IconName::ALL.len());
}
#[cfg(not(feature = "icons"))]
#[test]
fn a_tokened_icon_still_draws_without_the_data() {
use crate::widget::svg::render_to_svg;
let mut icon = Icon::new(Rect::new(0, 0, 24, 24));
icon.set_icon_enum(IconName::Check);
let svg = render_to_svg(&mut icon);
assert!(
svg.contains("<path"),
"the generated fallback must still draw a real outline: {svg}"
);
assert!(!svg.contains("?"), "the fallback must find the token, not draw the placeholder");
}
#[test]
fn the_fallback_table_is_complete_and_distinct() {
use crate::widget::icon_fallback_data::ICON_FALLBACK;
assert_eq!(
ICON_FALLBACK.len(),
IconName::ALL.len(),
"the fallback table and the IconName variants disagree on how many icons exist"
);
for (entry, variant) in ICON_FALLBACK.iter().zip(IconName::ALL.iter()) {
assert_eq!(
entry.name,
variant.as_str(),
"the fallback table's order has drifted from IconName's declaration order"
);
assert!(entry.grid > 0, "{:?} has a zero grid, so it cannot be scaled", entry.name);
assert!(!entry.contours.is_empty(), "{:?} has no fallback geometry", entry.name);
let mut points = 0usize;
for contour in entry.contours {
assert!(
contour.len() >= 3,
"{:?} has a contour of {} points, which cannot be a polygon",
entry.name,
contour.len()
);
points += contour.len();
}
assert!(points > 0, "{:?} flattened to geometry with no points", entry.name);
}
for (index, entry) in ICON_FALLBACK.iter().enumerate() {
for other in &ICON_FALLBACK[index + 1..] {
let same = entry.contours.len() == other.contours.len()
&& entry.contours.iter().zip(other.contours.iter()).all(|(a, b)| a == b);
assert!(
!same,
"{:?} and {:?} have identical fallback geometry, so two names draw one \
picture",
entry.name, other.name
);
}
}
}
#[cfg(feature = "icons")]
#[test]
fn measure_icon_is_square_contain_and_matches_where_the_glyph_is_drawn() {
use crate::core::Size;
use crate::render::{PaintBackend, RenderContext, SoftwarePaintBackend};
let wide = Rect::new(10, 20, 40, 12);
let metrics =
measure_icon(IconRef::Builtin(IconName::Info), wide).expect("a bundled token measures");
assert_eq!(
(metrics.bounds.width, metrics.bounds.height),
(12, 12),
"the square side is the slot's smaller dimension"
);
assert_eq!((metrics.bounds.x, metrics.bounds.y), (24, 20), "centred in the slot");
assert!(!metrics.is_empty());
let mut backend = SoftwarePaintBackend::new(Size::new(64, 40), 1.0);
backend.begin_frame(Color::TRANSPARENT);
{
let mut ctx = RenderContext::new(&mut backend);
paint_icon(&mut ctx, IconRef::Builtin(IconName::Info), wide, Color::BLACK)
.expect("the bundled outline paints");
}
backend.end_frame();
let mut ink = 0usize;
let (left, top) = (metrics.bounds.x, metrics.bounds.y);
let (right, bottom) =
(left + metrics.bounds.width as i32, top + metrics.bounds.height as i32);
for y in 0..40i32 {
for x in 0..64i32 {
let alpha = PaintBackend::frame_rgba(&backend)[((y * 64 + x) * 4 + 3) as usize];
if alpha == 0 {
continue;
}
ink += 1;
assert!(
(left..right).contains(&x) && (top..bottom).contains(&y),
"ink at ({x}, {y}) escapes the measured box {left}..{right} x {top}..{bottom}"
);
}
}
assert!(ink > 0, "the glyph must actually paint inside its box");
let square = Rect::new(4, 5, 16, 16);
assert_eq!(
measure_icon(IconRef::Builtin(IconName::Save), square).expect("measures").bounds,
square
);
assert_eq!(metrics.source_extent, u32::from(IconName::Info.data().grid));
}
#[test]
fn measure_icon_reports_an_empty_slot_as_empty_rather_than_failing() {
let metrics = measure_icon(IconRef::Builtin(IconName::Info), Rect::new(0, 0, 0, 8))
.expect("an empty slot is not an error");
assert!(metrics.is_empty());
}
#[cfg(feature = "icons")]
#[test]
fn validate_icon_reports_the_source_budget_and_refuses_the_undrawable() {
let metadata =
validate_icon(IconRef::Builtin(IconName::Info)).expect("a bundled token validates");
assert_eq!(metadata.grid, IconName::Info.data().grid);
assert!(metadata.contour_count >= 1);
static HOLE: IconData = IconData {
name: "validate-hole",
grid: 24,
paths: &["M2-2H22V-22H2Z M7-7V-17H17V-7Z"],
};
let two = validate_icon(IconRef::StaticGeometry(&HOLE)).expect("two subpaths validate");
assert_eq!(two.contour_count, 2, "one contour per `d` subpath");
assert_eq!(two.grid, 24);
static ZERO_GRID: IconData = IconData { name: "zero", grid: 0, paths: &["M0 0L1 0Z"] };
assert_eq!(
validate_icon(IconRef::StaticGeometry(&ZERO_GRID)),
Err(IconPaintError::ZeroGrid)
);
static BROKEN: IconData = IconData { name: "broken", grid: 24, paths: &["M0 0L oops"] };
assert!(matches!(
validate_icon(IconRef::StaticGeometry(&BROKEN)),
Err(IconPaintError::InvalidGeometry(crate::render::path::FlattenError::Parse(_)))
));
static NO_CONTOURS: IconData = IconData { name: "none", grid: 24, paths: &[""] };
assert_eq!(
validate_icon(IconRef::StaticGeometry(&NO_CONTOURS)),
Err(IconPaintError::EmptyGeometry)
);
}
#[test]
fn a_source_too_fine_for_the_slot_is_refused_at_paint_time_not_at_validation() {
use crate::core::Size;
use crate::render::{PaintBackend, RenderContext, SoftwarePaintBackend};
static TINY: IconData = IconData { name: "tiny", grid: 960, paths: &["M0 0L10 0L10 -10Z"] };
assert!(
validate_icon(IconRef::StaticGeometry(&TINY)).is_ok(),
"the source itself is valid"
);
let mut backend = SoftwarePaintBackend::new(Size::new(24, 24), 1.0);
backend.begin_frame(Color::TRANSPARENT);
let mut ctx = RenderContext::new(&mut backend);
assert_eq!(
paint_icon(
&mut ctx,
IconRef::StaticGeometry(&TINY),
Rect::new(0, 0, 24, 24),
Color::BLACK
),
Err(IconPaintError::EmptyGeometry),
"at 24 px the whole outline is sub-pixel, which paint reports for that size"
);
}
#[test]
fn painting_a_token_with_no_geometry_is_a_failure_not_a_silent_success() {
use crate::core::Size;
use crate::render::{PaintBackend, RenderContext, SoftwarePaintBackend};
let mut backend = SoftwarePaintBackend::new(Size::new(24, 24), 1.0);
backend.begin_frame(Color::TRANSPARENT);
let mut ctx = RenderContext::new(&mut backend);
assert!(!draw_icon_named(&mut ctx, Rect::new(0, 0, 24, 24), Color::BLACK, "not-an-icon"));
assert!(!icon_token_is_drawable("not-an-icon"));
}
#[test]
fn a_registered_icon_draws_through_the_real_pipeline() {
use crate::widget::svg::render_to_svg;
use crate::widget::{clear_registered_icons, register_icon};
clear_registered_icons();
assert!(register_icon("host_probe", &["M480-200 240-440l480 480-240-240Z"]));
let mut icon = Icon::new(Rect::new(0, 0, 24, 24));
icon.set_icon("host_probe");
let svg = render_to_svg(&mut icon);
assert!(
svg.contains("<path"),
"a registered icon must draw real geometry, not fall through to the placeholder: {svg}"
);
assert!(!svg.contains("?"), "the registered name must resolve, not draw the placeholder");
clear_registered_icons();
}
#[test]
fn an_unknown_name_still_draws_the_placeholder() {
use crate::widget::clear_registered_icons;
use crate::widget::svg::render_to_svg;
clear_registered_icons();
let mut unknown = Icon::new(Rect::new(0, 0, 24, 24));
unknown.set_icon("definitely_not_a_token");
let unknown_svg = render_to_svg(&mut unknown);
let mut registered = Icon::new(Rect::new(0, 0, 24, 24));
crate::widget::register_icon(
"probe_for_placeholder",
&["M480-200 240-440l480 480-240-240Z"],
);
registered.set_icon("probe_for_placeholder");
let registered_svg = render_to_svg(&mut registered);
clear_registered_icons();
assert_ne!(
unknown_svg, registered_svg,
"an unknown name must not draw what a registered icon draws"
);
assert!(
unknown_svg.contains("<path") || unknown_svg.contains("<rect"),
"the placeholder must draw something: {unknown_svg}"
);
}
#[test]
fn a_non_960_grid_is_placed_inside_its_box() {
use crate::render::path::{flatten_paths, IconPlacement};
let placement = IconPlacement::new(10, 20, 24.0, 24);
let mut points = [Point::new(0, 0); 16];
let mut contours = [(0usize, 0usize); 4];
let count = flatten_paths(&["M0-15h24L12-24Z"], placement, &mut points, &mut contours)
.expect("a triangle flattens");
assert_eq!(count, 1);
let (start, end) = contours[0];
for point in &points[start..end] {
assert!(
(10..=34).contains(&point.x) && (20..=44).contains(&point.y),
"a 24-grid icon must land inside its box, but a vertex is at {point:?}"
);
}
let apex_y = points[start..end].iter().map(|point| point.y).min().expect("a vertex");
let base_y = points[start..end].iter().map(|point| point.y).max().expect("a vertex");
assert!(
apex_y < base_y,
"the apex must be above the base, or the icon is rotated 180 degrees \
(apex y={apex_y} base y={base_y})"
);
assert_eq!(apex_y, 20, "design y = -grid is the grid's top, so it is the box's top edge");
assert_eq!(base_y, 29, "design y = -15 scales to 9 px below the box's top");
}
}