#[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::{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() {
draw_outline_in(ctx, rect, color, &data);
return;
}
draw_fallback_in(ctx, rect, color, name.as_str());
}
pub fn draw_icon_named(ctx: &mut RenderContext, rect: Rect, color: Color, name: &str) -> bool {
if rect.width == 0 || rect.height == 0 {
return false;
}
#[cfg(any(feature = "icons", widgets_unstripped))]
{
#[cfg(feature = "icons")]
if let Some(known) = IconName::from_name(name) {
if let Some(data) = known.data_opt() {
draw_outline_in(ctx, rect, color, &data);
return true;
}
}
if let Some(data) = crate::widget::display_widgets::icon_data_set::lookup_registered(name) {
draw_outline_in(ctx, rect, color, &data);
return true;
}
}
if crate::widget::icon_fallback_data::ICON_FALLBACK.iter().any(|entry| entry.name == name) {
draw_fallback_in(ctx, rect, color, name);
return true;
}
false
}
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) {
use crate::render::path::{
flatten_paths, IconPlacement, MAX_OUTLINE_CONTOURS, MAX_OUTLINE_POINTS,
};
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];
let Ok(count) = flatten_paths(data.paths, placement, &mut points, &mut contours) else {
return;
};
for &(start, end) in &contours[..count] {
let Some(contour) = points.get(start..end) else {
continue;
};
ctx.draw_path(contour, true, color, true, 0);
}
}
fn draw_fallback_in(ctx: &mut RenderContext, rect: Rect, color: Color, token: &str) {
use crate::widget::icon_fallback_data::ICON_FALLBACK;
let Some(fallback) = ICON_FALLBACK.iter().find(|entry| entry.name == token) else {
return;
};
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 points: crate::compat::Vec<Point> = 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,
)
})
.collect();
ctx.draw_path(&points, true, color, true, 0);
}
}
#[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,
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(),
size: 24.0,
color: None,
}
}
pub fn set_icon(&mut self, name: &str) {
self.icon_name = name.to_string();
self.base.request_redraw();
}
pub fn icon(&self) -> &str {
&self.icon_name
}
pub fn set_icon_enum(&mut self, icon: IconName) {
self.set_icon(icon.as_str());
}
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) {
#[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 !crate::widget::icon_fallback_data::ICON_FALLBACK.iter().any(|entry| entry.name == token)
{
self.draw_unknown(ctx);
return;
}
draw_fallback_in(ctx, rect, self.resolve_color(), token);
}
fn draw_outline_data(&self, ctx: &mut RenderContext, data: &IconData) {
let rect = self.icon_rect();
if rect.width == 0 || rect.height == 0 {
return;
}
draw_outline_in(ctx, rect, self.resolve_color(), data);
}
#[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 {
crate::core::Size::new(24, 24)
}
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())),
"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(())
}
"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", "size", "color", BASE_PROPERTY_NAMES]
}
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 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 });
}
#[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(feature = "mini")))]
#[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
);
}
}
}
#[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}"
);
}
}