1use crate::Theme;
28use egui::{Color32, Id, Pos2, Rect, Sense, Stroke, Ui, Vec2};
29use egui_cha::ViewCtx;
30
31#[derive(Clone, Debug, Default)]
33struct LayerDragState {
34 dragging: Option<usize>,
36 drop_target: Option<usize>,
38}
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
42pub enum BlendMode {
43 #[default]
44 Normal,
45 Add,
46 Multiply,
47 Screen,
48 Overlay,
49 Difference,
50 Exclusion,
51 ColorDodge,
52 ColorBurn,
53}
54
55impl BlendMode {
56 pub fn short_name(&self) -> &'static str {
58 match self {
59 BlendMode::Normal => "Norm",
60 BlendMode::Add => "Add",
61 BlendMode::Multiply => "Mul",
62 BlendMode::Screen => "Scr",
63 BlendMode::Overlay => "Ovl",
64 BlendMode::Difference => "Diff",
65 BlendMode::Exclusion => "Excl",
66 BlendMode::ColorDodge => "Dodg",
67 BlendMode::ColorBurn => "Burn",
68 }
69 }
70
71 pub fn all() -> &'static [BlendMode] {
73 &[
74 BlendMode::Normal,
75 BlendMode::Add,
76 BlendMode::Multiply,
77 BlendMode::Screen,
78 BlendMode::Overlay,
79 BlendMode::Difference,
80 BlendMode::Exclusion,
81 BlendMode::ColorDodge,
82 BlendMode::ColorBurn,
83 ]
84 }
85}
86
87#[derive(Debug, Clone)]
89pub struct Layer {
90 pub name: String,
91 pub visible: bool,
92 pub locked: bool,
93 pub solo: bool,
94 pub opacity: f32,
95 pub blend_mode: BlendMode,
96 pub color: Option<Color32>,
97 pub thumbnail: Option<egui::TextureId>,
98}
99
100impl Layer {
101 pub fn new(name: impl Into<String>) -> Self {
102 Self {
103 name: name.into(),
104 visible: true,
105 locked: false,
106 solo: false,
107 opacity: 1.0,
108 blend_mode: BlendMode::Normal,
109 color: None,
110 thumbnail: None,
111 }
112 }
113
114 pub fn with_opacity(mut self, opacity: f32) -> Self {
115 self.opacity = opacity.clamp(0.0, 1.0);
116 self
117 }
118
119 pub fn with_blend_mode(mut self, mode: BlendMode) -> Self {
120 self.blend_mode = mode;
121 self
122 }
123
124 pub fn with_color(mut self, color: Color32) -> Self {
125 self.color = Some(color);
126 self
127 }
128
129 pub fn with_visible(mut self, visible: bool) -> Self {
130 self.visible = visible;
131 self
132 }
133}
134
135#[derive(Debug, Clone)]
137pub enum LayerEvent {
138 Select(usize),
140 ToggleVisible(usize),
142 ToggleLock(usize),
144 ToggleSolo(usize),
146 SetOpacity(usize, f32),
148 SetBlendMode(usize, BlendMode),
150 Reorder { from: usize, to: usize },
152 AddLayer,
154 DeleteLayer(usize),
156 DuplicateLayer(usize),
158}
159
160pub struct LayerStack<'a> {
162 layers: &'a [Layer],
163 selected: Option<usize>,
164 row_height: f32,
165 show_thumbnails: bool,
166 show_blend_modes: bool,
167 show_controls: bool,
168 compact: bool,
169}
170
171impl<'a> LayerStack<'a> {
172 pub fn new(layers: &'a [Layer]) -> Self {
174 Self {
175 layers,
176 selected: None,
177 row_height: 40.0,
178 show_thumbnails: true,
179 show_blend_modes: true,
180 show_controls: true,
181 compact: false,
182 }
183 }
184
185 pub fn selected(mut self, index: Option<usize>) -> Self {
187 self.selected = index;
188 self
189 }
190
191 pub fn row_height(mut self, height: f32) -> Self {
193 self.row_height = height;
194 self
195 }
196
197 pub fn show_thumbnails(mut self, show: bool) -> Self {
199 self.show_thumbnails = show;
200 self
201 }
202
203 pub fn show_blend_modes(mut self, show: bool) -> Self {
205 self.show_blend_modes = show;
206 self
207 }
208
209 pub fn show_controls(mut self, show: bool) -> Self {
211 self.show_controls = show;
212 self
213 }
214
215 pub fn compact(mut self, compact: bool) -> Self {
217 self.compact = compact;
218 if compact {
219 self.row_height = 28.0;
220 self.show_thumbnails = false;
221 }
222 self
223 }
224
225 pub fn show_with<Msg>(self, ctx: &mut ViewCtx<'_, Msg>, on_event: impl Fn(LayerEvent) -> Msg) {
227 let event = self.show_internal(ctx.ui);
228 if let Some(e) = event {
229 ctx.emit(on_event(e));
230 }
231 }
232
233 pub fn show(self, ui: &mut Ui) -> Option<LayerEvent> {
235 self.show_internal(ui)
236 }
237
238 fn show_internal(self, ui: &mut Ui) -> Option<LayerEvent> {
239 let theme = Theme::current(ui.ctx());
240 let mut event: Option<LayerEvent> = None;
241
242 let row_height = if self.compact {
243 theme.spacing_lg + theme.spacing_sm
244 } else {
245 self.row_height
246 };
247
248 let available_width = ui.available_width();
250 let _thumbnail_width = if self.show_thumbnails {
251 row_height
252 } else {
253 0.0
254 };
255 let visibility_width = theme.spacing_lg;
256 let lock_width = theme.spacing_lg;
257 let opacity_width = if self.compact { 40.0 } else { 60.0 };
258 let blend_width = if self.show_blend_modes { 45.0 } else { 0.0 };
259
260 let drag_id = Id::new("layer_stack_drag");
262 let mut drag_state: LayerDragState = ui
263 .ctx()
264 .data_mut(|d| d.get_temp(drag_id).unwrap_or_default());
265
266 if self.show_controls {
268 ui.horizontal(|ui| {
269 ui.label(
270 egui::RichText::new("Layers")
271 .size(theme.font_size_sm)
272 .color(theme.text_secondary),
273 );
274 ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
275 let add_btn =
276 ui.add(egui::Button::new("+").min_size(Vec2::splat(theme.spacing_lg)));
277 if add_btn.clicked() {
278 event = Some(LayerEvent::AddLayer);
279 }
280 });
281 });
282 ui.add_space(theme.spacing_xs);
283 }
284
285 struct LayerInfo {
287 idx: usize,
288 rect: Rect,
289 row_hovered: bool,
290 row_clicked: bool,
291 row_drag_started: bool,
292 row_dragged: bool,
293 vis_rect: Rect,
294 vis_hovered: bool,
295 vis_clicked: bool,
296 lock_rect: Rect,
297 lock_hovered: bool,
298 lock_clicked: bool,
299 blend_rect: Option<Rect>,
300 blend_hovered: bool,
301 blend_clicked: bool,
302 opacity_rect: Rect,
303 opacity_hovered: bool,
304 opacity_dragged: bool,
305 opacity_drag_pos: Option<Pos2>,
306 }
307
308 let mut layer_infos: Vec<LayerInfo> = Vec::with_capacity(self.layers.len());
309
310 for (idx, _layer) in self.layers.iter().enumerate() {
312 let (rect, response) = ui.allocate_exact_size(
313 Vec2::new(available_width, row_height),
314 Sense::click_and_drag(),
315 );
316
317 if !ui.is_rect_visible(rect) {
318 continue;
319 }
320
321 let mut x_offset = rect.min.x + theme.spacing_xs;
322
323 let vis_rect = Rect::from_min_size(
325 Pos2::new(x_offset, rect.min.y),
326 Vec2::new(visibility_width, row_height),
327 );
328 let vis_response = ui.allocate_rect(vis_rect, Sense::click());
329 x_offset += visibility_width;
330
331 let lock_rect = Rect::from_min_size(
333 Pos2::new(x_offset, rect.min.y),
334 Vec2::new(lock_width, row_height),
335 );
336 let lock_response = ui.allocate_rect(lock_rect, Sense::click());
337 let blend_rect = if self.show_blend_modes {
341 Some(Rect::from_min_size(
342 Pos2::new(
343 rect.max.x - opacity_width - blend_width - theme.spacing_xs,
344 rect.min.y,
345 ),
346 Vec2::new(blend_width, row_height),
347 ))
348 } else {
349 None
350 };
351 let blend_response = blend_rect.map(|r| ui.allocate_rect(r, Sense::click()));
352
353 let opacity_rect = Rect::from_min_size(
355 Pos2::new(
356 rect.max.x - opacity_width - theme.spacing_xs,
357 rect.min.y + row_height * 0.3,
358 ),
359 Vec2::new(opacity_width, row_height * 0.4),
360 );
361 let opacity_response = ui.allocate_rect(opacity_rect, Sense::click_and_drag());
362
363 layer_infos.push(LayerInfo {
364 idx,
365 rect,
366 row_hovered: response.hovered(),
367 row_clicked: response.clicked(),
368 row_drag_started: response.drag_started(),
369 row_dragged: response.dragged(),
370 vis_rect,
371 vis_hovered: vis_response.hovered(),
372 vis_clicked: vis_response.clicked(),
373 lock_rect,
374 lock_hovered: lock_response.hovered(),
375 lock_clicked: lock_response.clicked(),
376 blend_rect,
377 blend_hovered: blend_response.as_ref().map_or(false, |r| r.hovered()),
378 blend_clicked: blend_response.as_ref().map_or(false, |r| r.clicked()),
379 opacity_rect,
380 opacity_hovered: opacity_response.hovered(),
381 opacity_dragged: opacity_response.dragged(),
382 opacity_drag_pos: opacity_response.interact_pointer_pos(),
383 });
384 }
385
386 let pointer_pos = ui.input(|i| i.pointer.hover_pos());
388 for info in &layer_infos {
389 if info.row_drag_started && drag_state.dragging.is_none() {
391 let layer = &self.layers[info.idx];
392 if !layer.locked {
393 drag_state.dragging = Some(info.idx);
394 }
395 }
396
397 if drag_state.dragging.is_some() && info.row_hovered {
399 if let Some(pos) = pointer_pos {
400 let mid_y = info.rect.center().y;
402 if pos.y < mid_y {
403 drag_state.drop_target = Some(info.idx);
404 } else {
405 drag_state.drop_target = Some(info.idx + 1);
406 }
407 }
408 }
409 }
410
411 if !ui.input(|i| i.pointer.any_down()) {
413 if let (Some(from), Some(to)) = (drag_state.dragging, drag_state.drop_target) {
414 if from != to && from + 1 != to {
416 event = Some(LayerEvent::Reorder { from, to });
417 }
418 }
419 drag_state.dragging = None;
420 drag_state.drop_target = None;
421 }
422
423 let painter = ui.painter();
425
426 for (info, layer) in layer_infos.iter().zip(self.layers.iter()) {
427 let is_selected = self.selected == Some(info.idx);
428
429 let bg_color = if is_selected {
431 theme.primary.gamma_multiply(0.2)
432 } else if info.row_hovered {
433 theme.bg_secondary
434 } else {
435 theme.bg_primary
436 };
437 painter.rect_filled(info.rect, theme.radius_sm, bg_color);
438
439 if is_selected {
441 painter.rect_stroke(
442 info.rect,
443 theme.radius_sm,
444 Stroke::new(theme.border_width, theme.primary),
445 egui::StrokeKind::Inside,
446 );
447 }
448
449 let vis_color = if layer.visible {
451 if info.vis_hovered {
452 theme.primary
453 } else {
454 theme.text_primary
455 }
456 } else {
457 theme.text_muted
458 };
459 painter.text(
460 info.vis_rect.center(),
461 egui::Align2::CENTER_CENTER,
462 if layer.visible { "๐" } else { "โ" },
463 egui::FontId::proportional(theme.font_size_sm),
464 vis_color,
465 );
466
467 let lock_color = if layer.locked {
469 theme.state_warning
470 } else if info.lock_hovered {
471 theme.text_secondary
472 } else {
473 theme.text_muted
474 };
475 painter.text(
476 info.lock_rect.center(),
477 egui::Align2::CENTER_CENTER,
478 if layer.locked { "๐" } else { "ยท" },
479 egui::FontId::proportional(theme.font_size_xs),
480 lock_color,
481 );
482
483 if self.show_thumbnails {
485 let thumb_rect = Rect::from_min_size(
486 Pos2::new(
487 info.lock_rect.max.x + theme.spacing_xs,
488 info.rect.min.y + theme.spacing_xs,
489 ),
490 Vec2::splat(row_height - theme.spacing_sm),
491 );
492 let thumb_color = layer.color.unwrap_or(theme.primary).gamma_multiply(0.5);
493 painter.rect_filled(thumb_rect, theme.radius_sm, thumb_color);
494 painter.rect_stroke(
495 thumb_rect,
496 theme.radius_sm,
497 Stroke::new(0.5, theme.border),
498 egui::StrokeKind::Inside,
499 );
500 }
501
502 let name_x = if self.show_thumbnails {
504 info.lock_rect.max.x + row_height + theme.spacing_sm
505 } else {
506 info.lock_rect.max.x + theme.spacing_sm
507 };
508
509 let name_color = if layer.visible {
510 if is_selected {
511 theme.text_primary
512 } else {
513 theme.text_secondary
514 }
515 } else {
516 theme.text_muted
517 };
518
519 let name_text = if layer.name.len() > 12 && self.compact {
520 format!("{}โฆ", &layer.name[..11])
521 } else {
522 layer.name.clone()
523 };
524
525 painter.text(
526 Pos2::new(name_x, info.rect.center().y),
527 egui::Align2::LEFT_CENTER,
528 &name_text,
529 egui::FontId::proportional(if self.compact {
530 theme.font_size_xs
531 } else {
532 theme.font_size_sm
533 }),
534 name_color,
535 );
536
537 if let Some(blend_rect) = info.blend_rect {
539 let blend_color = if info.blend_hovered {
540 theme.primary
541 } else {
542 theme.text_muted
543 };
544 painter.text(
545 blend_rect.center(),
546 egui::Align2::CENTER_CENTER,
547 layer.blend_mode.short_name(),
548 egui::FontId::proportional(theme.font_size_xs),
549 blend_color,
550 );
551 }
552
553 painter.rect_filled(info.opacity_rect, theme.radius_sm, theme.bg_tertiary);
555
556 let fill_width = info.opacity_rect.width() * layer.opacity;
557 let fill_rect = Rect::from_min_size(
558 info.opacity_rect.min,
559 Vec2::new(fill_width, info.opacity_rect.height()),
560 );
561 let fill_color = if info.opacity_hovered || info.opacity_dragged {
562 theme.primary
563 } else {
564 theme.primary.gamma_multiply(0.7)
565 };
566 painter.rect_filled(fill_rect, theme.radius_sm, fill_color);
567
568 painter.text(
569 info.opacity_rect.center(),
570 egui::Align2::CENTER_CENTER,
571 format!("{}%", (layer.opacity * 100.0) as u8),
572 egui::FontId::proportional(theme.font_size_xs * 0.9),
573 theme.text_primary,
574 );
575
576 if info.idx < self.layers.len() - 1 {
578 painter.line_segment(
579 [
580 Pos2::new(info.rect.min.x + theme.spacing_sm, info.rect.max.y),
581 Pos2::new(info.rect.max.x - theme.spacing_sm, info.rect.max.y),
582 ],
583 Stroke::new(0.5, theme.border),
584 );
585 }
586
587 if drag_state.dragging == Some(info.idx) {
589 painter.rect_filled(
590 info.rect,
591 theme.radius_sm,
592 Color32::from_rgba_unmultiplied(
593 theme.primary.r(),
594 theme.primary.g(),
595 theme.primary.b(),
596 60,
597 ),
598 );
599 painter.rect_stroke(
600 info.rect,
601 theme.radius_sm,
602 Stroke::new(2.0, theme.primary),
603 egui::StrokeKind::Inside,
604 );
605 }
606
607 if let Some(drop_idx) = drag_state.drop_target {
609 if drop_idx == info.idx {
610 painter.line_segment(
612 [
613 Pos2::new(info.rect.min.x, info.rect.min.y),
614 Pos2::new(info.rect.max.x, info.rect.min.y),
615 ],
616 Stroke::new(3.0, theme.primary),
617 );
618 } else if drop_idx == info.idx + 1 && info.idx == self.layers.len() - 1 {
619 painter.line_segment(
621 [
622 Pos2::new(info.rect.min.x, info.rect.max.y),
623 Pos2::new(info.rect.max.x, info.rect.max.y),
624 ],
625 Stroke::new(3.0, theme.primary),
626 );
627 }
628 }
629
630 if event.is_none() && drag_state.dragging.is_none() {
632 if info.row_clicked {
633 event = Some(LayerEvent::Select(info.idx));
634 } else if info.vis_clicked {
635 event = Some(LayerEvent::ToggleVisible(info.idx));
636 } else if info.lock_clicked {
637 event = Some(LayerEvent::ToggleLock(info.idx));
638 } else if info.blend_clicked {
639 let modes = BlendMode::all();
640 let current_idx = modes
641 .iter()
642 .position(|&m| m == layer.blend_mode)
643 .unwrap_or(0);
644 let next_idx = (current_idx + 1) % modes.len();
645 event = Some(LayerEvent::SetBlendMode(info.idx, modes[next_idx]));
646 } else if info.opacity_dragged {
647 if let Some(pos) = info.opacity_drag_pos {
648 let new_opacity = ((pos.x - info.opacity_rect.min.x)
649 / info.opacity_rect.width())
650 .clamp(0.0, 1.0);
651 event = Some(LayerEvent::SetOpacity(info.idx, new_opacity));
652 }
653 }
654 }
655 }
656
657 ui.ctx().data_mut(|d| d.insert_temp(drag_id, drag_state));
659
660 event
661 }
662}