continued ui improvement
This commit is contained in:
parent
839de0bd5c
commit
d53e294b7c
10 changed files with 220 additions and 40 deletions
crates/client
6
crates/client/dist/index.html
vendored
6
crates/client/dist/index.html
vendored
|
@ -94,7 +94,7 @@
|
|||
}
|
||||
}
|
||||
</style>
|
||||
<link rel="modulepreload" href="/wxbox-client-21f1a585bee166.js" crossorigin="anonymous" integrity="sha384-HYob1LR+vO//6cXv/O26ycz5UWAzGRoGIgqqjwGtEg7IyCm5vkgg1EMr8nvYobva"><link rel="preload" href="/wxbox-client-21f1a585bee166_bg.wasm" crossorigin="anonymous" integrity="sha384-qIpqnebYuFIyVLVlTpZyplFTfNmt4cw6Wgh1qG1FdgABRcHBhIkGKSsuM1/dLQB0" as="fetch" type="application/wasm"></head>
|
||||
<link rel="modulepreload" href="/wxbox-client-96cce58fa4aaba7a.js" crossorigin="anonymous" integrity="sha384-rOwE8Z+dDRuQLsg/fUPw7V9/GJ3Oifopq6b88/pPM7xq2QmCD54SJdnx/SgS1/R1"><link rel="preload" href="/wxbox-client-96cce58fa4aaba7a_bg.wasm" crossorigin="anonymous" integrity="sha384-Rw805YQqcRKZ9iiDhDqyezvRhVu5nQt+IPPnlZCFQ+bPI7pIaxw5QmFJ4EuhBRVf" as="fetch" type="application/wasm"></head>
|
||||
|
||||
<body>
|
||||
<!-- The WASM code will resize the canvas dynamically -->
|
||||
|
@ -111,8 +111,8 @@
|
|||
|
||||
|
||||
<script type="module">
|
||||
import init, * as bindings from '/wxbox-client-21f1a585bee166.js';
|
||||
const wasm = await init({ module_or_path: '/wxbox-client-21f1a585bee166_bg.wasm' });
|
||||
import init, * as bindings from '/wxbox-client-96cce58fa4aaba7a.js';
|
||||
const wasm = await init({ module_or_path: '/wxbox-client-96cce58fa4aaba7a_bg.wasm' });
|
||||
|
||||
|
||||
window.wasmBindings = bindings;
|
||||
|
|
|
@ -198,7 +198,7 @@ function debugString(val) {
|
|||
return className;
|
||||
}
|
||||
function __wbg_adapter_30(arg0, arg1, arg2) {
|
||||
wasm.closure765_externref_shim(arg0, arg1, arg2);
|
||||
wasm.closure791_externref_shim(arg0, arg1, arg2);
|
||||
}
|
||||
|
||||
function takeFromExternrefTable0(idx) {
|
||||
|
@ -214,7 +214,7 @@ function __wbg_adapter_35(arg0, arg1) {
|
|||
}
|
||||
|
||||
function __wbg_adapter_38(arg0, arg1, arg2) {
|
||||
wasm.closure1268_externref_shim(arg0, arg1, arg2);
|
||||
wasm.closure1294_externref_shim(arg0, arg1, arg2);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1519,20 +1519,20 @@ function __wbg_get_imports() {
|
|||
const ret = false;
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbindgen_closure_wrapper2675 = function(arg0, arg1, arg2) {
|
||||
const ret = makeMutClosure(arg0, arg1, 766, __wbg_adapter_30);
|
||||
imports.wbg.__wbindgen_closure_wrapper2733 = function(arg0, arg1, arg2) {
|
||||
const ret = makeMutClosure(arg0, arg1, 792, __wbg_adapter_30);
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbindgen_closure_wrapper2677 = function(arg0, arg1, arg2) {
|
||||
const ret = makeMutClosure(arg0, arg1, 766, __wbg_adapter_30);
|
||||
imports.wbg.__wbindgen_closure_wrapper2735 = function(arg0, arg1, arg2) {
|
||||
const ret = makeMutClosure(arg0, arg1, 792, __wbg_adapter_30);
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbindgen_closure_wrapper2679 = function(arg0, arg1, arg2) {
|
||||
const ret = makeMutClosure(arg0, arg1, 766, __wbg_adapter_35);
|
||||
imports.wbg.__wbindgen_closure_wrapper2737 = function(arg0, arg1, arg2) {
|
||||
const ret = makeMutClosure(arg0, arg1, 792, __wbg_adapter_35);
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbindgen_closure_wrapper5044 = function(arg0, arg1, arg2) {
|
||||
const ret = makeMutClosure(arg0, arg1, 1269, __wbg_adapter_38);
|
||||
imports.wbg.__wbindgen_closure_wrapper5102 = function(arg0, arg1, arg2) {
|
||||
const ret = makeMutClosure(arg0, arg1, 1295, __wbg_adapter_38);
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbindgen_debug_string = function(arg0, arg1) {
|
Binary file not shown.
|
@ -19,7 +19,8 @@ pub struct App {
|
|||
map: Map,
|
||||
layer_selector: LayerSelector,
|
||||
widget_gallery: WidgetGallery,
|
||||
pub(crate) frame_time_history: History<f32>
|
||||
pub(crate) frame_time_history: History<f32>,
|
||||
add_layer_open: bool
|
||||
}
|
||||
|
||||
impl App {
|
||||
|
@ -49,6 +50,7 @@ impl App {
|
|||
layer_selector: LayerSelector::default(),
|
||||
widget_gallery: WidgetGallery::default(),
|
||||
frame_time_history: History::new(1..100, 0.5),
|
||||
add_layer_open: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -65,7 +67,7 @@ impl eframe::App for App {
|
|||
|
||||
top_bar(ctx);
|
||||
footer(ctx, &self);
|
||||
left_bar(ctx);
|
||||
left_bar(ctx, &mut self.add_layer_open, &mut self.map.layer_manager);
|
||||
|
||||
/*
|
||||
egui::SidePanel::left("sidebar")
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
|
||||
use std::convert::Into;
|
||||
use std::sync::LazyLock;
|
||||
use crate::map::tiles::LayerSource;
|
||||
use crate::map::tiles::{LayerSource, LayerTypeHint};
|
||||
|
||||
pub static BASELAYER_OSM: LazyLock<LayerSource> = LazyLock::new(|| LayerSource {
|
||||
source_id: 0x572dd260332d5f7f,
|
||||
tile_url: "https://tile.openstreetmap.org/{z}/{x}/{y}.png".into(),
|
||||
display_name: "OpenStreetMap (Base)".into()
|
||||
display_name: "OpenStreetMap".into(),
|
||||
type_hint: LayerTypeHint::Baselayer,
|
||||
location: "Worldwide".into(),
|
||||
source: "OpenStreetMap contributors".into()
|
||||
});
|
|
@ -1,35 +1,53 @@
|
|||
|
||||
use std::convert::Into;
|
||||
use std::sync::LazyLock;
|
||||
use crate::map::tiles::LayerSource;
|
||||
use crate::map::tiles::{LayerSource, LayerTypeHint};
|
||||
|
||||
pub static NOAA_MRMS_MERGED_CREF_QC_CONUS: LazyLock<LayerSource> = LazyLock::new(|| LayerSource {
|
||||
source_id: 0xC10FCD0566B7F952,
|
||||
tile_url: format!("{}/grib2.noaa_mrms_merged_composite_reflectivity_qc_CONUS/{{z}}/{{x}}/{{y}}.png", env!("TILER_BASE_URL")),
|
||||
display_name: "Merged Composite Reflectivity [CONUS-QCd] (NOAA MRMS)".into()
|
||||
display_name: "Merged Composite Reflectivity QCd".into(),
|
||||
type_hint: LayerTypeHint::RadarData,
|
||||
location: "Continental US".into(),
|
||||
source: "NOAA / MRMS".into()
|
||||
});
|
||||
pub static NOAA_MRMS_MERGED_CREF_QC_ALASKA: LazyLock<LayerSource> = LazyLock::new(|| LayerSource {
|
||||
source_id: 0x779F656117545E91,
|
||||
tile_url: format!("{}/grib2.noaa_mrms_merged_composite_reflectivity_qc_ALASKA/{{z}}/{{x}}/{{y}}.png", env!("TILER_BASE_URL")),
|
||||
display_name: "Merged Composite Reflectivity [ALASKA-QCd] (NOAA MRMS)".into()
|
||||
display_name: "Merged Composite Reflectivity QCd".into(),
|
||||
type_hint: LayerTypeHint::RadarData,
|
||||
location: "Alaska".into(),
|
||||
source: "NOAA / MRMS".into()
|
||||
});
|
||||
pub static NOAA_MRMS_MERGED_CREF_QC_CARIB: LazyLock<LayerSource> = LazyLock::new(|| LayerSource {
|
||||
source_id: 0xA7076E0145BC4BDD,
|
||||
tile_url: format!("{}/grib2.noaa_mrms_merged_composite_reflectivity_qc_CARIB/{{z}}/{{x}}/{{y}}.png", env!("TILER_BASE_URL")),
|
||||
display_name: "Merged Composite Reflectivity [CARIB-QCd] (NOAA MRMS)".into()
|
||||
display_name: "Merged Composite Reflectivity QCd".into(),
|
||||
type_hint: LayerTypeHint::RadarData,
|
||||
location: "Caribbean".into(),
|
||||
source: "NOAA / MRMS".into()
|
||||
});
|
||||
pub static NOAA_MRMS_MERGED_CREF_QC_GUAM: LazyLock<LayerSource> = LazyLock::new(|| LayerSource {
|
||||
source_id: 0xB277C4B00469BD02,
|
||||
tile_url: format!("{}/grib2.noaa_mrms_merged_composite_reflectivity_qc_GUAM/{{z}}/{{x}}/{{y}}.png", env!("TILER_BASE_URL")),
|
||||
display_name: "Merged Composite Reflectivity [GUAM-QCd] (NOAA MRMS)".into()
|
||||
display_name: "Merged Composite Reflectivity QCd".into(),
|
||||
type_hint: LayerTypeHint::RadarData,
|
||||
location: "Guam".into(),
|
||||
source: "NOAA / MRMS".into()
|
||||
});
|
||||
pub static NOAA_MRMS_MERGED_CREF_QC_HAWAII: LazyLock<LayerSource> = LazyLock::new(|| LayerSource {
|
||||
source_id: 0x238710F2CD275445,
|
||||
tile_url: format!("{}/grib2.noaa_mrms_merged_composite_reflectivity_qc_HAWAII/{{z}}/{{x}}/{{y}}.png", env!("TILER_BASE_URL")),
|
||||
display_name: "Merged Composite Reflectivity [HAWAII-QCd] (NOAA MRMS)".into()
|
||||
display_name: "Merged Composite Reflectivity QCd".into(),
|
||||
type_hint: LayerTypeHint::RadarData,
|
||||
location: "Hawaii".into(),
|
||||
source: "NOAA / MRMS".into()
|
||||
});
|
||||
pub static NOAA_MRMS_MERGED_RHOHV_3KM_CONUS: LazyLock<LayerSource> = LazyLock::new(|| LayerSource {
|
||||
source_id: 0x0EFAF68CB6E30B8F,
|
||||
tile_url: format!("{}/grib2.noaa_mrms_merged_rhohv_3km_CONUS/{{z}}/{{x}}/{{y}}.png", env!("TILER_BASE_URL")),
|
||||
display_name: "Merged RhoHV @ 3km [CONUS] (NOAA MRMS)".into()
|
||||
display_name: "Merged RhoHV @ 3km".into(),
|
||||
type_hint: LayerTypeHint::RadarData,
|
||||
location: "Continental US".into(),
|
||||
source: "NOAA / MRMS".into()
|
||||
});
|
|
@ -1,4 +1,5 @@
|
|||
use std::collections::HashMap;
|
||||
use std::fmt::{Debug, Formatter};
|
||||
use image::DynamicImage;
|
||||
use poll_promise::Promise;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
@ -12,7 +13,7 @@ pub type XCoord = usize;
|
|||
pub type YCoord = usize;
|
||||
pub type LayerId = u64;
|
||||
|
||||
#[derive(Default, Serialize, Deserialize)]
|
||||
#[derive(Default, Serialize, Deserialize, Debug)]
|
||||
#[serde(default)]
|
||||
pub struct LayerManager {
|
||||
pub registered_layers: HashMap<LayerId, LayerSource>,
|
||||
|
@ -79,13 +80,27 @@ impl LayerManager {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct LayerSource {
|
||||
pub source_id: u64,
|
||||
pub tile_url: String,
|
||||
pub display_name: String,
|
||||
pub location: String,
|
||||
pub source: String,
|
||||
pub type_hint: LayerTypeHint
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Copy, Clone, Debug)]
|
||||
pub enum LayerTypeHint {
|
||||
Baselayer,
|
||||
RadarData,
|
||||
}
|
||||
|
||||
pub struct Tile {
|
||||
pub promise: Promise<Result<DynamicImage, String>>,
|
||||
}
|
||||
impl Debug for Tile {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", if self.promise.ready().is_some() { "<loaded texture>" } else { "<texture promise>" })
|
||||
}
|
||||
}
|
|
@ -1,4 +1,6 @@
|
|||
use std::any::Any;
|
||||
use egui::emath::GuiRounding;
|
||||
use egui::{emath, CursorIcon, Id, InnerResponse, LayerId, Order, Sense, Ui, UiBuilder};
|
||||
|
||||
pub mod layer_selector;
|
||||
pub mod tokens;
|
||||
|
@ -48,7 +50,6 @@ pub trait UiExt {
|
|||
}
|
||||
response
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
impl UiExt for egui::Ui {
|
||||
|
|
|
@ -1,9 +1,28 @@
|
|||
use eframe::emath::Align;
|
||||
use egui::{Color32, CursorIcon, FontFamily, Frame, Id, Layout, Response, RichText, ScrollArea, Theme, Ui, vec2, Visuals};
|
||||
use egui::{Color32, CursorIcon, FontFamily, Frame, Id, Layout, Response, RichText, ScrollArea, Theme, Ui, vec2, Visuals, CornerRadius, Button, Margin, Label, Rect, Sense};
|
||||
use egui::epaint::Marginf;
|
||||
use egui::frame::Prepared;
|
||||
use tracing::debug;
|
||||
use crate::map::tiles::{LayerManager, LayerTypeHint};
|
||||
use crate::ui::tokens::DESIGN_TOKENS;
|
||||
use crate::ui::UiExt;
|
||||
|
||||
pub fn left_bar(ctx: &egui::Context) {
|
||||
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 left_bar(ctx: &egui::Context, add_open: &mut bool, layer_manager: &mut LayerManager) {
|
||||
|
||||
let mut frame = Frame::side_top_panel(&ctx.style());
|
||||
frame.inner_margin.top = 4;
|
||||
|
@ -14,15 +33,135 @@ pub fn left_bar(ctx: &egui::Context) {
|
|||
.frame(frame)
|
||||
.show(ctx, |ui| {
|
||||
pane_header(ui, "Layers", Some(egui_phosphor::regular::STACK), false, |ui| {
|
||||
ui.button(RichText::new(egui_phosphor::regular::PLUS).size(12.0));
|
||||
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;
|
||||
}
|
||||
});
|
||||
layer(ui, "Active Alerts", egui_phosphor::regular::TREE_STRUCTURE);
|
||||
layer(ui, "Composite Reflectivity", egui_phosphor::regular::TARGET);
|
||||
layer(ui, "TDWR Sites", egui_phosphor::regular::TREE_STRUCTURE);
|
||||
layer(ui, "WSR-88D Sites", egui_phosphor::regular::TREE_STRUCTURE);
|
||||
layer(ui, "OpenStreetMap", egui_phosphor::regular::MAP_TRIFOLD);
|
||||
|
||||
for (idx, layer) in layer_manager.active_layers.iter().enumerate() {
|
||||
ui.dnd_drag_source(
|
||||
Id::new(idx as u64 + layer),
|
||||
idx,
|
||||
|ui| {
|
||||
if ui.button("hello").clicked() {
|
||||
*add_open = true;
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
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_layers {
|
||||
// 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| {
|
||||
|
||||
let icon = match registered_layer.type_hint {
|
||||
LayerTypeHint::Baselayer => egui_phosphor::regular::MAP_TRIFOLD,
|
||||
LayerTypeHint::RadarData => egui_phosphor::regular::TARGET
|
||||
};
|
||||
|
||||
ui.label(RichText::new(icon).size(24.0));
|
||||
});
|
||||
|
||||
ui.vertical(|ui| {
|
||||
ui.label(®istered_layer.display_name);
|
||||
ui.horizontal(|ui| {
|
||||
|
||||
let type_hint = match registered_layer.type_hint {
|
||||
LayerTypeHint::Baselayer => "Baselayer",
|
||||
LayerTypeHint::RadarData => "Radar data",
|
||||
};
|
||||
let icon = match registered_layer.type_hint {
|
||||
LayerTypeHint::Baselayer => egui_phosphor::regular::MAP_TRIFOLD,
|
||||
LayerTypeHint::RadarData => egui_phosphor::regular::TARGET
|
||||
};
|
||||
|
||||
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(*id);
|
||||
debug!("{:?}", layer_manager);
|
||||
*add_open = false;
|
||||
}
|
||||
frame.paint(ui);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn pane_header(ui: &mut Ui, title: &str, icon: Option<&str>, top_separator: bool, add_contents: impl FnOnce(&mut Ui)) {
|
||||
|
@ -57,8 +196,9 @@ fn pane_header(ui: &mut Ui, title: &str, icon: Option<&str>, top_separator: bool
|
|||
ui.full_span_separator();
|
||||
}
|
||||
|
||||
/*
|
||||
fn layer(ui: &mut Ui, title: &str, icon: &str) {
|
||||
let response = ui.dnd_drag_source(Id::new(title), "test", |ui| {
|
||||
ui.passthrough_dnd_drag_source(Id::new(title), title.to_owned(), |ui| {
|
||||
let mut frame = Frame::new();
|
||||
//frame.fill = ui.style().visuals.hyperlink_color;
|
||||
frame.inner_margin.left = 8;
|
||||
|
@ -99,11 +239,12 @@ fn layer(ui: &mut Ui, title: &str, icon: &str) {
|
|||
}
|
||||
let response = frame.allocate_space(ui);
|
||||
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();
|
||||
frame.frame.fill = ;
|
||||
ui.ctx().output_mut(|u| u.cursor_icon = CursorIcon::Grab);
|
||||
}
|
||||
frame.paint(ui);
|
||||
}).response;
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
*/
|
|
@ -1,7 +1,7 @@
|
|||
|
||||
|
||||
use std::sync::LazyLock;
|
||||
use egui::{Color32, FontData, FontFamily, Stroke, Style, Theme, Vec2};
|
||||
use egui::{Color32, CornerRadius, FontData, FontFamily, Stroke, Style, Theme, Vec2};
|
||||
use egui::epaint::text::{FontInsert, FontPriority, InsertFontFamily};
|
||||
use egui::style::Selection;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
|
Loading…
Add table
Reference in a new issue