feat: client ui
This commit is contained in:
parent
23571406ea
commit
04c99188b8
9 changed files with 673 additions and 435 deletions
Cargo.lock
crates/client
10
Cargo.lock
generated
10
Cargo.lock
generated
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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| {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
50
crates/client/src/ui/shell/inner_footer.rs
Normal file
50
crates/client/src/ui/shell/inner_footer.rs
Normal file
|
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
446
crates/client/src/ui/shell/main_menu.rs
Normal file
446
crates/client/src/ui/shell/main_menu.rs
Normal file
|
@ -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)
|
||||
}
|
|
@ -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;
|
Loading…
Add table
Reference in a new issue