topola/topola-egui/src/layers_panel.rs

276 lines
12 KiB
Rust

// SPDX-FileCopyrightText: 2026 Topola contributors
//
// SPDX-License-Identifier: MIT OR Apache-2.0
use std::collections::BTreeMap;
use egui::{Context, Grid, ScrollArea, SidePanel, widget_text::WidgetText};
use serde::{Deserialize, Serialize};
use topola::board::{Board, LayerDesc, LayerSide, LayerType};
use topola::layout::LayerId;
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Colors {
pub layers: ColorLayers,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct ColorLayers {
default: LayerColors,
colors: BTreeMap<LayerDesc, LayerColors>,
}
impl ColorLayers {
pub fn colors(&self, layer_desc: Option<&LayerDesc>) -> &LayerColors {
layer_desc
.map(|layer_desc| self.colors.get(layer_desc).unwrap_or(&self.default))
.unwrap_or(&self.default)
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct LayerColors {
pub normal: egui::Color32,
pub highlighted: egui::Color32,
pub pin_selected: egui::Color32,
pub pin_selected_highlighted: egui::Color32,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct LayersPanel {
// TODO:
// In1.Cu shall be #7fc87f (#d5ecd5 when selected).
// In2.Cu shall be #ce7d2c (#e8c39e when selected).
dark_colors: Colors,
light_colors: Colors,
#[serde(skip)]
pub active: LayerId,
#[serde(skip)]
pub visible: Box<[bool]>,
}
impl LayersPanel {
pub fn new(board: &Board) -> Self {
let mut dark_layer_colors = BTreeMap::new();
let mut light_layer_colors = BTreeMap::new();
for layer_index in 0..*board.layout().layer_count() {
let layer = LayerId::new(layer_index);
let Some(layer_desc) = board.layer_desc(layer) else {
continue;
};
let dark_light_colors = match layer_desc.typ {
LayerType::Copper => match layer_desc.side {
LayerSide::Top => Some((
LayerColors {
normal: egui::Color32::from_rgb(255, 52, 52),
highlighted: egui::Color32::from_rgb(255, 100, 100),
pin_selected: egui::Color32::from_rgb(190, 200, 70),
pin_selected_highlighted: egui::Color32::from_rgb(210, 240, 110),
},
LayerColors {
normal: egui::Color32::from_rgb(255, 27, 27),
highlighted: egui::Color32::from_rgb(255, 52, 52),
pin_selected: egui::Color32::from_rgb(160, 170, 50),
pin_selected_highlighted: egui::Color32::from_rgb(190, 210, 80),
},
)),
LayerSide::Bottom => Some((
LayerColors {
normal: egui::Color32::from_rgb(52, 52, 255),
highlighted: egui::Color32::from_rgb(100, 100, 255),
pin_selected: egui::Color32::from_rgb(70, 190, 190),
pin_selected_highlighted: egui::Color32::from_rgb(100, 230, 230),
},
LayerColors {
normal: egui::Color32::from_rgb(27, 27, 255),
highlighted: egui::Color32::from_rgb(52, 52, 255),
pin_selected: egui::Color32::from_rgb(50, 160, 160),
pin_selected_highlighted: egui::Color32::from_rgb(70, 200, 200),
},
)),
LayerSide::Inner => (layer_desc.index % 2 == 0)
.then_some((
LayerColors {
normal: egui::Color32::from_rgb(127, 200, 127),
highlighted: egui::Color32::from_rgb(213, 236, 213),
pin_selected: egui::Color32::from_rgb(100, 230, 100),
pin_selected_highlighted: egui::Color32::from_rgb(170, 250, 170),
},
LayerColors {
normal: egui::Color32::from_rgb(76, 169, 76),
highlighted: egui::Color32::from_rgb(127, 200, 127),
pin_selected: egui::Color32::from_rgb(60, 200, 60),
pin_selected_highlighted: egui::Color32::from_rgb(100, 230, 100),
},
))
.or(Some((
LayerColors {
normal: egui::Color32::from_rgb(206, 125, 44),
highlighted: egui::Color32::from_rgb(232, 195, 158),
pin_selected: egui::Color32::from_rgb(170, 200, 70),
pin_selected_highlighted: egui::Color32::from_rgb(200, 230, 120),
},
LayerColors {
normal: egui::Color32::from_rgb(183, 80, 12),
highlighted: egui::Color32::from_rgb(206, 125, 44),
pin_selected: egui::Color32::from_rgb(140, 170, 50),
pin_selected_highlighted: egui::Color32::from_rgb(170, 200, 80),
},
))),
},
LayerType::Outline => match layer_desc.side {
LayerSide::Top => Some((
LayerColors {
normal: egui::Color32::from_rgb(222, 217, 141),
highlighted: egui::Color32::from_rgb(255, 255, 215),
pin_selected: egui::Color32::from_rgb(170, 235, 100),
pin_selected_highlighted: egui::Color32::from_rgb(210, 255, 160),
},
LayerColors {
normal: egui::Color32::from_rgb(185, 180, 110),
highlighted: egui::Color32::from_rgb(245, 240, 165),
pin_selected: egui::Color32::from_rgb(140, 210, 80),
pin_selected_highlighted: egui::Color32::from_rgb(190, 245, 130),
},
)),
LayerSide::Bottom => Some((
LayerColors {
normal: egui::Color32::from_rgb(212, 158, 147),
highlighted: egui::Color32::from_rgb(255, 228, 210),
pin_selected: egui::Color32::from_rgb(160, 210, 110),
pin_selected_highlighted: egui::Color32::from_rgb(200, 245, 165),
},
LayerColors {
normal: egui::Color32::from_rgb(170, 130, 120),
highlighted: egui::Color32::from_rgb(235, 195, 185),
pin_selected: egui::Color32::from_rgb(130, 190, 85),
pin_selected_highlighted: egui::Color32::from_rgb(175, 230, 140),
},
)),
LayerSide::Inner => None,
},
};
if let Some((dark_color, light_color)) = dark_light_colors {
dark_layer_colors.insert(layer_desc.clone(), dark_color);
light_layer_colors.insert(layer_desc.clone(), light_color);
}
}
let dark_colors = Colors {
layers: ColorLayers {
default: LayerColors {
normal: egui::Color32::from_rgb(255, 255, 255),
highlighted: egui::Color32::from_rgb(255, 255, 255),
pin_selected: egui::Color32::from_rgb(200, 255, 200),
pin_selected_highlighted: egui::Color32::from_rgb(220, 255, 220),
},
colors: dark_layer_colors,
},
};
let light_colors = Colors {
layers: ColorLayers {
default: LayerColors {
normal: egui::Color32::from_rgb(0, 0, 0),
highlighted: egui::Color32::from_rgb(0, 0, 0),
pin_selected: egui::Color32::from_rgb(0, 100, 0),
pin_selected_highlighted: egui::Color32::from_rgb(0, 140, 0),
},
colors: light_layer_colors,
},
};
let layer_count = board.layout().layer_count();
let visible = core::iter::repeat(true)
.take(*layer_count)
.collect::<Box<[_]>>();
Self {
dark_colors,
light_colors,
active: board.layer_id("F.Cu").unwrap_or(LayerId::new(0)),
visible,
}
}
pub fn update(&mut self, ctx: &Context, board: &Board) {
SidePanel::right("appearance_panel").show(ctx, |ui| {
ui.label("Layers");
let row_height = ui.spacing().interact_size.y;
ScrollArea::vertical().show_rows(
ui,
row_height,
self.visible.len(),
|ui, row_range| {
let start_row = row_range.start;
Grid::new("appearance_layers")
.min_col_width(ui.spacing().icon_width)
.num_columns(3)
.start_row(start_row)
.show(ui, |ui| {
for layer_index in row_range {
let layer = LayerId::new(layer_index);
let visible = &mut self.visible[layer.index()];
let layer_name = board.layer_name(layer);
ui.radio_value(&mut self.active, layer, WidgetText::default());
ui.checkbox(visible, WidgetText::default());
ui.label(layer_name.unwrap_or_else(|| {
format!("{} - Unnamed layer", layer.index())
}));
ui.end_row();
}
})
},
);
});
}
pub fn colors(&self, ctx: &Context) -> &Colors {
match ctx.theme() {
egui::Theme::Dark => &self.dark_colors,
egui::Theme::Light => &self.light_colors,
}
}
pub fn layer_colors(&self, ctx: &Context, layer_desc: Option<&LayerDesc>) -> &LayerColors {
self.colors(ctx).layers.colors(layer_desc)
}
pub fn layer_color(
&self,
ctx: &Context,
layer_desc: Option<&LayerDesc>,
pin_selected: bool,
highlight: bool,
) -> egui::Color32 {
let colors = self.colors(ctx).layers.colors(layer_desc);
if pin_selected {
if highlight {
colors.pin_selected_highlighted
} else {
colors.pin_selected
}
} else if highlight {
colors.highlighted
} else {
colors.normal
}
}
pub fn layers_in_display_order(&self, layer_count: usize) -> Vec<LayerId> {
let active = self.active.index();
let mut layers = (0..layer_count)
.rev()
.filter(|&layer| layer != active)
.map(LayerId::new)
.collect::<Vec<_>>();
// The active layer should be drawn on top.
layers.push(self.active);
layers
}
}