596 lines
21 KiB
Rust
596 lines
21 KiB
Rust
//! Layers panel — Photoshop-like layer management with thumbnails, badges,
|
|
//! blend mode selector, opacity/fill controls, group hierarchy, and effects display.
|
|
//!
|
|
//! Layout (top to bottom):
|
|
//! 1. Active layer controls — blend mode pick_list, opacity slider, lock toggle
|
|
//! 2. Scrollable layer list — each row shows indent, collapse arrow, visibility, thumbnail,
|
|
//! name, FX icon, and type badge. Layers with effects show expandable effect sub-items.
|
|
//! 3. Bottom toolbar — add layer, add group, flatten, delete, move up/down
|
|
//!
|
|
//! Z-order convention:
|
|
//! The engine renders layers in array order: the first layer is at the back, the
|
|
//! last layer is at the front. The UI list mirrors this order, so the topmost
|
|
//! (front-most) layer appears at the bottom of the list. Therefore "Move Up" in
|
|
//! the toolbar moves the layer toward the end of the array / front of the canvas,
|
|
//! and "Move Down" moves it toward the start / back.
|
|
|
|
use crate::app::Message;
|
|
use crate::panels::styles;
|
|
use crate::panels::typography::BODY;
|
|
use crate::theme::ThemeColors;
|
|
use crate::widgets::plain_slider::plain_slider;
|
|
use hcie_engine_api::{BlendMode, LayerInfo, LayerType};
|
|
use iced::widget::{
|
|
button, column, container, horizontal_rule, pick_list, row, scrollable, svg, text, Space,
|
|
};
|
|
use iced::{Element, Length};
|
|
use std::path::Path;
|
|
|
|
/// Wrapper for BlendMode to provide Display for pick_list.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
struct BlendModeItem(BlendMode);
|
|
|
|
impl std::fmt::Display for BlendModeItem {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
write!(f, "{}", self.0.label())
|
|
}
|
|
}
|
|
|
|
impl From<BlendMode> for BlendModeItem {
|
|
fn from(m: BlendMode) -> Self {
|
|
Self(m)
|
|
}
|
|
}
|
|
|
|
impl From<BlendModeItem> for BlendMode {
|
|
fn from(m: BlendModeItem) -> Self {
|
|
m.0
|
|
}
|
|
}
|
|
|
|
const BLEND_MODE_ITEMS: &[BlendModeItem] = &[
|
|
BlendModeItem(BlendMode::Normal),
|
|
BlendModeItem(BlendMode::Dissolve),
|
|
BlendModeItem(BlendMode::Darken),
|
|
BlendModeItem(BlendMode::Multiply),
|
|
BlendModeItem(BlendMode::ColorBurn),
|
|
BlendModeItem(BlendMode::LinearBurn),
|
|
BlendModeItem(BlendMode::DarkerColor),
|
|
BlendModeItem(BlendMode::Lighten),
|
|
BlendModeItem(BlendMode::Screen),
|
|
BlendModeItem(BlendMode::ColorDodge),
|
|
BlendModeItem(BlendMode::LinearDodge),
|
|
BlendModeItem(BlendMode::LighterColor),
|
|
BlendModeItem(BlendMode::Overlay),
|
|
BlendModeItem(BlendMode::SoftLight),
|
|
BlendModeItem(BlendMode::HardLight),
|
|
BlendModeItem(BlendMode::VividLight),
|
|
BlendModeItem(BlendMode::LinearLight),
|
|
BlendModeItem(BlendMode::PinLight),
|
|
BlendModeItem(BlendMode::HardMix),
|
|
BlendModeItem(BlendMode::Difference),
|
|
BlendModeItem(BlendMode::Exclusion),
|
|
BlendModeItem(BlendMode::Subtract),
|
|
BlendModeItem(BlendMode::Divide),
|
|
BlendModeItem(BlendMode::Hue),
|
|
BlendModeItem(BlendMode::Saturation),
|
|
BlendModeItem(BlendMode::Color),
|
|
BlendModeItem(BlendMode::Luminosity),
|
|
BlendModeItem(BlendMode::PassThrough),
|
|
];
|
|
|
|
/// Tree entry for rendering nested layer hierarchy.
|
|
struct LayerEntry<'a> {
|
|
info: &'a LayerInfo,
|
|
depth: usize,
|
|
is_group: bool,
|
|
}
|
|
|
|
/// Build a flat rendering order from the layer list, respecting group hierarchy.
|
|
/// Layers are returned in visual order: the top/front-most layer first and the
|
|
/// bottom/back-most layer last. This matches Photoshop-style panels where the
|
|
/// layer you see on top of the canvas appears at the top of the list.
|
|
fn build_tree<'a>(layers: &'a [LayerInfo]) -> Vec<LayerEntry<'a>> {
|
|
let mut children_of: std::collections::HashMap<Option<u64>, Vec<&LayerInfo>> =
|
|
std::collections::HashMap::new();
|
|
for info in layers.iter().rev() {
|
|
children_of.entry(info.parent_id).or_default().push(info);
|
|
}
|
|
|
|
let mut result = Vec::new();
|
|
fn walk<'a>(
|
|
parent_id: Option<u64>,
|
|
depth: usize,
|
|
children_of: &std::collections::HashMap<Option<u64>, Vec<&'a LayerInfo>>,
|
|
result: &mut Vec<LayerEntry<'a>>,
|
|
) {
|
|
if let Some(children) = children_of.get(&parent_id) {
|
|
// children is already in reverse-engine order (front-most first); walk
|
|
// it as-is so the list renders front-to-back.
|
|
for info in children {
|
|
let is_group = info.layer_type == LayerType::Group;
|
|
result.push(LayerEntry {
|
|
info,
|
|
depth,
|
|
is_group,
|
|
});
|
|
if is_group && !info.collapsed {
|
|
walk(Some(info.id), depth + 1, children_of, result);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
walk(None, 0, &children_of, &mut result);
|
|
result
|
|
}
|
|
|
|
/// Photoshop-like flat button style (no background, subtle border on hover).
|
|
fn flat_btn_style(colors: ThemeColors) -> iced::widget::button::Style {
|
|
iced::widget::button::Style {
|
|
background: Some(iced::Background::Color(iced::Color::TRANSPARENT)),
|
|
border: iced::Border::default().color(iced::Color::TRANSPARENT),
|
|
text_color: colors.text_primary,
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
/// Helper to create a small SVG icon button.
|
|
/// Falls back to a text button if the SVG file is missing.
|
|
fn svg_icon_btn<'a>(
|
|
icon_path: &str,
|
|
_tip: &'a str,
|
|
msg: Message,
|
|
colors: ThemeColors,
|
|
size: f32,
|
|
) -> Element<'a, Message> {
|
|
let full_path = Path::new("hcie-iced-app/assets").join(icon_path);
|
|
if full_path.exists() {
|
|
let icon = svg(svg::Handle::from_path(&full_path))
|
|
.width(size)
|
|
.height(size)
|
|
.style(move |_theme: &iced::Theme, _status: svg::Status| svg::Style {
|
|
color: Some(colors.text_primary),
|
|
});
|
|
button(icon)
|
|
.on_press(msg)
|
|
.padding([2, 3])
|
|
.style(move |_theme, _status: iced::widget::button::Status| flat_btn_style(colors))
|
|
.into()
|
|
} else {
|
|
// Fallback to text
|
|
button(text(_tip).size((size * 0.6) as u16))
|
|
.on_press(msg)
|
|
.padding([2, 3])
|
|
.style(move |_theme, _status: iced::widget::button::Status| flat_btn_style(colors))
|
|
.into()
|
|
}
|
|
}
|
|
|
|
/// Build the thumbnail element for a layer row from the cache or as a fallback icon.
|
|
fn layer_thumbnail<'a>(
|
|
entry: &LayerEntry<'a>,
|
|
thumb_cache: &std::collections::HashMap<u64, (u32, u32, Vec<u8>)>,
|
|
_thumb_gen: u64,
|
|
colors: ThemeColors,
|
|
) -> Element<'a, Message> {
|
|
if entry.is_group {
|
|
return container(text("\u{1F4C1}").size(16))
|
|
.width(50)
|
|
.height(38)
|
|
.center_y(Length::Fixed(38.0))
|
|
.into();
|
|
}
|
|
|
|
if let Some((tw, th, pixels)) = thumb_cache.get(&entry.info.id) {
|
|
let (tw32, th32) = (*tw as u32, *th as u32);
|
|
let handle = iced::widget::image::Handle::from_rgba(tw32, th32, pixels.clone());
|
|
let img = iced::widget::Image::new(handle)
|
|
.width(Length::Fixed(*tw as f32))
|
|
.height(Length::Fixed(*th as f32));
|
|
return container(img)
|
|
.width(50)
|
|
.height(38)
|
|
.center_y(Length::Fixed(38.0))
|
|
.padding(1)
|
|
.style(move |_theme| iced::widget::container::Style {
|
|
border: iced::Border::default().color(colors.border_high).width(1),
|
|
..Default::default()
|
|
})
|
|
.into();
|
|
}
|
|
|
|
// Fallback: type-based icon
|
|
let (icon_char, icon_color) = match entry.info.layer_type {
|
|
LayerType::Vector => ("V", iced::Color::from_rgb(0.4, 0.8, 0.4)),
|
|
LayerType::Text => ("T", iced::Color::from_rgb(0.4, 0.6, 1.0)),
|
|
LayerType::Mask => ("M", iced::Color::from_rgb(0.8, 0.4, 0.8)),
|
|
_ => ("\u{25A3}", colors.text_secondary),
|
|
};
|
|
|
|
container(text(icon_char).size(12).style(move |_theme: &iced::Theme| {
|
|
iced::widget::text::Style {
|
|
color: Some(icon_color),
|
|
}
|
|
}))
|
|
.width(50)
|
|
.height(38)
|
|
.center_x(Length::Fixed(50.0))
|
|
.center_y(Length::Fixed(38.0))
|
|
.style(move |_theme| iced::widget::container::Style {
|
|
background: Some(iced::Background::Color(iced::Color::from_rgba(
|
|
1.0, 1.0, 1.0, 0.05,
|
|
))),
|
|
border: iced::Border::default().color(colors.border_high).width(1),
|
|
..Default::default()
|
|
})
|
|
.into()
|
|
}
|
|
|
|
/// Build the layers panel element.
|
|
pub fn view<'a>(
|
|
layers: &'a [LayerInfo],
|
|
active_layer_id: u64,
|
|
engine: &hcie_engine_api::Engine,
|
|
colors: ThemeColors,
|
|
thumb_cache: &'a std::collections::HashMap<u64, (u32, u32, Vec<u8>)>,
|
|
thumb_gen: u64,
|
|
) -> Element<'a, Message> {
|
|
let entries = build_tree(layers);
|
|
|
|
// ── Active layer controls ──────────────────────────────
|
|
let active_info = layers.iter().find(|l| l.id == active_layer_id);
|
|
let active_blend = active_info
|
|
.map(|l| l.blend_mode)
|
|
.unwrap_or(BlendMode::Normal);
|
|
let active_opacity = active_info.map(|l| l.opacity).unwrap_or(1.0);
|
|
let active_locked = active_info.map(|l| l.locked).unwrap_or(false);
|
|
|
|
let blend_list = pick_list(
|
|
BLEND_MODE_ITEMS,
|
|
Some(BlendModeItem(active_blend)),
|
|
move |item| Message::LayerSetBlendMode(active_layer_id, item.into()),
|
|
)
|
|
.width(Length::Fill)
|
|
.text_size(BODY);
|
|
|
|
let opacity_slider = plain_slider(
|
|
"Opacity",
|
|
active_opacity,
|
|
0.0..=1.0,
|
|
0.01,
|
|
"%",
|
|
0,
|
|
colors,
|
|
move |v| Message::LayerSetOpacity(active_layer_id, v),
|
|
);
|
|
|
|
let opacity_row = opacity_slider;
|
|
|
|
let lock_icon = if active_locked {
|
|
"\u{1F512}"
|
|
} else {
|
|
"\u{1F513}"
|
|
};
|
|
let lock_btn = button(text(lock_icon).size(11))
|
|
.on_press(Message::LayerToggleLock(active_layer_id))
|
|
.padding([1, 4])
|
|
.style(move |_theme, _status| flat_btn_style(colors));
|
|
|
|
let fx_btn = button(text("fx").size(11))
|
|
.on_press(Message::OpenLayerStyleDialog)
|
|
.padding([1, 4])
|
|
.style(move |_theme, _status| flat_btn_style(colors));
|
|
|
|
let controls = column![
|
|
row![blend_list].spacing(2),
|
|
opacity_row,
|
|
text("Fill opacity: unavailable in hcie-engine-api")
|
|
.size(BODY)
|
|
.style(move |_theme: &iced::Theme| iced::widget::text::Style {
|
|
color: Some(colors.text_secondary),
|
|
}),
|
|
row![lock_btn, fx_btn]
|
|
.spacing(8)
|
|
.align_y(iced::Alignment::Center),
|
|
]
|
|
.spacing(2);
|
|
|
|
// ── Layer list ─────────────────────────────────────────
|
|
let mut layer_list = column![].spacing(1);
|
|
|
|
for entry in &entries {
|
|
let info = entry.info;
|
|
let is_active = info.id == active_layer_id;
|
|
let depth = entry.depth;
|
|
|
|
// Indent spacer
|
|
let indent: Element<'a, Message> =
|
|
Space::with_width(Length::Fixed((depth as f32) * 14.0)).into();
|
|
|
|
// Collapse arrow (groups only)
|
|
let collapse: Element<'a, Message> = if entry.is_group {
|
|
let arrow = if info.collapsed {
|
|
"\u{25B6}"
|
|
} else {
|
|
"\u{25BC}"
|
|
};
|
|
button(text(arrow).size(8))
|
|
.on_press(Message::LayerToggleCollapse(info.id))
|
|
.padding([0, 1])
|
|
.style(move |_theme, _status| flat_btn_style(colors))
|
|
.into()
|
|
} else {
|
|
Space::with_width(Length::Fixed(12.0)).into()
|
|
};
|
|
|
|
// Visibility toggle (eye icon)
|
|
let vis_icon_path = if info.visible {
|
|
"icons/visible.svg"
|
|
} else {
|
|
"icons/eye_closed.svg"
|
|
};
|
|
let vis_tip = if info.visible { "Hide" } else { "Show" };
|
|
let vis_btn = svg_icon_btn(
|
|
vis_icon_path,
|
|
vis_tip,
|
|
Message::LayerToggleVisibility(info.id),
|
|
colors,
|
|
14.0,
|
|
);
|
|
|
|
let lock_icon_path = if info.locked {
|
|
"icons/lock.svg"
|
|
} else {
|
|
"icons/unlock.svg"
|
|
};
|
|
let row_lock = svg_icon_btn(
|
|
lock_icon_path,
|
|
if info.locked { "Unlock" } else { "Lock" },
|
|
Message::LayerToggleLock(info.id),
|
|
colors,
|
|
12.0,
|
|
);
|
|
|
|
// Thumbnail
|
|
let thumb_elem = layer_thumbnail(entry, thumb_cache, thumb_gen, colors);
|
|
|
|
// Layer name
|
|
let c = if is_active {
|
|
colors.accent
|
|
} else {
|
|
colors.text_primary
|
|
};
|
|
let name_text = text(&info.name)
|
|
.size(BODY)
|
|
.width(Length::Fill)
|
|
.style(move |_theme: &iced::Theme| iced::widget::text::Style { color: Some(c) });
|
|
|
|
// "fx" icon for layers with effects (Photoshop-style)
|
|
let fx_icon: Element<'a, Message> = if info.has_effects {
|
|
button(text("fx").size(8).style(move |_theme: &iced::Theme| {
|
|
iced::widget::text::Style {
|
|
color: Some(colors.accent),
|
|
}
|
|
}))
|
|
.on_press(Message::LayerStyleToggle(0)) // Opens styles panel
|
|
.padding([0, 2])
|
|
.style(move |_theme, _status| flat_btn_style(colors))
|
|
.into()
|
|
} else {
|
|
Space::with_width(Length::Fixed(1.0)).into()
|
|
};
|
|
|
|
// Type badge
|
|
let type_badge: Element<'a, Message> = match info.layer_type {
|
|
LayerType::Vector => text("V")
|
|
.size(8)
|
|
.style(move |_theme: &iced::Theme| iced::widget::text::Style {
|
|
color: Some(iced::Color::from_rgb(0.4, 0.8, 0.4)),
|
|
})
|
|
.into(),
|
|
LayerType::Text => text("T")
|
|
.size(8)
|
|
.style(move |_theme: &iced::Theme| iced::widget::text::Style {
|
|
color: Some(iced::Color::from_rgb(0.4, 0.6, 1.0)),
|
|
})
|
|
.into(),
|
|
LayerType::Group => text("G")
|
|
.size(8)
|
|
.style(move |_theme: &iced::Theme| iced::widget::text::Style {
|
|
color: Some(iced::Color::from_rgb(0.85, 0.7, 0.3)),
|
|
})
|
|
.into(),
|
|
LayerType::Mask => text("M")
|
|
.size(8)
|
|
.style(move |_theme: &iced::Theme| iced::widget::text::Style {
|
|
color: Some(iced::Color::from_rgb(0.8, 0.4, 0.8)),
|
|
})
|
|
.into(),
|
|
_ => text("").size(8).into(),
|
|
};
|
|
|
|
// Main layer row
|
|
let name_row =
|
|
row![collapse, vis_btn, row_lock, thumb_elem, name_text, fx_icon, type_badge,]
|
|
.spacing(2)
|
|
.align_y(iced::Alignment::Center);
|
|
|
|
let layer_item = if is_active {
|
|
container(name_row)
|
|
.width(Length::Fill)
|
|
.padding([2, 3])
|
|
.style(move |_theme| {
|
|
let accent_bg = iced::Color::from_rgba(
|
|
colors.accent.r,
|
|
colors.accent.g,
|
|
colors.accent.b,
|
|
0.18,
|
|
);
|
|
iced::widget::container::Style {
|
|
background: Some(iced::Background::Color(accent_bg)),
|
|
border: iced::Border::default()
|
|
.color(iced::Color::from_rgba(
|
|
colors.accent.r,
|
|
colors.accent.g,
|
|
colors.accent.b,
|
|
0.4,
|
|
))
|
|
.width(1),
|
|
..Default::default()
|
|
}
|
|
})
|
|
} else {
|
|
container(name_row)
|
|
.width(Length::Fill)
|
|
.padding([2, 3])
|
|
.style(move |_theme| styles::inactive_item_bg(colors))
|
|
};
|
|
|
|
let clickable =
|
|
iced::widget::mouse_area(layer_item).on_press(Message::LayerSelect(info.id));
|
|
|
|
let row_with_indent = row![indent, clickable].align_y(iced::Alignment::Center);
|
|
layer_list = layer_list.push(row_with_indent);
|
|
|
|
// ── Expandable effects sub-items (Photoshop-style) ──
|
|
if info.has_effects && is_active {
|
|
let badges = engine.get_layer_style_badges(info.id);
|
|
let effects_depth = depth + 2;
|
|
|
|
for (code, enabled) in &badges {
|
|
if *enabled {
|
|
let effect_name = effect_full_name(code);
|
|
let effect_color = fx_color(code);
|
|
let effect_indent =
|
|
Space::with_width(Length::Fixed((effects_depth as f32) * 14.0));
|
|
let effect_row = row![
|
|
effect_indent,
|
|
text("\u{25B8}").size(8).style(move |_theme: &iced::Theme| {
|
|
iced::widget::text::Style {
|
|
color: Some(colors.text_secondary),
|
|
}
|
|
}),
|
|
text(effect_name)
|
|
.size(BODY)
|
|
.style(move |_theme: &iced::Theme| iced::widget::text::Style {
|
|
color: Some(effect_color),
|
|
}),
|
|
]
|
|
.spacing(2)
|
|
.align_y(iced::Alignment::Center);
|
|
|
|
layer_list = layer_list.push(effect_row);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── Bottom toolbar ─────────────────────────────────────
|
|
let add_btn = svg_icon_btn(
|
|
"icons/new_file.svg",
|
|
"New Layer",
|
|
Message::LayerAdd,
|
|
colors,
|
|
16.0,
|
|
);
|
|
let group_btn = svg_icon_btn(
|
|
"icons/panel-layers.svg",
|
|
"New Group",
|
|
Message::LayerAddGroup,
|
|
colors,
|
|
16.0,
|
|
);
|
|
let flatten_btn = button(text("Flatten").size(BODY))
|
|
.on_press(Message::LayerFlatten)
|
|
.padding([5, 8])
|
|
.style(move |_theme, _status| flat_btn_style(colors));
|
|
let del_btn = svg_icon_btn(
|
|
"icons/close.svg",
|
|
"Delete Layer",
|
|
Message::LayerDelete(active_layer_id),
|
|
colors,
|
|
14.0,
|
|
);
|
|
let move_up_btn = button(text("\u{25B2}").size(BODY))
|
|
.on_press(Message::LayerMoveUp(active_layer_id))
|
|
.padding([3, 4])
|
|
.style(move |_theme, _status| flat_btn_style(colors));
|
|
let move_down_btn = button(text("\u{25BC}").size(BODY))
|
|
.on_press(Message::LayerMoveDown(active_layer_id))
|
|
.padding([3, 4])
|
|
.style(move |_theme, _status| flat_btn_style(colors));
|
|
|
|
let fx_btn = button(text("fx").size(BODY))
|
|
.on_press(Message::OpenLayerStyleDialog)
|
|
.padding([3, 6])
|
|
.style(move |_theme, _status| flat_btn_style(colors));
|
|
let duplicate_btn = button(text("Duplicate").size(BODY)).padding([5, 8]);
|
|
let mask_btn = button(text("Mask").size(BODY)).padding([5, 8]);
|
|
let rasterize_btn = button(text("Rasterize").size(BODY)).padding([5, 8]);
|
|
let toolbar = column![
|
|
text("Unavailable: duplicate, mask, rasterize").size(BODY),
|
|
row![duplicate_btn, mask_btn]
|
|
.spacing(2)
|
|
.align_y(iced::Alignment::Center),
|
|
row![rasterize_btn].align_y(iced::Alignment::Center),
|
|
row![add_btn, group_btn]
|
|
.spacing(2)
|
|
.align_y(iced::Alignment::Center),
|
|
row![flatten_btn, fx_btn]
|
|
.spacing(2)
|
|
.align_y(iced::Alignment::Center),
|
|
row![move_up_btn, move_down_btn, del_btn]
|
|
.spacing(2)
|
|
.align_y(iced::Alignment::Center),
|
|
]
|
|
.spacing(2);
|
|
|
|
// ── Panel layout (no duplicate title — dock tab provides it) ──
|
|
let panel = column![
|
|
controls,
|
|
horizontal_rule(1),
|
|
scrollable(layer_list).height(Length::Fill),
|
|
horizontal_rule(1),
|
|
toolbar,
|
|
]
|
|
.spacing(2)
|
|
.padding(4);
|
|
|
|
container(panel)
|
|
.width(Length::Fill)
|
|
.height(Length::Fill)
|
|
.style(move |_theme| styles::panel_background(colors))
|
|
.into()
|
|
}
|
|
|
|
/// Map FX badge code to full effect name (for expandable sub-items).
|
|
fn effect_full_name(code: &str) -> &'static str {
|
|
match code {
|
|
"DS" => "Drop Shadow",
|
|
"IS" => "Inner Shadow",
|
|
"OG" => "Outer Glow",
|
|
"IG" => "Inner Glow",
|
|
"BE" => "Bevel & Emboss",
|
|
"St" => "Satin",
|
|
"CO" => "Color Overlay",
|
|
"GO" => "Gradient Overlay",
|
|
"PO" => "Pattern Overlay",
|
|
"Sr" => "Stroke",
|
|
_ => "Effect",
|
|
}
|
|
}
|
|
|
|
/// Map FX badge code to a display color (matches egui palette).
|
|
fn fx_color(code: &str) -> iced::Color {
|
|
match code {
|
|
"DS" => iced::Color::from_rgb(0.86, 0.39, 0.39),
|
|
"IS" => iced::Color::from_rgb(0.71, 0.47, 0.47),
|
|
"OG" => iced::Color::from_rgb(0.86, 0.86, 0.39),
|
|
"IG" => iced::Color::from_rgb(0.71, 0.71, 0.39),
|
|
"BE" => iced::Color::from_rgb(0.39, 0.71, 0.86),
|
|
"St" => iced::Color::from_rgb(0.71, 0.55, 0.86),
|
|
_ => iced::Color::from_rgb(0.5, 0.5, 0.5),
|
|
}
|
|
}
|