feat: client ui

This commit is contained in:
core 2025-04-05 23:17:02 -04:00
parent 23571406ea
commit 04c99188b8
Signed by: core
GPG key ID: FDBF740DADDCEECF
9 changed files with 673 additions and 435 deletions

10
Cargo.lock generated
View file

@ -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",

View file

@ -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"

View file

@ -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| {

View file

@ -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()
}
}

View file

@ -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);
});
});
});
}

View 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);
});
});
}

View file

@ -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(&registered_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,
&registered_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,
&registered_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)
}

View 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(&registered_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,
&registered_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,
&registered_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)
}

View file

@ -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;