diff --git a/Cargo.lock b/Cargo.lock index 1ce3e1b..020fd37 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1359,6 +1359,15 @@ dependencies = [ "serde", ] +[[package]] +name = "egui_flex" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d2598b8bd01a7bfdaede32b297e5ba671640d4220f6c353025042a82cefee6e" +dependencies = [ + "egui", +] + [[package]] name = "egui_glow" version = "0.31.1" @@ -6238,6 +6247,7 @@ dependencies = [ "egui-phosphor", "egui_demo_lib", "egui_extras", + "egui_flex", "futures", "getrandom 0.3.2", "image", diff --git a/crates/client/Cargo.toml b/crates/client/Cargo.toml index 83dd903..a6f3b3d 100644 --- a/crates/client/Cargo.toml +++ b/crates/client/Cargo.toml @@ -21,6 +21,7 @@ walkers = { version = "0.36", git = "https://github.com/c0repwn3r/walkers", bran getrandom = { version = "0.3", features = ["wasm_js"] } lru = "0.13" futures = "0.3" +egui_flex = "0.3" [target.'cfg(target_arch = "wasm32")'.dependencies] tracing-web = "0.1" diff --git a/crates/client/src/app.rs b/crates/client/src/app.rs index 6620573..35220bb 100644 --- a/crates/client/src/app.rs +++ b/crates/client/src/app.rs @@ -14,15 +14,17 @@ use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::ops::{Deref, DerefMut}; use walkers::{HttpTiles, MapMemory, Position, Tiles}; +use crate::ui::{Breakpoint, CtxExt}; #[derive(Serialize, Deserialize)] pub struct App { pub(crate) frame_time_history: History<f32>, - add_layer_open: bool, - selected_layer: Option<LayerId>, - map_memory: MapMemory, - layer_manager: LayerManager, - position: Position, + pub add_layer_open: bool, + pub selected_layer: Option<LayerId>, + pub map_memory: MapMemory, + pub layer_manager: LayerManager, + pub position: Position, + pub menu_open: bool } impl App { @@ -57,6 +59,7 @@ impl App { map_memory: MapMemory::default(), layer_manager, position: Position::new(35.227085, -80.843124), + menu_open: false } } } @@ -72,13 +75,19 @@ impl eframe::App for App { } top_bar(ctx); - footer(ctx, &self); - left_bar( - ctx, - &mut self.add_layer_open, - &mut self.layer_manager, - &mut self.selected_layer, - ); + + let show_main_menu_in_footer = ctx.breakpoint() < Breakpoint::Xl4; + + footer(ctx, self, show_main_menu_in_footer); + + if !show_main_menu_in_footer { + left_bar( + ctx, + &mut self.add_layer_open, + &mut self.layer_manager, + &mut self.selected_layer, + ); + } //right_bar(ctx); egui::CentralPanel::default().show(ctx, |ui| { diff --git a/crates/client/src/ui/mod.rs b/crates/client/src/ui/mod.rs index f35230f..83ee64b 100644 --- a/crates/client/src/ui/mod.rs +++ b/crates/client/src/ui/mod.rs @@ -1,6 +1,7 @@ use egui::emath::GuiRounding; -use egui::{emath, CursorIcon, Id, InnerResponse, LayerId, Order, Sense, Ui, UiBuilder}; +use egui::{emath, CursorIcon, Id, InnerResponse, LayerId, Order, Sense, Ui, UiBuilder, Context}; use std::any::Any; +use std::cmp::Ordering; pub mod shell; pub mod tokens; @@ -48,6 +49,11 @@ pub trait UiExt { } response } + + fn breakpoint(&self) -> Breakpoint { + let ui = self.ui(); + ui.ctx().breakpoint() + } } impl UiExt for egui::Ui { @@ -61,3 +67,93 @@ impl UiExt for egui::Ui { self } } + +pub trait CtxExt { + fn ctx(&self) -> &egui::Context; + fn breakpoint(&self) -> Breakpoint { + let ctx = self.ctx(); + Breakpoint::current_breakpoint(ctx.input(|i| i.screen_rect().width())) + } +} +impl CtxExt for egui::Context { + #[inline] + fn ctx(&self) -> &Context { self } +} + + +#[derive(PartialEq, Eq)] +pub enum Breakpoint { + Xs3, + Xs2, + Xs, + Sm, + Md, + Lg, + Xl, + Xl2, + Xl3, + Xl4, + Xl5, + Xl6, + Xl7, +} +impl Breakpoint { + pub const fn minimum_width(&self) -> f32 { + match self { + Breakpoint::Xs3 => 256.0, + Breakpoint::Xs2 => 288.0, + Breakpoint::Xs => 320.0, + Breakpoint::Sm => 384.0, + Breakpoint::Md => 448.0, + Breakpoint::Lg => 512.0, + Breakpoint::Xl => 576.0, + Breakpoint::Xl2 => 672.0, + Breakpoint::Xl3 => 768.0, + Breakpoint::Xl4 => 896.0, + Breakpoint::Xl5 => 1024.0, + Breakpoint::Xl6 => 1152.0, + Breakpoint::Xl7 => 1280.0, + } + } + pub const fn current_breakpoint(width: f32) -> Self { + return if width > Breakpoint::Xl7.minimum_width() { + Breakpoint::Xl7 + } else if width > Breakpoint::Xl6.minimum_width() { + Breakpoint::Xl6 + } else if width > Breakpoint::Xl5.minimum_width() { + Breakpoint::Xl5 + } else if width > Breakpoint::Xl4.minimum_width() { + Breakpoint::Xl4 + } else if width > Breakpoint::Xl3.minimum_width() { + Breakpoint::Xl3 + } else if width > Breakpoint::Xl2.minimum_width() { + Breakpoint::Xl2 + } else if width > Breakpoint::Xl.minimum_width() { + Breakpoint::Xl + } else if width > Breakpoint::Lg.minimum_width() { + Breakpoint::Lg + } else if width > Breakpoint::Md.minimum_width() { + Breakpoint::Md + } else if width > Breakpoint::Sm.minimum_width() { + Breakpoint::Sm + } else if width > Breakpoint::Xs.minimum_width() { + Breakpoint::Xs + } else if width > Breakpoint::Xs2.minimum_width() { + Breakpoint::Xs2 + } else { + Breakpoint::Xs3 + }; + } +} + +impl PartialOrd<Self> for Breakpoint { + fn partial_cmp(&self, other: &Self) -> Option<Ordering> { + self.minimum_width().partial_cmp(&other.minimum_width()) + } +} + +impl Ord for Breakpoint { + fn cmp(&self, other: &Self) -> Ordering { + self.minimum_width().partial_cmp(&other.minimum_width()).unwrap() + } +} \ No newline at end of file diff --git a/crates/client/src/ui/shell/footer.rs b/crates/client/src/ui/shell/footer.rs index 19d2d19..af0267d 100644 --- a/crates/client/src/ui/shell/footer.rs +++ b/crates/client/src/ui/shell/footer.rs @@ -1,6 +1,10 @@ use crate::app::App; use eframe::emath::Align; -use egui::{Hyperlink, Layout, Ui}; +use egui::{Color32, FontFamily, Hyperlink, Layout, RichText, Ui}; +use crate::map::sources::LayerId; +use crate::map::tiles::LayerManager; +use crate::ui::shell::inner_footer::inner_footer; +use crate::ui::shell::main_menu::main_menu; fn version_and_changelog(ui: &mut Ui) { ui.add( @@ -36,19 +40,49 @@ fn e3team_credits(ui: &mut Ui) { ); } -pub fn footer(ctx: &egui::Context, app: &App) { +pub fn footer( + ctx: &egui::Context, + app: &mut App, + show_main_menu_here: bool +) { egui::TopBottomPanel::bottom("footer") .resizable(false) .show(ctx, |ui| { - ui.style_mut().visuals.hyperlink_color = ui.visuals().weak_text_color(); - ui.horizontal(|ui| { - version_and_changelog(ui); - frame_time_warning(ui, app); + if show_main_menu_here { + if app.menu_open { + ui.with_layout(Layout::top_down_justified(Align::Center), |ui| { + if ui.button( + RichText::new(egui_phosphor::regular::ARROW_DOWN) + .family(FontFamily::Name("icon-only".into())) + ).clicked() { + app.menu_open = false; + } + main_menu( + ui, + &mut app.add_layer_open, + &mut app.layer_manager, + &mut app.selected_layer + ); + inner_footer(ui, app); + }); + + } else { + ui.with_layout(Layout::top_down_justified(Align::Center), |ui| { + let previous_visuals = ui.visuals().widgets.inactive.clone(); + ui.visuals_mut().widgets.inactive.weak_bg_fill = Color32::TRANSPARENT; + if ui.button( + RichText::new(egui_phosphor::regular::ARROW_UP) + .family(FontFamily::Name("icon-only".into())) + ).clicked() { + app.menu_open = true; + } + ui.visuals_mut().widgets.inactive = previous_visuals; + }); + } + } else { + inner_footer(ui, app); + } - ui.with_layout(Layout::right_to_left(Align::Center), |ui| { - e3team_credits(ui); - }); - }); }); } diff --git a/crates/client/src/ui/shell/inner_footer.rs b/crates/client/src/ui/shell/inner_footer.rs new file mode 100644 index 0000000..025a482 --- /dev/null +++ b/crates/client/src/ui/shell/inner_footer.rs @@ -0,0 +1,50 @@ +use crate::app::App; +use eframe::emath::Align; +use egui::{Direction, FontFamily, Hyperlink, Layout, RichText, Ui}; + +fn version_and_changelog(ui: &mut Ui) { + ui.add( + Hyperlink::from_label_and_url( + format!("v{}", env!("CARGO_PKG_VERSION")), + "https://weather.ax/changelog", + ) + .open_in_new_tab(true), + ); +} + +fn frame_time_warning(ui: &mut Ui, app: &App) { + if let Some(frame_time) = app.frame_time_history.average() { + ui.separator(); + + let ms = frame_time * 1e3; + let visuals = ui.visuals(); + let color = if ms < 15.0 { + visuals.weak_text_color() + } else { + visuals.warn_fg_color + }; + let text = format!("{ms:.1} ms"); + ui.label(egui::RichText::new(text).monospace().color(color)) + .on_hover_text("CPU time used by wxbox client each frame, lower is better"); + } +} + +fn e3team_credits(ui: &mut Ui) { + ui.add( + Hyperlink::from_label_and_url("built with <3 by e3team", "https://e3t.cc") + .open_in_new_tab(true), + ); +} + +pub fn inner_footer(ui: &mut egui::Ui, app: &App) { + ui.style_mut().visuals.hyperlink_color = ui.visuals().weak_text_color(); + + ui.horizontal(|ui| { + version_and_changelog(ui); + frame_time_warning(ui, app); + + ui.with_layout(Layout::right_to_left(Align::Center), |ui| { + e3team_credits(ui); + }); + }); +} diff --git a/crates/client/src/ui/shell/left_bar.rs b/crates/client/src/ui/shell/left_bar.rs index 3eec890..89212d0 100644 --- a/crates/client/src/ui/shell/left_bar.rs +++ b/crates/client/src/ui/shell/left_bar.rs @@ -14,6 +14,7 @@ use egui::{ use std::collections::HashMap; use std::sync::Arc; use tracing::debug; +use crate::ui::shell::main_menu::main_menu; fn outer_rect(f: &Prepared) -> Rect { let content_rect = f.content_ui.min_rect(); @@ -32,420 +33,9 @@ pub fn left_bar( frame.inner_margin.left = 0; frame.inner_margin.right = 0; - let mut needs_any_update = false; - egui::SidePanel::left("left_panel") .frame(frame) .show(ctx, |ui| { - pane_header( - ui, - "Layers", - Some(egui_phosphor::regular::STACK), - false, - |ui| { - ui.style_mut().visuals.widgets.inactive.weak_bg_fill = Color32::TRANSPARENT; - if ui - .button(RichText::new(egui_phosphor::regular::PLUS).size(12.0)) - .clicked() - { - *add_open = true; - } - }, - ); - - let lm_registry = &layer_manager.registered_sources; - let layers_to_draw = layer_manager.active_layers.len(); - - let mut layers_to_remove = vec![]; - - for i in 0..layer_manager.active_layers.len() { - let (retain, update_layers) = draw_layer( - ui, - i, - lm_registry, - &mut layer_manager.active_layers, - selected_layer, - ); - - if !retain { - layers_to_remove.push(layer_manager.active_layers[i].layer_id); - } - needs_any_update = needs_any_update || update_layers; - } - - for layer_to_remove in &layers_to_remove { - if *selected_layer == Some(*layer_to_remove) { - *selected_layer = None; - } - } - - layer_manager - .active_layers - .retain(|layer| !layers_to_remove.contains(&layer.layer_id)); + main_menu(ui, add_open, layer_manager, selected_layer); }); - - if *add_open { - let mut frame = Frame::popup(&ctx.style()).corner_radius(CornerRadius::ZERO); - frame.inner_margin.left = 0; - frame.inner_margin.right = 0; - egui::Modal::new(Id::new("add_source_modal")) - .frame(frame) - .show(ctx, |ui| { - pane_header( - ui, - "Add layer", - Some(egui_phosphor::regular::STACK_PLUS), - false, - |ui| { - ui.style_mut().visuals.widgets.inactive.weak_bg_fill = Color32::TRANSPARENT; - if ui - .button(RichText::new(egui_phosphor::regular::X).size(12.0)) - .clicked() - { - *add_open = false; - } - }, - ); - - for (id, registered_layer) in &layer_manager.registered_sources { - // ghost button - - let mut frame = Frame::new(); - - frame.inner_margin.top = 4; - frame.inner_margin.left = 8; - frame.inner_margin.right = 8; - frame.inner_margin.bottom = 4; - - let mut frame = frame.begin(ui); - - { - frame.content_ui.horizontal(|ui| { - let mut frame = Frame::new().fill( - DESIGN_TOKENS - .get_color( - "colors.base.4", - if ui.style().visuals.dark_mode { - Theme::Dark - } else { - Theme::Light - }, - ) - .unwrap(), - ); - - frame.inner_margin.top = 4; - frame.inner_margin.left = 8; - frame.inner_margin.right = 8; - frame.inner_margin.bottom = 4; - - frame.corner_radius = CornerRadius::from(4); - - frame.show(ui, |ui| { - ui.label( - RichText::new(registered_layer.type_hint.icon()).size(24.0), - ); - }); - - ui.vertical(|ui| { - ui.label(®istered_layer.display_name); - ui.horizontal(|ui| { - let type_hint = registered_layer.type_hint.name(); - let icon = registered_layer.type_hint.icon(); - - let type_lbl = format!("{icon} {type_hint}"); - ui.allocate_ui_with_layout( - vec2(90.0, 20.0), - Layout::left_to_right(Align::Center), - |ui| { - ui.label(RichText::new(type_lbl).weak()); - ui.add_space(ui.available_width()); - }, - ); - - ui.allocate_ui_with_layout( - vec2(120.0, 20.0), - Layout::left_to_right(Align::Center), - |ui| { - ui.label( - RichText::new(format!( - "{} {}", - egui_phosphor::regular::NAVIGATION_ARROW, - ®istered_layer.location - )) - .weak(), - ); - ui.add_space(ui.available_width()); - }, - ); - - ui.allocate_ui_with_layout( - vec2(100.0, 20.0), - Layout::left_to_right(Align::Center), - |ui| { - ui.label( - RichText::new(format!( - "{} {}", - egui_phosphor::regular::IDENTIFICATION_BADGE, - ®istered_layer.source - )) - .weak(), - ); - ui.add_space(ui.available_width()); - }, - ); - }); - }); - - ui.add_space(ui.available_width()); - }); - } - let response = ui.allocate_rect(outer_rect(&frame), Sense::click_and_drag()); - if response.hovered() { - frame.frame.fill = DESIGN_TOKENS - .get_color( - "colors.base.7", - if ui.style().visuals.dark_mode { - Theme::Dark - } else { - Theme::Light - }, - ) - .unwrap(); - ui.ctx() - .output_mut(|u| u.cursor_icon = CursorIcon::PointingHand); - } - if response.clicked() { - layer_manager.active_layers.push(ActiveLayer { - source_id: *id, - layer_id: LayerId::new(), - visible: true, - }); - debug!("{:?}", layer_manager); - *add_open = false; - } - frame.paint(ui); - } - }); - } -} - -fn draw_layer( - ui: &mut Ui, - you: usize, - registry: &HashMap<SourceId, LayerSource>, - active_layers: &mut Vec<ActiveLayer>, - selected_layer: &mut Option<LayerId>, -) -> (bool, bool) { - let mut retain = true; - let mut needs_layers_update = false; - let layer = active_layers[you]; - - let source = registry.get(&layer.source_id).unwrap(); - - let r = ui.scope_builder( - UiBuilder::new() - .id_salt(layer.layer_id) - .sense(Sense::click_and_drag()), - |ui| { - let title = source.short_name.as_str(); - let icon = source.type_hint.icon(); - - let mut any_children_hovered = false; - - let mut frame = Frame::new(); - //frame.fill = ui.style().visuals.hyperlink_color; - frame.inner_margin.left = 8; - frame.inner_margin.right = 8; - frame.inner_margin.top = 2; - frame.inner_margin.bottom = 2; - - let mut frame = frame.begin(ui); - { - frame.content_ui.horizontal(|ui| { - ui.add( - Label::new( - RichText::new(egui_phosphor::regular::DOTS_SIX_VERTICAL) - .size(12.0) - .family(FontFamily::Name("icon-only".into())) - .weak(), - ) - .sense(Sense::empty()) - .selectable(false), - ); - - ui.add_space(8.0); - - let mut name_label = RichText::new(format!("{icon} {title}")); - if !layer.visible { - name_label = name_label.weak(); - } - - ui.add( - Label::new(name_label) - .sense(Sense::empty()) - .selectable(false), - ); - - ui.with_layout(Layout::right_to_left(Align::Center), |ui| { - ui.style_mut().visuals.widgets.inactive.weak_bg_fill = Color32::TRANSPARENT; - - let mut visibility_label = RichText::new(if layer.visible { - egui_phosphor::regular::EYE - } else { - egui_phosphor::regular::EYE_SLASH - }) - .size(12.0) - .family(FontFamily::Name("icon-only".into())); - - if !layer.visible { - visibility_label = visibility_label.weak(); - } - - let visibility_btn = ui.button(visibility_label); - - any_children_hovered = any_children_hovered || visibility_btn.hovered(); - if visibility_btn.clicked() { - active_layers[you].visible = !active_layers[you].visible; - } - - let mut delete_label = RichText::new(egui_phosphor::regular::TRASH) - .size(12.0) - .family(FontFamily::Name("icon-only".into())); - - if !layer.visible { - delete_label = delete_label.weak(); - } - - let delete_button = ui.button(delete_label); - any_children_hovered = any_children_hovered || delete_button.hovered(); - - if delete_button.clicked() { - retain = false; - needs_layers_update = true; - } - }); - }); - } - frame.allocate_space(ui); - - let response = ui.response(); - - if response.hovered() && !any_children_hovered { - frame.frame.fill = ui.visuals().widgets.active.weak_bg_fill; - ui.ctx().output_mut(|u| u.cursor_icon = CursorIcon::Grab); - } - - if response.clicked() { - *selected_layer = Some(layer.layer_id); - } - - if let Some(sel_layer) = selected_layer { - if *sel_layer == layer.layer_id { - if !layer.visible { - frame.frame.fill = ui.visuals().widgets.inactive.bg_fill; - } else { - frame.frame.fill = ui.visuals().widgets.hovered.bg_fill; - } - } - } - - frame.paint(ui); - }, - ); - - let response = r.response; - - let is_layer_being_dragged = egui::DragAndDrop::has_payload_of_type::<LayerId>(ui.ctx()); - if is_layer_being_dragged { - let layer_being_dragged: Arc<LayerId> = egui::DragAndDrop::payload(ui.ctx()).unwrap(); - if *layer_being_dragged != layer.layer_id && response.contains_pointer() { - let rect = response.rect; - if let Some(pointer) = ui.input(|i| i.pointer.interact_pos()) { - let stroke = ui.style().visuals.widgets.active.fg_stroke; - - let above = pointer.y < rect.center().y; - if above { - // above us - ui.painter().hline(rect.x_range(), rect.top(), stroke); - } else { - // below us - ui.painter().hline(rect.x_range(), rect.bottom(), stroke); - } - - if let Some(payload) = response.dnd_release_payload() { - // released this frame - let layer_id: LayerId = *payload; - // remove it from the array - - let pos = active_layers - .iter() - .position(|u| u.layer_id == layer_id) - .unwrap(); - let layer = active_layers.remove(pos); - - // add it back at the appropriate index - let mut add_idx = you; - if !above { - add_idx += 1; - } - if add_idx > active_layers.len() { - active_layers.push(layer); - } else { - active_layers.insert(add_idx, layer); - } - - needs_layers_update = true; - } - } - } - } - - if response.dragged() { - egui::DragAndDrop::set_payload(ui.ctx(), layer.layer_id); - *selected_layer = Some(layer.layer_id); - let layer_id = egui::LayerId::new(Order::Tooltip, layer.layer_id.into()); - ui.ctx().style_mut(|style| { - style.visuals.window_fill = DESIGN_TOKENS - .get_color( - "colors.accent.3", - if ui.style().visuals.dark_mode { - Theme::Dark - } else { - Theme::Light - }, - ) - .unwrap(); - style.visuals.widgets.noninteractive.fg_stroke.color = DESIGN_TOKENS - .get_color( - "colors.accent.12", - if ui.style().visuals.dark_mode { - Theme::Dark - } else { - Theme::Light - }, - ) - .unwrap(); - }); - egui::show_tooltip(ui.ctx(), layer_id, egui::Id::new("drag_detail"), |ui| { - ui.horizontal(|ui| { - ui.label(RichText::new(source.type_hint.icon())); - ui.label(RichText::new(&source.short_name)); - }) - }); - // reset style - DESIGN_TOKENS.set_color(ui.ctx()); - } - - if response.drag_started() {} - - // handle potential droppage - let Some(dragged_layer_id) = - egui::DragAndDrop::payload(ui.ctx()).map(|payload: Arc<LayerId>| (*payload)) - else { - return (retain, needs_layers_update); // nothing is being dragged - }; - ui.ctx().set_cursor_icon(CursorIcon::Grabbing); - - (retain, needs_layers_update) } diff --git a/crates/client/src/ui/shell/main_menu.rs b/crates/client/src/ui/shell/main_menu.rs new file mode 100644 index 0000000..a56a9e3 --- /dev/null +++ b/crates/client/src/ui/shell/main_menu.rs @@ -0,0 +1,446 @@ +use crate::map::sources::{ActiveLayer, LayerId, LayerSource}; +use crate::map::tiles::{LayerManager, SourceId}; +use crate::ui::shell::bars::pane_header; +use crate::ui::tokens::DESIGN_TOKENS; +use crate::ui::UiExt; +use eframe::emath::Align; +use egui::epaint::Marginf; +use egui::frame::Prepared; +use egui::{ + vec2, Button, Color32, CornerRadius, CursorIcon, FontFamily, Frame, Id, InnerResponse, Label, + Layout, Margin, Order, Rect, Response, RichText, ScrollArea, Sense, Separator, Theme, Ui, + UiBuilder, Visuals, +}; +use std::collections::HashMap; +use std::sync::Arc; +use tracing::debug; + +fn outer_rect(f: &Prepared) -> Rect { + let content_rect = f.content_ui.min_rect(); + + content_rect + f.frame.inner_margin + Marginf::from(f.frame.stroke.width) + f.frame.outer_margin +} + +pub fn main_menu( + ui: &mut egui::Ui, + add_open: &mut bool, + layer_manager: &mut LayerManager, + selected_layer: &mut Option<LayerId>, +) { + let mut frame = Frame::side_top_panel(&ui.ctx().style()); + frame.inner_margin.top = 4; + frame.inner_margin.left = 0; + frame.inner_margin.right = 0; + + let mut needs_any_update = false; + pane_header( + ui, + "Layers", + Some(egui_phosphor::regular::STACK), + false, + |ui| { + ui.style_mut().visuals.widgets.inactive.weak_bg_fill = Color32::TRANSPARENT; + if ui + .button(RichText::new(egui_phosphor::regular::PLUS).size(12.0)) + .clicked() + { + *add_open = true; + } + }, + ); + + let lm_registry = &layer_manager.registered_sources; + let layers_to_draw = layer_manager.active_layers.len(); + + let mut layers_to_remove = vec![]; + + for i in 0..layer_manager.active_layers.len() { + let (retain, update_layers) = draw_layer( + ui, + i, + lm_registry, + &mut layer_manager.active_layers, + selected_layer, + ); + + if !retain { + layers_to_remove.push(layer_manager.active_layers[i].layer_id); + } + needs_any_update = needs_any_update || update_layers; + } + + for layer_to_remove in &layers_to_remove { + if *selected_layer == Some(*layer_to_remove) { + *selected_layer = None; + } + } + + layer_manager + .active_layers + .retain(|layer| !layers_to_remove.contains(&layer.layer_id)); + + if *add_open { + let mut frame = Frame::popup(&ui.ctx().style()).corner_radius(CornerRadius::ZERO); + frame.inner_margin.left = 0; + frame.inner_margin.right = 0; + egui::Modal::new(Id::new("add_source_modal")) + .frame(frame) + .show(ui.ctx(), |ui| { + pane_header( + ui, + "Add layer", + Some(egui_phosphor::regular::STACK_PLUS), + false, + |ui| { + ui.style_mut().visuals.widgets.inactive.weak_bg_fill = Color32::TRANSPARENT; + if ui + .button(RichText::new(egui_phosphor::regular::X).size(12.0)) + .clicked() + { + *add_open = false; + } + }, + ); + + for (id, registered_layer) in &layer_manager.registered_sources { + // ghost button + + let mut frame = Frame::new(); + + frame.inner_margin.top = 4; + frame.inner_margin.left = 8; + frame.inner_margin.right = 8; + frame.inner_margin.bottom = 4; + + let mut frame = frame.begin(ui); + + { + frame.content_ui.horizontal(|ui| { + let mut frame = Frame::new().fill( + DESIGN_TOKENS + .get_color( + "colors.base.4", + if ui.style().visuals.dark_mode { + Theme::Dark + } else { + Theme::Light + }, + ) + .unwrap(), + ); + + frame.inner_margin.top = 4; + frame.inner_margin.left = 8; + frame.inner_margin.right = 8; + frame.inner_margin.bottom = 4; + + frame.corner_radius = CornerRadius::from(4); + + frame.show(ui, |ui| { + ui.label( + RichText::new(registered_layer.type_hint.icon()).size(24.0), + ); + }); + + ui.vertical(|ui| { + ui.label(®istered_layer.display_name); + ui.horizontal(|ui| { + let type_hint = registered_layer.type_hint.name(); + let icon = registered_layer.type_hint.icon(); + + let type_lbl = format!("{icon} {type_hint}"); + ui.allocate_ui_with_layout( + vec2(90.0, 20.0), + Layout::left_to_right(Align::Center), + |ui| { + ui.label(RichText::new(type_lbl).weak()); + ui.add_space(ui.available_width()); + }, + ); + + ui.allocate_ui_with_layout( + vec2(120.0, 20.0), + Layout::left_to_right(Align::Center), + |ui| { + ui.label( + RichText::new(format!( + "{} {}", + egui_phosphor::regular::NAVIGATION_ARROW, + ®istered_layer.location + )) + .weak(), + ); + ui.add_space(ui.available_width()); + }, + ); + + ui.allocate_ui_with_layout( + vec2(100.0, 20.0), + Layout::left_to_right(Align::Center), + |ui| { + ui.label( + RichText::new(format!( + "{} {}", + egui_phosphor::regular::IDENTIFICATION_BADGE, + ®istered_layer.source + )) + .weak(), + ); + ui.add_space(ui.available_width()); + }, + ); + }); + }); + + ui.add_space(ui.available_width()); + }); + } + let response = ui.allocate_rect(outer_rect(&frame), Sense::click_and_drag()); + if response.hovered() { + frame.frame.fill = DESIGN_TOKENS + .get_color( + "colors.base.7", + if ui.style().visuals.dark_mode { + Theme::Dark + } else { + Theme::Light + }, + ) + .unwrap(); + ui.ctx() + .output_mut(|u| u.cursor_icon = CursorIcon::PointingHand); + } + if response.clicked() { + layer_manager.active_layers.push(ActiveLayer { + source_id: *id, + layer_id: LayerId::new(), + visible: true, + }); + debug!("{:?}", layer_manager); + *add_open = false; + } + frame.paint(ui); + } + }); + } +} + +fn draw_layer( + ui: &mut Ui, + you: usize, + registry: &HashMap<SourceId, LayerSource>, + active_layers: &mut Vec<ActiveLayer>, + selected_layer: &mut Option<LayerId>, +) -> (bool, bool) { + let mut retain = true; + let mut needs_layers_update = false; + let layer = active_layers[you]; + + let source = registry.get(&layer.source_id).unwrap(); + + let r = ui.scope_builder( + UiBuilder::new() + .id_salt(layer.layer_id) + .sense(Sense::click_and_drag()), + |ui| { + let title = source.short_name.as_str(); + let icon = source.type_hint.icon(); + + let mut any_children_hovered = false; + + let mut frame = Frame::new(); + //frame.fill = ui.style().visuals.hyperlink_color; + frame.inner_margin.left = 8; + frame.inner_margin.right = 8; + frame.inner_margin.top = 2; + frame.inner_margin.bottom = 2; + + let mut frame = frame.begin(ui); + { + frame.content_ui.horizontal(|ui| { + ui.add( + Label::new( + RichText::new(egui_phosphor::regular::DOTS_SIX_VERTICAL) + .size(12.0) + .family(FontFamily::Name("icon-only".into())) + .weak(), + ) + .sense(Sense::empty()) + .selectable(false), + ); + + ui.add_space(8.0); + + let mut name_label = RichText::new(format!("{icon} {title}")); + if !layer.visible { + name_label = name_label.weak(); + } + + ui.add( + Label::new(name_label) + .sense(Sense::empty()) + .selectable(false), + ); + + ui.with_layout(Layout::right_to_left(Align::Center), |ui| { + ui.style_mut().visuals.widgets.inactive.weak_bg_fill = Color32::TRANSPARENT; + + let mut visibility_label = RichText::new(if layer.visible { + egui_phosphor::regular::EYE + } else { + egui_phosphor::regular::EYE_SLASH + }) + .size(12.0) + .family(FontFamily::Name("icon-only".into())); + + if !layer.visible { + visibility_label = visibility_label.weak(); + } + + let visibility_btn = ui.button(visibility_label); + + any_children_hovered = any_children_hovered || visibility_btn.hovered(); + if visibility_btn.clicked() { + active_layers[you].visible = !active_layers[you].visible; + } + + let mut delete_label = RichText::new(egui_phosphor::regular::TRASH) + .size(12.0) + .family(FontFamily::Name("icon-only".into())); + + if !layer.visible { + delete_label = delete_label.weak(); + } + + let delete_button = ui.button(delete_label); + any_children_hovered = any_children_hovered || delete_button.hovered(); + + if delete_button.clicked() { + retain = false; + needs_layers_update = true; + } + }); + }); + } + frame.allocate_space(ui); + + let response = ui.response(); + + if response.hovered() && !any_children_hovered { + frame.frame.fill = ui.visuals().widgets.active.weak_bg_fill; + ui.ctx().output_mut(|u| u.cursor_icon = CursorIcon::Grab); + } + + if response.clicked() { + *selected_layer = Some(layer.layer_id); + } + + if let Some(sel_layer) = selected_layer { + if *sel_layer == layer.layer_id { + if !layer.visible { + frame.frame.fill = ui.visuals().widgets.inactive.bg_fill; + } else { + frame.frame.fill = ui.visuals().widgets.hovered.bg_fill; + } + } + } + + frame.paint(ui); + }, + ); + + let response = r.response; + + let is_layer_being_dragged = egui::DragAndDrop::has_payload_of_type::<LayerId>(ui.ctx()); + if is_layer_being_dragged { + let layer_being_dragged: Arc<LayerId> = egui::DragAndDrop::payload(ui.ctx()).unwrap(); + if *layer_being_dragged != layer.layer_id && response.contains_pointer() { + let rect = response.rect; + if let Some(pointer) = ui.input(|i| i.pointer.interact_pos()) { + let stroke = ui.style().visuals.widgets.active.fg_stroke; + + let above = pointer.y < rect.center().y; + if above { + // above us + ui.painter().hline(rect.x_range(), rect.top(), stroke); + } else { + // below us + ui.painter().hline(rect.x_range(), rect.bottom(), stroke); + } + + if let Some(payload) = response.dnd_release_payload() { + // released this frame + let layer_id: LayerId = *payload; + // remove it from the array + + let pos = active_layers + .iter() + .position(|u| u.layer_id == layer_id) + .unwrap(); + let layer = active_layers.remove(pos); + + // add it back at the appropriate index + let mut add_idx = you; + if !above { + add_idx += 1; + } + if add_idx > active_layers.len() { + active_layers.push(layer); + } else { + active_layers.insert(add_idx, layer); + } + + needs_layers_update = true; + } + } + } + } + + if response.dragged() { + egui::DragAndDrop::set_payload(ui.ctx(), layer.layer_id); + *selected_layer = Some(layer.layer_id); + let layer_id = egui::LayerId::new(Order::Tooltip, layer.layer_id.into()); + ui.ctx().style_mut(|style| { + style.visuals.window_fill = DESIGN_TOKENS + .get_color( + "colors.accent.3", + if ui.style().visuals.dark_mode { + Theme::Dark + } else { + Theme::Light + }, + ) + .unwrap(); + style.visuals.widgets.noninteractive.fg_stroke.color = DESIGN_TOKENS + .get_color( + "colors.accent.12", + if ui.style().visuals.dark_mode { + Theme::Dark + } else { + Theme::Light + }, + ) + .unwrap(); + }); + egui::show_tooltip(ui.ctx(), layer_id, egui::Id::new("drag_detail"), |ui| { + ui.horizontal(|ui| { + ui.label(RichText::new(source.type_hint.icon())); + ui.label(RichText::new(&source.short_name)); + }) + }); + // reset style + DESIGN_TOKENS.set_color(ui.ctx()); + } + + if response.drag_started() {} + + // handle potential droppage + let Some(dragged_layer_id) = + egui::DragAndDrop::payload(ui.ctx()).map(|payload: Arc<LayerId>| (*payload)) + else { + return (retain, needs_layers_update); // nothing is being dragged + }; + ui.ctx().set_cursor_icon(CursorIcon::Grabbing); + + (retain, needs_layers_update) +} diff --git a/crates/client/src/ui/shell/mod.rs b/crates/client/src/ui/shell/mod.rs index 7de822b..33a90d3 100644 --- a/crates/client/src/ui/shell/mod.rs +++ b/crates/client/src/ui/shell/mod.rs @@ -3,3 +3,5 @@ pub mod footer; pub mod left_bar; pub mod right_bar; pub mod top_bar; +mod main_menu; +pub mod inner_footer; \ No newline at end of file