use cranpose_core::{NodeId, rememberKeyed};
use cranpose_ui_graphics::{Brush, Color, VectorPath};
use cranpose_ui_layout::Alignment;
use crate::{
Modifier, SemanticsWidgetRole, Size, composable,
interaction::{MutableInteractionSource, rememberMutableInteractionSource},
widgets::{Box, BoxSpec},
};
const ICON_VIEW_BOX: f32 = 24.0;
pub const DEFAULT_ICON_SIZE: f32 = 24.0;
pub const MINIMUM_TOUCH_TARGET: f32 = 48.0;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct IconSpec {
pub size: f32,
pub tint: Option<Color>,
}
impl Default for IconSpec {
fn default() -> Self {
Self {
size: DEFAULT_ICON_SIZE,
tint: None,
}
}
}
impl IconSpec {
pub const fn sized(size: f32) -> Self {
Self { size, tint: None }
}
pub const fn with_size(mut self, size: f32) -> Self {
self.size = size;
self
}
pub const fn with_tint(mut self, tint: Color) -> Self {
self.tint = Some(tint);
self
}
}
#[composable]
pub fn Icon(
path: &'static str,
content_description: Option<String>,
size: f32,
color: Color,
) -> NodeId {
IconWith(
Modifier::empty(),
path,
IconSpec::sized(size).with_tint(color),
content_description,
)
}
#[composable]
pub fn IconWith(
modifier: Modifier,
path: &'static str,
spec: IconSpec,
content_description: Option<String>,
) -> NodeId {
let parsed = rememberKeyed(path, |value| VectorPath::parse(value).ok());
let size = spec.size;
let tint = spec.tint;
let modifier = modifier
.size(Size::new(size, size))
.draw_behind(move |scope| {
if let Some(path) = &parsed {
let scaled = path.scaled(size / ICON_VIEW_BOX);
match tint {
Some(tint) => scope.draw_vector_path(&scaled, Brush::solid(tint)),
None => {
scope.draw_vector_path(&scaled, Brush::solid(Color(0.0, 0.0, 0.0, 1.0)));
}
}
}
});
let modifier = match content_description {
Some(description) => modifier.semantics_spec(
cranpose_foundation::SemanticsSpec::new()
.content_description(description)
.role(SemanticsWidgetRole::Image),
),
None => modifier,
};
Box(modifier, BoxSpec::default(), || {})
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct IconButtonColors {
pub background: Option<Color>,
pub pressed_background: Option<Color>,
pub disabled_background: Option<Color>,
}
impl IconButtonColors {
pub const fn with_background(mut self, color: Color) -> Self {
self.background = Some(color);
self
}
pub const fn with_pressed_background(mut self, color: Color) -> Self {
self.pressed_background = Some(color);
self
}
pub const fn with_disabled_background(mut self, color: Color) -> Self {
self.disabled_background = Some(color);
self
}
fn surface(&self, enabled: bool, pressed: bool) -> Option<Color> {
if !enabled {
return self.disabled_background.or(self.background);
}
if pressed {
return self.pressed_background.or(self.background);
}
self.background
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct IconButtonSpec {
pub enabled: bool,
pub touch_target: f32,
pub colors: IconButtonColors,
}
impl Default for IconButtonSpec {
fn default() -> Self {
Self {
enabled: true,
touch_target: MINIMUM_TOUCH_TARGET,
colors: IconButtonColors::default(),
}
}
}
impl IconButtonSpec {
pub const fn with_enabled(mut self, enabled: bool) -> Self {
self.enabled = enabled;
self
}
pub fn with_touch_target(mut self, size: f32) -> Self {
self.touch_target = size.max(MINIMUM_TOUCH_TARGET);
self
}
pub const fn with_colors(mut self, colors: IconButtonColors) -> Self {
self.colors = colors;
self
}
pub fn resolved_touch_target(&self) -> f32 {
self.touch_target.max(MINIMUM_TOUCH_TARGET)
}
}
#[composable]
pub fn IconButton<F>(
modifier: Modifier,
content_description: impl Into<String>,
on_click: impl Fn() + 'static,
content: F,
) -> NodeId
where
F: FnMut() + 'static,
{
IconButtonWith(
modifier,
content_description,
IconButtonSpec::default(),
None,
on_click,
content,
)
}
#[composable]
pub fn IconButtonWith<F>(
modifier: Modifier,
content_description: impl Into<String>,
spec: IconButtonSpec,
interaction_source: Option<MutableInteractionSource>,
on_click: impl Fn() + 'static,
content: F,
) -> NodeId
where
F: FnMut() + 'static,
{
let description = content_description.into();
let enabled = spec.enabled;
let target = spec.resolved_touch_target();
let source = interaction_source.unwrap_or_else(rememberMutableInteractionSource);
let pressed = source.collectIsPressedAsState().get();
let mut modifier = modifier
.size(Size::new(target, target))
.press_interaction_source(source)
.semantics(move |config| {
config.content_description = Some(description.clone());
config.role = Some(SemanticsWidgetRole::Button);
config.enabled = enabled;
config.is_clickable = enabled;
});
if let Some(surface) = spec.colors.surface(enabled, pressed) {
modifier = modifier.background(surface);
}
if enabled {
modifier = modifier.clickable(move |_| on_click());
}
Box(
modifier,
BoxSpec::default().content_alignment(Alignment::CENTER),
content,
)
}
#[cfg(test)]
#[path = "tests/icon_tests.rs"]
mod tests;