#[cfg(widgets_unstripped)]
use crate::core::HorizontalAlignment;
use crate::core::{Color, Point, Rect};
#[cfg(widgets_unstripped)]
use crate::event::{Event, EventHandler};
#[cfg(all(widgets_unstripped, feature = "image"))]
use crate::image::Image;
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 fallback = IconName::from_name(token)
.and_then(|name| ICON_FALLBACK.get(name as usize))
.filter(|entry| entry.name == token)
.or_else(|| ICON_FALLBACK.iter().find(|entry| entry.name == token));
let Some(fallback) = fallback 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 const ICON_FRAME_INTERVAL_MS: u32 = 100;
#[cfg(widgets_unstripped)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum IconState {
#[default]
Normal,
Disabled,
Selected,
}
#[cfg(widgets_unstripped)]
pub struct Icon {
base: BaseWidget,
icon_name: String,
icon_source: Option<IconSource>,
size: f32,
color: Option<Color>,
state: IconState,
scale_factor: f32,
#[cfg(feature = "image")]
frames: crate::compat::Vec<Image>,
#[cfg(feature = "image")]
frame_index: usize,
#[cfg(feature = "image")]
frame_elapsed_ms: u32,
#[cfg(feature = "image")]
frame_interval_ms: u32,
#[cfg(feature = "image")]
frame_tint: crate::compat::Vec<u8>,
#[cfg(feature = "image")]
frames_generation: u64,
#[cfg(feature = "image")]
frame_tint_key: Option<(u64, usize, bool, bool)>,
}
#[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,
state: IconState::Normal,
scale_factor: 1.0,
#[cfg(feature = "image")]
frames: crate::compat::Vec::new(),
#[cfg(feature = "image")]
frame_index: 0,
#[cfg(feature = "image")]
frame_elapsed_ms: 0,
#[cfg(feature = "image")]
frame_interval_ms: ICON_FRAME_INTERVAL_MS,
#[cfg(feature = "image")]
frame_tint: crate::compat::Vec::new(),
#[cfg(feature = "image")]
frames_generation: 0,
#[cfg(feature = "image")]
frame_tint_key: None,
}
}
pub fn set_icon(&mut self, name: &str) {
if self.icon_name == name && self.icon_source.is_none() {
return;
}
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_state(&mut self, state: IconState) {
if self.state == state {
return;
}
self.state = state;
self.base.request_redraw();
}
pub fn state(&self) -> IconState {
self.state
}
pub fn set_scale_factor(&mut self, factor: f32) {
let factor = if factor.is_finite() { factor.max(1.0) } else { 1.0 };
if (self.scale_factor - factor).abs() <= f32::EPSILON {
return;
}
self.scale_factor = factor;
self.base.request_redraw();
}
pub fn scale_factor(&self) -> f32 {
self.scale_factor
}
pub fn set_size(&mut self, size: f32) {
let size = size.max(4.0);
if self.size == size {
return;
}
self.size = size;
self.base.request_redraw();
}
pub fn size(&self) -> f32 {
self.size
}
pub fn set_color(&mut self, color: Color) {
if self.color == Some(color) {
return;
}
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
}
#[cfg(feature = "image")]
pub fn set_frames(&mut self, frames: crate::compat::Vec<Image>) {
if frame_lists_identical(&self.frames, &frames) {
return;
}
self.frames = frames;
self.frame_index = 0;
self.frame_elapsed_ms = 0;
self.frames_generation = self.frames_generation.wrapping_add(1);
self.frame_tint_key = None;
self.base.request_redraw();
}
#[cfg(feature = "image")]
pub fn frames(&self) -> &[Image] {
&self.frames
}
#[cfg(feature = "image")]
pub fn frame_interval_ms(&self) -> u32 {
self.frame_interval_ms
}
#[cfg(feature = "image")]
pub fn set_frame_interval_ms(&mut self, interval_ms: u32) {
let interval = interval_ms.max(1);
if self.frame_interval_ms == interval {
return;
}
self.frame_interval_ms = interval;
self.base.request_redraw();
}
fn draw_color(&self) -> Color {
let base = self.resolve_color();
if !self.base.is_enabled() || self.state == IconState::Disabled {
return self.disabled_tint(base);
}
if self.state == IconState::Selected {
return base.blend(&Color::PRIMARY.with_alpha(base.a), 0.35);
}
base
}
fn disabled_tint(&self, base: Color) -> Color {
let grey = ((base.r as u16 + base.g as u16 + base.b as u16) / 3) as u8;
let alpha = base.a / 2;
let grey = Color::rgba(grey, grey, grey, base.a);
let composed = match self.style().background_color {
Some(surface) => grey.blend(&surface.with_alpha(base.a), 0.25),
None => grey,
};
Color::rgba(composed.r, composed.g, composed.b, alpha)
}
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))
}
#[cfg(feature = "image")]
fn draw_frames(&mut self, ctx: &mut RenderContext) {
let Some(frame) = self.frames.get(self.frame_index) else {
return;
};
let rect = self.icon_rect();
if rect.width == 0 || rect.height == 0 {
return;
}
let crate::image::ImageData::Rgba8(pixels) = &frame.inner.data else {
return;
};
if pixels.is_empty() {
return;
}
let fitted =
crate::widget::icon_source::fit_image_into(frame.width(), frame.height(), rect);
let disabled = !self.base.is_enabled() || self.state == IconState::Disabled;
let selected = self.state == IconState::Selected && self.base.is_enabled();
if !disabled && !selected {
ctx.draw_image(fitted.x, fitted.y, fitted.width, fitted.height, pixels);
return;
}
let key = (self.frames_generation, self.frame_index, disabled, selected);
if self.frame_tint_key != Some(key) || self.frame_tint.is_empty() {
self.frame_tint.clear();
self.frame_tint.extend_from_slice(pixels);
if disabled {
for chunk in self.frame_tint.as_chunks_mut::<4>().0 {
let luma =
((chunk[0] as u32 * 299 + chunk[1] as u32 * 587 + chunk[2] as u32 * 114)
/ 1000) as u8;
chunk[0] = luma;
chunk[1] = luma;
chunk[2] = luma;
chunk[3] /= 2;
}
} else {
let (r, g, b) =
(Color::PRIMARY.r as f32, Color::PRIMARY.g as f32, Color::PRIMARY.b as f32);
for chunk in self.frame_tint.as_chunks_mut::<4>().0 {
chunk[0] = (chunk[0] as f32 * 0.65 + r * 0.35).round() as u8;
chunk[1] = (chunk[1] as f32 * 0.65 + g * 0.35).round() as u8;
chunk[2] = (chunk[2] as f32 * 0.65 + b * 0.35).round() as u8;
}
}
self.frame_tint_key = Some(key);
}
ctx.draw_image(fitted.x, fitted.y, fitted.width, fitted.height, &self.frame_tint);
}
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.draw_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_scaled(
&self.icon_name,
self.scale_factor,
) {
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.draw_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.draw_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.draw_color();
let cx = r.x;
let cy = r.y;
let s = r.width as i32;
static UNKNOWN_FONT: crate::compat::OnceLock<crate::core::Font> =
crate::compat::OnceLock::new();
let font = UNKNOWN_FONT.get_or_init(|| crate::core::Font::simple("sans-serif", 12.0));
ctx.draw_text(
Point::new(cx + s / 4, cy + s * 3 / 4),
"?",
font,
c,
HorizontalAlignment::Left,
);
}
}
#[cfg(all(widgets_unstripped, feature = "image"))]
fn frame_lists_identical(left: &[Image], right: &[Image]) -> bool {
left.len() == right.len()
&& left.iter().zip(right.iter()).all(|(a, b)| {
a.width() == b.width()
&& a.height() == b.height()
&& same_pixel_buffer(&a.inner.data, &b.inner.data)
})
}
#[cfg(all(widgets_unstripped, feature = "image"))]
fn same_pixel_buffer(left: &crate::image::ImageData, right: &crate::image::ImageData) -> bool {
if core::mem::discriminant(left) != core::mem::discriminant(right) {
return false;
}
let (left, right) = (left.as_bytes(), right.as_bytes());
left.len() == right.len() && left.as_ptr() == right.as_ptr()
}
#[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)
}
fn tick(&mut self, delta_ms: u32) -> bool {
#[cfg(feature = "image")]
{
if self.frames.len() < 2 {
return false;
}
let interval = self.frame_interval_ms.max(1);
self.frame_elapsed_ms = self.frame_elapsed_ms.saturating_add(delta_ms);
let mut advanced = false;
while self.frame_elapsed_ms >= interval {
self.frame_elapsed_ms -= interval;
self.frame_index = (self.frame_index + 1) % self.frames.len();
advanced = true;
}
if advanced {
self.base.request_redraw();
}
true
}
#[cfg(not(feature = "image"))]
{
let _ = delta_ms;
false
}
}
fn is_animating(&self) -> bool {
#[cfg(feature = "image")]
{
self.frames.len() > 1
}
#[cfg(not(feature = "image"))]
{
false
}
}
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" => match value {
CapabilityValue::Null => {
self.clear_color();
Ok(())
}
other => {
let raw = expect_string(other)?;
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;
}
#[cfg(feature = "image")]
if !self.frames.is_empty() {
self.draw_frames(context);
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");
}
fn render_icon_pixels(icon: &mut Icon, width: u32, height: u32) -> Vec<u8> {
use crate::render::{PaintBackend, RenderContext, SoftwarePaintBackend};
use crate::widget::draw::Draw;
let mut backend = SoftwarePaintBackend::new(crate::core::Size::new(width, height), 1.0);
backend.begin_frame(Color::TRANSPARENT);
{
let mut context = RenderContext::new(&mut backend);
icon.draw(&mut context);
}
backend.end_frame();
backend.frame_rgba().to_vec()
}
#[test]
fn disabled_state_greys_the_icon_and_halves_alpha() {
static BLOCK: IconData =
IconData { name: "solid-block", grid: 24, paths: &["M2-2H22V-22H2Z"] };
let mut icon = Icon::new(Rect::new(0, 0, 24, 24));
icon.set_icon_source(IconSource::static_geometry(&BLOCK));
icon.set_color(Color::rgb(200, 20, 20));
let normal = render_icon_pixels(&mut icon, 24, 24);
let normal_pixel = &normal[(12 * 24 + 12) * 4..][..4];
assert_eq!(normal_pixel, &[200, 20, 20, 255], "the enabled icon is untouched");
icon.set_state(IconState::Disabled);
let disabled = render_icon_pixels(&mut icon, 24, 24);
let disabled_pixel = &disabled[(12 * 24 + 12) * 4..][..4];
assert_eq!(disabled_pixel[0], disabled_pixel[1], "disabled ink is desaturated");
assert_eq!(disabled_pixel[1], disabled_pixel[2], "disabled ink is desaturated");
assert!(
disabled_pixel[3] < normal_pixel[3],
"disabled ink must reduce the alpha (normal {} vs disabled {})",
normal_pixel[3],
disabled_pixel[3]
);
icon.set_state(IconState::Normal);
icon.set_enabled(false);
let by_flag = render_icon_pixels(&mut icon, 24, 24);
let flag_pixel = &by_flag[(12 * 24 + 12) * 4..][..4];
let spread = flag_pixel[..3].iter().max().unwrap() - flag_pixel[..3].iter().min().unwrap();
assert!(spread <= 8, "base-disabled ink is desaturated (spread {spread})");
assert!(flag_pixel[0] < 160, "the original red channel must not survive");
assert!(
flag_pixel[3] < normal_pixel[3],
"base-disabled ink must reduce the alpha (normal {} vs disabled {})",
normal_pixel[3],
flag_pixel[3]
);
}
#[test]
fn state_defaults_to_normal_and_selection_tints_the_colour() {
static BLOCK: IconData =
IconData { name: "solid-block", grid: 24, paths: &["M2-2H22V-22H2Z"] };
let mut icon = Icon::new(Rect::new(0, 0, 24, 24));
icon.set_icon_source(IconSource::static_geometry(&BLOCK));
icon.set_color(Color::rgb(200, 20, 20));
assert_eq!(icon.state(), IconState::Normal, "a new icon is in the normal state");
let normal = render_icon_pixels(&mut icon, 24, 24);
icon.set_state(IconState::Selected);
assert_eq!(icon.state(), IconState::Selected);
let selected = render_icon_pixels(&mut icon, 24, 24);
assert_ne!(normal, selected, "the selected state must tint the resolved colour");
icon.set_state(IconState::Normal);
assert_eq!(render_icon_pixels(&mut icon, 24, 24), normal, "Normal stays byte-identical");
}
#[test]
fn a_two_x_source_is_preferred_at_scale_factor_two() {
use crate::widget::clear_registered_icons;
use crate::widget::display_widgets::icon_data_set::{
lookup_registered_scaled, register_icon_data_with_density,
};
use crate::widget::svg::render_to_svg;
static BASE: [&str; 1] = ["M2-2H10V-10H2Z"];
static RETINA: [&str; 1] = ["M14-14H22V-22H14Z"];
let name = "icon_density_probe";
clear_registered_icons();
assert!(register_icon_data_with_density(name, &BASE, 24, 1));
assert!(register_icon_data_with_density(name, &RETINA, 24, 2));
assert_eq!(
lookup_registered_scaled(name, 1.0).map(|data| data.paths[0]),
Some(BASE[0]),
"the ordinary density wins below 2x"
);
assert_eq!(
lookup_registered_scaled(name, 2.0).map(|data| data.paths[0]),
Some(RETINA[0]),
"the 2x source wins at 2x"
);
let mut icon = Icon::new(Rect::new(0, 0, 24, 24));
icon.set_icon(name);
assert_eq!(icon.scale_factor(), 1.0, "the default scale is the historical 1.0");
let standard = render_to_svg(&mut icon);
icon.set_scale_factor(2.0);
assert_eq!(icon.scale_factor(), 2.0);
let retina = render_to_svg(&mut icon);
assert_ne!(standard, retina, "the widget must draw the density-2 source at 2x");
clear_registered_icons();
}
#[cfg(feature = "image")]
#[test]
fn frames_advance_when_ticked() {
use crate::image::{Image, ImageData};
let red = Image::from_raw(ImageData::Rgba8([255u8, 0, 0, 255].repeat(24 * 24)), 24, 24);
let blue = Image::from_raw(ImageData::Rgba8([0u8, 0, 255, 255].repeat(24 * 24)), 24, 24);
let mut icon = Icon::new(Rect::new(0, 0, 24, 24));
icon.set_frames(vec![red, blue]);
assert_eq!(icon.frames().len(), 2);
assert!(Widget::is_animating(&icon), "two frames owe animation frames");
let first = render_icon_pixels(&mut icon, 24, 24);
let centre = (12 * 24 + 12) * 4;
assert_eq!(&first[centre..centre + 4], &[255, 0, 0, 255], "the first frame is drawn");
assert!(Widget::tick(&mut icon, ICON_FRAME_INTERVAL_MS));
let second = render_icon_pixels(&mut icon, 24, 24);
assert_eq!(&second[centre..centre + 4], &[0, 0, 255, 255], "the second frame is drawn");
assert!(Widget::tick(&mut icon, ICON_FRAME_INTERVAL_MS));
let third = render_icon_pixels(&mut icon, 24, 24);
assert_eq!(&third[centre..centre + 4], &[255, 0, 0, 255], "the list loops");
}
#[cfg(feature = "image")]
#[test]
fn a_tinted_frame_is_cached_between_frames() {
use crate::image::Image;
let mut icon = Icon::new(crate::widget::census::CENSUS_RECT);
let frame = Image::try_from_rgba(
vec![255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255],
2,
2,
)
.expect("the fixture is valid");
icon.set_frames(crate::compat::vec![frame]);
icon.set_state(IconState::Disabled);
let _ = render_icon_pixels(&mut icon, 24, 24);
assert!(icon.frame_tint_key.is_some(), "the first draw computes the tint");
let generation = icon.frames_generation;
let ptr = icon.frame_tint.as_ptr();
let _ = render_icon_pixels(&mut icon, 24, 24);
assert_eq!(icon.frame_tint.as_ptr(), ptr, "a still, same-state frame reuses the buffer");
assert_eq!(icon.frame_tint_key.unwrap().0, generation);
let other = Image::try_from_rgba(
vec![0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255],
2,
2,
)
.expect("the fixture is valid");
icon.set_frames(crate::compat::vec![other]);
assert!(icon.frame_tint_key.is_none(), "a new movie invalidates the tint");
}
}