feat: current progress - connect panel layout, manual checkbox, serial enum
This commit is contained in:
216
Cargo.lock
generated
216
Cargo.lock
generated
@@ -36,7 +36,7 @@ dependencies = [
|
||||
"accesskit",
|
||||
"accesskit_consumer 0.36.0",
|
||||
"atspi-common",
|
||||
"phf",
|
||||
"phf 0.13.1",
|
||||
"serde",
|
||||
"zvariant",
|
||||
]
|
||||
@@ -1079,6 +1079,21 @@ dependencies = [
|
||||
"winit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "egui_extras"
|
||||
version = "0.34.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8c609fc87f6c70ffd3afd679cbb294985096d2fc0be33e762ad5614bde4925bc"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"egui",
|
||||
"enum-map",
|
||||
"image",
|
||||
"log",
|
||||
"mime_guess2",
|
||||
"profiling",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "egui_glow"
|
||||
version = "0.34.2"
|
||||
@@ -1117,6 +1132,26 @@ version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099"
|
||||
|
||||
[[package]]
|
||||
name = "enum-map"
|
||||
version = "2.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6866f3bfdf8207509a033af1a75a7b08abda06bbaaeae6669323fd5a097df2e9"
|
||||
dependencies = [
|
||||
"enum-map-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "enum-map-derive"
|
||||
version = "0.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f282cfdfe92516eb26c2af8589c274c7c17681f5ecc03c18255fe741c6aa64eb"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "enumflags2"
|
||||
version = "0.7.12"
|
||||
@@ -1238,9 +1273,11 @@ dependencies = [
|
||||
"anyhow",
|
||||
"bytemuck",
|
||||
"eframe",
|
||||
"egui_extras",
|
||||
"env_logger",
|
||||
"glam",
|
||||
"image",
|
||||
"serialport",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1854,6 +1891,16 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "io-kit-sys"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "617ee6cf8e3f66f3b4ea67a4058564628cde41901316e19f559e14c7c72c5e7b"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"mach2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.2"
|
||||
@@ -2064,6 +2111,26 @@ dependencies = [
|
||||
"redox_syscall 0.7.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libudev"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78b324152da65df7bb95acfcaab55e3097ceaab02fb19b228a9eb74d55f135e0"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"libudev-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libudev-sys"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c8469b4a23b962c1396b9b451dda50ef5b283e8dd309d69033475fa9b334324"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linebender_resource_handle"
|
||||
version = "0.1.1"
|
||||
@@ -2118,6 +2185,15 @@ dependencies = [
|
||||
"imgref",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mach2"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "maybe-rayon"
|
||||
version = "0.1.1"
|
||||
@@ -2152,6 +2228,24 @@ dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mime"
|
||||
version = "0.3.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||
|
||||
[[package]]
|
||||
name = "mime_guess2"
|
||||
version = "2.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1706dc14a2e140dec0a7a07109d9a3d5890b81e85bd6c60b906b249a77adf0ca"
|
||||
dependencies = [
|
||||
"mime",
|
||||
"phf 0.11.3",
|
||||
"phf_shared 0.11.3",
|
||||
"unicase",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.8.9"
|
||||
@@ -2234,6 +2328,17 @@ version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.26.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "no_std_io2"
|
||||
version = "0.9.4"
|
||||
@@ -2756,17 +2861,37 @@ version = "2.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
||||
|
||||
[[package]]
|
||||
name = "phf"
|
||||
version = "0.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078"
|
||||
dependencies = [
|
||||
"phf_macros 0.11.3",
|
||||
"phf_shared 0.11.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf"
|
||||
version = "0.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf"
|
||||
dependencies = [
|
||||
"phf_macros",
|
||||
"phf_shared",
|
||||
"phf_macros 0.13.1",
|
||||
"phf_shared 0.13.1",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_generator"
|
||||
version = "0.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d"
|
||||
dependencies = [
|
||||
"phf_shared 0.11.3",
|
||||
"rand 0.8.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_generator"
|
||||
version = "0.13.1"
|
||||
@@ -2774,7 +2899,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"phf_shared",
|
||||
"phf_shared 0.13.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_macros"
|
||||
version = "0.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216"
|
||||
dependencies = [
|
||||
"phf_generator 0.11.3",
|
||||
"phf_shared 0.11.3",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"unicase",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2783,13 +2922,23 @@ version = "0.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef"
|
||||
dependencies = [
|
||||
"phf_generator",
|
||||
"phf_shared",
|
||||
"phf_generator 0.13.1",
|
||||
"phf_shared 0.13.1",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_shared"
|
||||
version = "0.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5"
|
||||
dependencies = [
|
||||
"siphasher",
|
||||
"unicase",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_shared"
|
||||
version = "0.13.1"
|
||||
@@ -3028,6 +3177,15 @@ version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.8.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a"
|
||||
dependencies = [
|
||||
"rand_core 0.6.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.9.4"
|
||||
@@ -3035,7 +3193,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea"
|
||||
dependencies = [
|
||||
"rand_chacha",
|
||||
"rand_core",
|
||||
"rand_core 0.9.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3045,9 +3203,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core",
|
||||
"rand_core 0.9.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.9.5"
|
||||
@@ -3090,7 +3254,7 @@ dependencies = [
|
||||
"num-traits",
|
||||
"paste",
|
||||
"profiling",
|
||||
"rand",
|
||||
"rand 0.9.4",
|
||||
"rand_chacha",
|
||||
"simd_helpers",
|
||||
"thiserror 2.0.18",
|
||||
@@ -3382,6 +3546,25 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serialport"
|
||||
version = "4.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4d91116f97173694f1642263b2ff837f80d933aa837e2314969f6728f661df3"
|
||||
dependencies = [
|
||||
"bitflags 2.11.1",
|
||||
"cfg-if",
|
||||
"core-foundation 0.10.1",
|
||||
"core-foundation-sys",
|
||||
"io-kit-sys",
|
||||
"libudev",
|
||||
"mach2",
|
||||
"nix",
|
||||
"scopeguard",
|
||||
"unescaper",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
@@ -3786,6 +3969,21 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unescaper"
|
||||
version = "0.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4064ed685c487dbc25bd3f0e9548f2e34bab9d18cefc700f9ec2dba74ba1138e"
|
||||
dependencies = [
|
||||
"thiserror 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicase"
|
||||
version = "2.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
|
||||
@@ -10,3 +10,5 @@ bytemuck = { version = "1", features = ["derive"] }
|
||||
glam = "0.32.1"
|
||||
image = { version = "0.25.10", features = ["png", "jpeg"] }
|
||||
anyhow = "1.0.102"
|
||||
serialport = "4.9.0"
|
||||
egui_extras = { version = "0.34.2", features = ["image"] }
|
||||
1
JE-Skin
Submodule
1
JE-Skin
Submodule
Submodule JE-Skin added at 59e9203363
1
eskin-finger-sdk
Submodule
1
eskin-finger-sdk
Submodule
Submodule eskin-finger-sdk added at 705375085f
25
src/app.rs
25
src/app.rs
@@ -2,22 +2,30 @@ use std::time::Instant;
|
||||
|
||||
use eframe::{egui, egui_wgpu};
|
||||
|
||||
use crate::theme::{ENGINEERING_DARK, apply_theme};
|
||||
use crate::theme::{ENGINEERING_DARK, apply_fonts, apply_theme};
|
||||
use crate::{
|
||||
matrix::{MATRIX_COLS, MATRIX_ROWS},
|
||||
render::{BackgroundRenderResources, WgpuBackgroundCallback},
|
||||
ui::{FloatingPanelState, draw_config_panel, draw_scene_panel, draw_stats_panel},
|
||||
ui::{
|
||||
ConfigPanelState, ConnectPanelState, FloatingPanelState, draw_config_panel,
|
||||
draw_connect_panel, draw_scene_panel, draw_stats_panel,
|
||||
},
|
||||
};
|
||||
|
||||
pub struct EskinDesktopApp {
|
||||
connect_panel: FloatingPanelState,
|
||||
connect_state: ConnectPanelState,
|
||||
scene_panel: FloatingPanelState,
|
||||
config_panel: FloatingPanelState,
|
||||
config_state: ConfigPanelState,
|
||||
stats_panel: FloatingPanelState,
|
||||
started_at: Instant,
|
||||
}
|
||||
|
||||
impl EskinDesktopApp {
|
||||
pub fn new(cc: &eframe::CreationContext<'_>) -> Self {
|
||||
egui_extras::install_image_loaders(&cc.egui_ctx);
|
||||
apply_fonts(&cc.egui_ctx);
|
||||
apply_theme(&cc.egui_ctx, &ENGINEERING_DARK);
|
||||
|
||||
let wgpu_state = cc
|
||||
@@ -36,8 +44,11 @@ impl EskinDesktopApp {
|
||||
));
|
||||
|
||||
Self {
|
||||
connect_panel: FloatingPanelState::new([0.0, 0.0], [0.0, 0.0]),
|
||||
connect_state: ConnectPanelState::default(),
|
||||
scene_panel: FloatingPanelState::new([16.0, 48.0], [16.0, 48.0]),
|
||||
config_panel: FloatingPanelState::new([840.0, 48.0], [128.0, 48.0]),
|
||||
config_state: ConfigPanelState::default(),
|
||||
stats_panel: FloatingPanelState::new([16.0, 520.0], [240.0, 48.0]),
|
||||
started_at: Instant::now(),
|
||||
}
|
||||
@@ -61,16 +72,18 @@ impl EskinDesktopApp {
|
||||
fn draw_toolbar(&mut self, ui: &mut egui::Ui) {
|
||||
egui::Panel::top("main_menu").show_inside(ui, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.checkbox(&mut self.scene_panel.visible, "Scene");
|
||||
ui.checkbox(&mut self.config_panel.visible, "Config");
|
||||
ui.checkbox(&mut self.stats_panel.visible, "Stats");
|
||||
ui.checkbox(&mut self.connect_panel.visible, "连接");
|
||||
ui.checkbox(&mut self.scene_panel.visible, "场景");
|
||||
ui.checkbox(&mut self.config_panel.visible, "配置");
|
||||
ui.checkbox(&mut self.stats_panel.visible, "统计");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn draw_floating_panels(&mut self, ctx: &egui::Context) {
|
||||
draw_connect_panel(ctx, &mut self.connect_panel, &mut self.connect_state);
|
||||
draw_scene_panel(ctx, &mut self.scene_panel);
|
||||
draw_config_panel(ctx, &mut self.config_panel);
|
||||
draw_config_panel(ctx, &mut self.config_panel, &mut self.config_state);
|
||||
draw_stats_panel(ctx, &mut self.stats_panel);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,19 +3,23 @@ mod matrix;
|
||||
mod render;
|
||||
mod theme;
|
||||
mod ui;
|
||||
|
||||
mod utils;
|
||||
use app::EskinDesktopApp;
|
||||
use eframe::egui;
|
||||
|
||||
fn main() -> eframe::Result<()> {
|
||||
env_logger::init();
|
||||
|
||||
let options = eframe::NativeOptions {
|
||||
renderer: eframe::Renderer::Wgpu,
|
||||
viewport: egui::ViewportBuilder::default()
|
||||
.with_inner_size([1920.0, 1080.0])
|
||||
.with_min_inner_size([1280.0, 720.0]),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
eframe::run_native(
|
||||
"Eskin Model Player",
|
||||
"Eskin 模型播放器",
|
||||
options,
|
||||
Box::new(|cc| Ok(Box::new(EskinDesktopApp::new(cc)))),
|
||||
)
|
||||
|
||||
46
src/theme.rs
46
src/theme.rs
@@ -22,8 +22,8 @@ pub const ENGINEERING_DARK: AppTheme = AppTheme {
|
||||
panel_deep: egui::Color32::from_rgb(14, 18, 24),
|
||||
border: egui::Color32::from_rgb(53, 75, 92),
|
||||
border_soft: egui::Color32::from_rgb(36, 53, 66),
|
||||
text: egui::Color32::from_rgb(210, 218, 232),
|
||||
text_dim: egui::Color32::from_rgb(155, 168, 190),
|
||||
text: egui::Color32::from_rgb(242, 246, 252),
|
||||
text_dim: egui::Color32::from_rgb(206, 216, 230),
|
||||
accent: egui::Color32::from_rgb(255, 118, 47),
|
||||
accent_hot: egui::Color32::from_rgb(255, 169, 77),
|
||||
radius: 2,
|
||||
@@ -43,7 +43,7 @@ pub fn apply_theme(ctx: &egui::Context, theme: &AppTheme) {
|
||||
|
||||
visuals.widgets.noninteractive.bg_fill = theme.panel_strong;
|
||||
visuals.widgets.noninteractive.bg_stroke = egui::Stroke::new(1.0, theme.border_soft);
|
||||
visuals.widgets.noninteractive.fg_stroke = egui::Stroke::new(1.0, theme.text_dim);
|
||||
visuals.widgets.noninteractive.fg_stroke = egui::Stroke::new(1.0, theme.text);
|
||||
visuals.widgets.inactive.bg_fill = theme.panel_strong;
|
||||
visuals.widgets.inactive.bg_stroke = egui::Stroke::new(1.0, theme.border_soft);
|
||||
visuals.widgets.inactive.fg_stroke = egui::Stroke::new(1.0, theme.text);
|
||||
@@ -76,6 +76,46 @@ pub fn apply_theme(ctx: &egui::Context, theme: &AppTheme) {
|
||||
ctx.set_global_style(style);
|
||||
}
|
||||
|
||||
pub fn apply_fonts(ctx: &egui::Context) {
|
||||
let mut fonts = egui::FontDefinitions::default();
|
||||
|
||||
fonts.font_data.insert(
|
||||
"Hack-Bold".to_owned(),
|
||||
egui::FontData::from_static(include_bytes!("../static/Hack-Bold.ttf")).into(),
|
||||
);
|
||||
|
||||
if let Ok(font_data) = std::fs::read(r"C:\Windows\Fonts\msyh.ttc") {
|
||||
fonts.font_data.insert(
|
||||
"Microsoft-YaHei".to_owned(),
|
||||
egui::FontData::from_owned(font_data).into(),
|
||||
);
|
||||
}
|
||||
|
||||
fonts
|
||||
.families
|
||||
.entry(egui::FontFamily::Proportional)
|
||||
.or_default()
|
||||
.insert(0, "Hack-Bold".to_owned());
|
||||
fonts
|
||||
.families
|
||||
.entry(egui::FontFamily::Proportional)
|
||||
.or_default()
|
||||
.push("Microsoft-YaHei".to_owned());
|
||||
|
||||
fonts
|
||||
.families
|
||||
.entry(egui::FontFamily::Monospace)
|
||||
.or_default()
|
||||
.insert(0, "Hack-Bold".to_owned());
|
||||
fonts
|
||||
.families
|
||||
.entry(egui::FontFamily::Monospace)
|
||||
.or_default()
|
||||
.push("Microsoft-YaHei".to_owned());
|
||||
|
||||
ctx.set_fonts(fonts);
|
||||
}
|
||||
|
||||
pub fn panel_frame(ctx: &egui::Context) -> egui::Frame {
|
||||
let style = ctx.global_style();
|
||||
egui::Frame::window(&style)
|
||||
|
||||
524
src/ui.rs
524
src/ui.rs
@@ -1,6 +1,9 @@
|
||||
use eframe::egui;
|
||||
|
||||
use crate::theme::{accent_text, dim_text, group_frame, panel_frame, tag_button};
|
||||
use crate::{
|
||||
theme::{ENGINEERING_DARK, accent_text, dim_text, group_frame, panel_frame, tag_button},
|
||||
utils::serial_enum,
|
||||
};
|
||||
|
||||
pub struct FloatingPanelState {
|
||||
pub visible: bool,
|
||||
@@ -8,6 +11,50 @@ pub struct FloatingPanelState {
|
||||
tag_pos: egui::Pos2,
|
||||
}
|
||||
|
||||
pub struct ConfigPanelState {
|
||||
pub mode: SerialMode,
|
||||
pub port: String,
|
||||
pub baud_rate: u32,
|
||||
pub data_bits: u8,
|
||||
pub stop_bits: u8,
|
||||
pub parity: Parity,
|
||||
pub timeout_ms: u32,
|
||||
pub module_addr: u8,
|
||||
pub connected: bool,
|
||||
pub auto_reconnect: bool,
|
||||
pub manual_tx: String,
|
||||
pub model_path: String,
|
||||
}
|
||||
|
||||
pub struct ConnectPanelState {
|
||||
pub mode: SerialMode,
|
||||
pub port: Vec<String>,
|
||||
pub duration: u8,
|
||||
pub manual: bool,
|
||||
pub rows: u8,
|
||||
pub cols: u8,
|
||||
pub connection: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub enum SerialMode {
|
||||
SingleModule,
|
||||
Manual,
|
||||
Model,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Parity {
|
||||
None,
|
||||
Odd,
|
||||
Even,
|
||||
}
|
||||
|
||||
pub enum IconButtonIcon<'a> {
|
||||
Font(&'a str),
|
||||
Png(egui::ImageSource<'a>),
|
||||
}
|
||||
|
||||
impl FloatingPanelState {
|
||||
pub fn new(default_pos: [f32; 2], tag_pos: [f32; 2]) -> Self {
|
||||
Self {
|
||||
@@ -18,49 +65,429 @@ impl FloatingPanelState {
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ConfigPanelState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
mode: SerialMode::SingleModule,
|
||||
port: "COM3".to_owned(),
|
||||
baud_rate: 115_200,
|
||||
data_bits: 8,
|
||||
stop_bits: 1,
|
||||
parity: Parity::None,
|
||||
timeout_ms: 1000,
|
||||
module_addr: 1,
|
||||
connected: false,
|
||||
auto_reconnect: true,
|
||||
manual_tx: "01 03 00 00 00 02".to_owned(),
|
||||
model_path: "model/default.eskin".to_owned(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ConnectPanelState {
|
||||
fn default() -> Self {
|
||||
let port = serial_enum().unwrap();
|
||||
Self {
|
||||
mode: SerialMode::SingleModule,
|
||||
port,
|
||||
duration: 10,
|
||||
manual: false,
|
||||
rows: 12,
|
||||
cols: 7,
|
||||
connection: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn draw_scene_panel(ctx: &egui::Context, panel: &mut FloatingPanelState) {
|
||||
draw_floating_panel(ctx, panel, "Scene", "scene_panel", |ui| {
|
||||
draw_floating_panel(ctx, panel, "场景", "scene_panel", |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.colored_label(dim_text(), "View");
|
||||
let _ = ui.selectable_label(true, "Clusters");
|
||||
let _ = ui.selectable_label(false, "Triangles");
|
||||
ui.colored_label(dim_text(), "视图");
|
||||
let _ = ui.selectable_label(true, "簇");
|
||||
let _ = ui.selectable_label(false, "三角形");
|
||||
});
|
||||
ui.separator();
|
||||
group_frame().show(ui, |ui| {
|
||||
ui.label("Models / materials / lights");
|
||||
ui.label("target tasks 64");
|
||||
ui.label("slack cache 100.0%");
|
||||
ui.label("模型 / 材质 / 灯光");
|
||||
ui.label("目标任务 64");
|
||||
ui.label("缓存命中 100.0%");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
pub fn draw_config_panel(ctx: &egui::Context, panel: &mut FloatingPanelState) {
|
||||
draw_floating_panel(ctx, panel, "Config", "config_panel", |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Shader");
|
||||
let _ = ui.selectable_label(true, "Clusters");
|
||||
let _ = ui.selectable_label(false, "wireframe");
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Grid");
|
||||
ui.label("x 32");
|
||||
ui.label("z 32");
|
||||
ui.label("side 1.000");
|
||||
pub fn draw_connect_panel(
|
||||
ctx: &egui::Context,
|
||||
panel: &mut FloatingPanelState,
|
||||
config: &mut ConnectPanelState,
|
||||
) {
|
||||
draw_center_floating_panel(ctx, panel, "connect_center_panel", 64.0, |ui| {
|
||||
ui.set_min_width(320.0);
|
||||
|
||||
ui.vertical(|ui| {
|
||||
// ui.horizontal_centered(|ui| {
|
||||
// mode_button(ui, &mut config.mode, SerialMode::SingleModule, "单模块");
|
||||
// ui.add_space(8.0);
|
||||
// mode_button(ui, &mut config.mode, SerialMode::Manual, "全手");
|
||||
// ui.add_space(8.0);
|
||||
// mode_button(ui, &mut config.mode, SerialMode::Model, "模型");
|
||||
// });
|
||||
|
||||
let button_width = 96.0;
|
||||
let gap = 8.0;
|
||||
let total_width = button_width * 3.0 + gap * 2.0;
|
||||
let left_space = ((ui.available_width() - total_width) * 0.5).max(0.0);
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.add_space(left_space);
|
||||
|
||||
mode_button(ui, &mut config.mode, SerialMode::SingleModule, "单模块");
|
||||
ui.add_space(gap);
|
||||
mode_button(ui, &mut config.mode, SerialMode::Manual, "全手");
|
||||
ui.add_space(gap);
|
||||
mode_button(ui, &mut config.mode, SerialMode::Model, "模型");
|
||||
});
|
||||
|
||||
ui.add_space(6.0);
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.add_space(10.0);
|
||||
ui.add(
|
||||
egui::Image::new(egui::include_image!("../static/cpu.png"))
|
||||
.fit_to_exact_size(egui::vec2(72.0, 72.0)),
|
||||
);
|
||||
|
||||
ui.vertical(|ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.colored_label(ENGINEERING_DARK.text, "串口");
|
||||
egui::ComboBox::from_id_salt("connect_ports")
|
||||
.width(130.0)
|
||||
.selected_text(
|
||||
config
|
||||
.port
|
||||
.first()
|
||||
.map(String::as_str)
|
||||
.unwrap_or("无可用串口"),
|
||||
)
|
||||
.show_ui(ui, |ui| {
|
||||
for port in &config.port {
|
||||
let _ = ui.selectable_label(false, port);
|
||||
}
|
||||
});
|
||||
ui.add_space(10.0);
|
||||
|
||||
ui.label("频率");
|
||||
ui.add_sized(
|
||||
egui::vec2(72.0, 20.0),
|
||||
egui::DragValue::new(&mut config.duration).range(1..=120),
|
||||
);
|
||||
});
|
||||
|
||||
ui.add_space(10.0);
|
||||
ui.horizontal(|ui| {
|
||||
ui.checkbox(&mut config.manual, "手动");
|
||||
|
||||
ui.add_enabled_ui(config.manual, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.colored_label(ENGINEERING_DARK.text_dim, "行");
|
||||
ui.add_sized(
|
||||
egui::vec2(48.0, 20.0),
|
||||
egui::DragValue::new(&mut config.rows).range(1..=64),
|
||||
);
|
||||
ui.colored_label(ENGINEERING_DARK.text_dim, "列");
|
||||
ui.add_sized(
|
||||
egui::vec2(48.0, 20.0),
|
||||
egui::DragValue::new(&mut config.cols).range(1..=64),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
})
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
pub fn draw_config_panel(
|
||||
ctx: &egui::Context,
|
||||
panel: &mut FloatingPanelState,
|
||||
config: &mut ConfigPanelState,
|
||||
) {
|
||||
draw_floating_panel(ctx, panel, "配置", "config_panel", |ui| {
|
||||
ui.set_min_width(560.0);
|
||||
|
||||
draw_mode_row(ui, config);
|
||||
ui.separator();
|
||||
draw_connection_row(ui, config);
|
||||
ui.add_space(8.0);
|
||||
draw_serial_grid(ui, config);
|
||||
ui.add_space(8.0);
|
||||
draw_mode_body(ui, config);
|
||||
});
|
||||
}
|
||||
|
||||
fn draw_mode_row(ui: &mut egui::Ui, config: &mut ConfigPanelState) {
|
||||
ui.horizontal(|ui| {
|
||||
ui.colored_label(dim_text(), "模式");
|
||||
ui.add_space(12.0);
|
||||
|
||||
mode_button(ui, &mut config.mode, SerialMode::SingleModule, "单模块");
|
||||
mode_button(ui, &mut config.mode, SerialMode::Manual, "全手");
|
||||
mode_button(ui, &mut config.mode, SerialMode::Model, "模型");
|
||||
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
ui.checkbox(&mut config.auto_reconnect, "自动");
|
||||
ui.colored_label(dim_text(), "重连");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn draw_connection_row(ui: &mut egui::Ui, config: &mut ConfigPanelState) {
|
||||
ui.horizontal(|ui| {
|
||||
ui.add(
|
||||
egui::Image::new(egui::include_image!("../static/cpu.png"))
|
||||
.fit_to_exact_size(egui::vec2(72.0, 72.0)),
|
||||
);
|
||||
|
||||
ui.add_space(10.0);
|
||||
|
||||
ui.vertical(|ui| {
|
||||
ui.label(format!("端口 {}", config.port));
|
||||
ui.label(format!("波特率 {}", config.baud_rate));
|
||||
let status = if config.connected {
|
||||
"已连接"
|
||||
} else {
|
||||
"未连接"
|
||||
};
|
||||
let status_color = if config.connected {
|
||||
egui::Color32::from_rgb(158, 184, 101)
|
||||
} else {
|
||||
egui::Color32::from_rgb(255, 98, 82)
|
||||
};
|
||||
ui.colored_label(status_color, status);
|
||||
});
|
||||
|
||||
ui.add_space(22.0);
|
||||
|
||||
let button_text = if config.connected { "断开" } else { "连接" };
|
||||
if ui
|
||||
.add(
|
||||
egui::Button::new(button_text)
|
||||
.fill(ENGINEERING_DARK.accent)
|
||||
.stroke(egui::Stroke::new(1.0, ENGINEERING_DARK.accent_hot))
|
||||
.min_size(egui::vec2(120.0, 30.0)),
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
config.connected = !config.connected;
|
||||
}
|
||||
|
||||
ui.add_space(18.0);
|
||||
ui.colored_label(
|
||||
egui::Color32::from_rgb(158, 184, 101),
|
||||
if config.auto_reconnect {
|
||||
"链路保护 开"
|
||||
} else {
|
||||
"链路保护 关"
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
fn draw_serial_grid(ui: &mut egui::Ui, config: &mut ConfigPanelState) {
|
||||
group_frame().show(ui, |ui| {
|
||||
egui::Grid::new("serial_config_grid")
|
||||
.num_columns(4)
|
||||
.spacing(egui::vec2(10.0, 5.0))
|
||||
.striped(true)
|
||||
.show(ui, |ui| {
|
||||
ui.label("端口");
|
||||
ui.add_sized(
|
||||
egui::vec2(110.0, 20.0),
|
||||
egui::TextEdit::singleline(&mut config.port),
|
||||
);
|
||||
|
||||
ui.label("波特率");
|
||||
baud_combo(ui, config);
|
||||
ui.end_row();
|
||||
|
||||
ui.label("数据位");
|
||||
ui.add_sized(
|
||||
egui::vec2(70.0, 20.0),
|
||||
egui::DragValue::new(&mut config.data_bits).range(5..=8),
|
||||
);
|
||||
|
||||
ui.label("校验");
|
||||
parity_combo(ui, config);
|
||||
ui.end_row();
|
||||
|
||||
ui.label("停止位");
|
||||
ui.add_sized(
|
||||
egui::vec2(70.0, 20.0),
|
||||
egui::DragValue::new(&mut config.stop_bits).range(1..=2),
|
||||
);
|
||||
|
||||
ui.label("超时");
|
||||
ui.horizontal(|ui| {
|
||||
ui.add_sized(
|
||||
egui::vec2(84.0, 20.0),
|
||||
egui::DragValue::new(&mut config.timeout_ms)
|
||||
.range(50..=30_000)
|
||||
.speed(50),
|
||||
);
|
||||
ui.colored_label(dim_text(), "毫秒");
|
||||
});
|
||||
ui.end_row();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn draw_mode_body(ui: &mut egui::Ui, config: &mut ConfigPanelState) {
|
||||
group_frame().show(ui, |ui| match config.mode {
|
||||
SerialMode::SingleModule => {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("模块地址");
|
||||
ui.add_sized(
|
||||
egui::vec2(80.0, 20.0),
|
||||
egui::DragValue::new(&mut config.module_addr).range(1..=247),
|
||||
);
|
||||
ui.add_space(16.0);
|
||||
if ui.add(tag_button("读取信息")).clicked() {}
|
||||
if ui.add(tag_button("探测")).clicked() {}
|
||||
});
|
||||
ui.separator();
|
||||
ui.horizontal(|ui| {
|
||||
ui.colored_label(dim_text(), "状态");
|
||||
ui.label("就绪");
|
||||
ui.colored_label(dim_text(), "接收");
|
||||
ui.label("0 字节");
|
||||
ui.colored_label(dim_text(), "发送");
|
||||
ui.label("0 字节");
|
||||
});
|
||||
}
|
||||
SerialMode::Manual => {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("发送");
|
||||
ui.add_sized(
|
||||
egui::vec2(300.0, 20.0),
|
||||
egui::TextEdit::singleline(&mut config.manual_tx),
|
||||
);
|
||||
if ui.add(tag_button("发送")).clicked() {}
|
||||
if ui.add(tag_button("清空")).clicked() {
|
||||
config.manual_tx.clear();
|
||||
}
|
||||
});
|
||||
}
|
||||
SerialMode::Model => {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("模型");
|
||||
ui.add_sized(
|
||||
egui::vec2(300.0, 20.0),
|
||||
egui::TextEdit::singleline(&mut config.model_path),
|
||||
);
|
||||
if ui.add(tag_button("加载")).clicked() {}
|
||||
if ui.add(tag_button("运行")).clicked() {}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn icon_button<'a>(
|
||||
ui: &mut egui::Ui,
|
||||
icon: IconButtonIcon<'a>,
|
||||
tooltip: impl Into<egui::WidgetText>,
|
||||
) -> egui::Response {
|
||||
icon_button_sized(ui, icon, tooltip, egui::vec2(28.0, 24.0))
|
||||
}
|
||||
|
||||
pub fn icon_button_sized<'a>(
|
||||
ui: &mut egui::Ui,
|
||||
icon: IconButtonIcon<'a>,
|
||||
tooltip: impl Into<egui::WidgetText>,
|
||||
size: egui::Vec2,
|
||||
) -> egui::Response {
|
||||
let button = match icon {
|
||||
IconButtonIcon::Font(icon) => egui::Button::new(
|
||||
egui::RichText::new(icon)
|
||||
.color(egui::Color32::WHITE)
|
||||
.size(size.y - 8.0),
|
||||
),
|
||||
IconButtonIcon::Png(source) => egui::Button::image(
|
||||
egui::Image::new(source).fit_to_exact_size(egui::vec2(size.y - 8.0, size.y - 8.0)),
|
||||
),
|
||||
}
|
||||
.fill(ENGINEERING_DARK.panel_strong)
|
||||
.stroke(egui::Stroke::new(1.0, ENGINEERING_DARK.border))
|
||||
.corner_radius(egui::CornerRadius::same(2))
|
||||
.min_size(size);
|
||||
|
||||
ui.add(button).on_hover_text(tooltip)
|
||||
}
|
||||
|
||||
fn mode_button(ui: &mut egui::Ui, mode: &mut SerialMode, value: SerialMode, label: &'static str) {
|
||||
let selected = *mode == value;
|
||||
let fill = if selected {
|
||||
ENGINEERING_DARK.accent
|
||||
} else {
|
||||
ENGINEERING_DARK.panel_strong
|
||||
};
|
||||
let stroke = if selected {
|
||||
ENGINEERING_DARK.accent_hot
|
||||
} else {
|
||||
ENGINEERING_DARK.border_soft
|
||||
};
|
||||
|
||||
if ui
|
||||
.add(
|
||||
egui::Button::new(egui::RichText::new(label).color(egui::Color32::WHITE))
|
||||
.fill(fill)
|
||||
.stroke(egui::Stroke::new(1.0, stroke))
|
||||
.min_size(egui::vec2(96.0, 24.0)),
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
*mode = value;
|
||||
}
|
||||
}
|
||||
|
||||
fn baud_combo(ui: &mut egui::Ui, config: &mut ConfigPanelState) {
|
||||
egui::ComboBox::from_id_salt("serial_baud_rate")
|
||||
.width(110.0)
|
||||
.selected_text(config.baud_rate.to_string())
|
||||
.show_ui(ui, |ui| {
|
||||
for baud_rate in [
|
||||
9_600, 19_200, 38_400, 57_600, 115_200, 230_400, 460_800, 921_600,
|
||||
] {
|
||||
ui.selectable_value(&mut config.baud_rate, baud_rate, baud_rate.to_string());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn parity_combo(ui: &mut egui::Ui, config: &mut ConfigPanelState) {
|
||||
egui::ComboBox::from_id_salt("serial_parity")
|
||||
.width(110.0)
|
||||
.selected_text(match config.parity {
|
||||
Parity::None => "无",
|
||||
Parity::Odd => "奇",
|
||||
Parity::Even => "偶",
|
||||
})
|
||||
.show_ui(ui, |ui| {
|
||||
ui.selectable_value(&mut config.parity, Parity::None, "无");
|
||||
ui.selectable_value(&mut config.parity, Parity::Odd, "奇");
|
||||
ui.selectable_value(&mut config.parity, Parity::Even, "偶");
|
||||
});
|
||||
}
|
||||
|
||||
pub fn draw_stats_panel(ctx: &egui::Context, panel: &mut FloatingPanelState) {
|
||||
draw_floating_panel(ctx, panel, "Stats", "stats_panel", |ui| {
|
||||
draw_floating_panel(ctx, panel, "统计", "stats_panel", |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.colored_label(accent_text(), "0.030");
|
||||
ui.label("81m:51s");
|
||||
});
|
||||
ui.separator();
|
||||
group_frame().show(ui, |ui| {
|
||||
ui.label("FPS / GPU info");
|
||||
ui.label("bounds 589.0us");
|
||||
ui.label("clusters 12.8ms");
|
||||
ui.label("帧率 / GPU 信息");
|
||||
ui.label("边界 589.0us");
|
||||
ui.label("簇 12.8ms");
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -75,8 +502,9 @@ fn draw_floating_panel(
|
||||
if panel.visible {
|
||||
let mut open = true;
|
||||
let mut hide_requested = false;
|
||||
let mut window_rect = None;
|
||||
|
||||
egui::Window::new(title)
|
||||
let window_response = egui::Window::new(title)
|
||||
.id(egui::Id::new(id))
|
||||
.open(&mut open)
|
||||
.default_pos(panel.default_pos)
|
||||
@@ -85,7 +513,7 @@ fn draw_floating_panel(
|
||||
.frame(panel_frame(ctx))
|
||||
.show(ctx, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
if ui.add(tag_button("Hide")).clicked() {
|
||||
if ui.add(tag_button("隐藏")).clicked() {
|
||||
hide_requested = true;
|
||||
}
|
||||
ui.add_space(6.0);
|
||||
@@ -95,6 +523,29 @@ fn draw_floating_panel(
|
||||
add_contents(ui);
|
||||
});
|
||||
|
||||
if let Some(response) = window_response {
|
||||
window_rect = Some(response.response.rect);
|
||||
}
|
||||
|
||||
if hide_requested {
|
||||
if let Some(rect) = window_rect {
|
||||
let screen = ctx.content_rect();
|
||||
let tag_size = egui::vec2(86.0, 22.0);
|
||||
|
||||
let distance_to_left = rect.left();
|
||||
let distance_to_right = screen.right() - rect.right();
|
||||
|
||||
let x = if distance_to_left <= distance_to_right {
|
||||
screen.left()
|
||||
} else {
|
||||
screen.right() - tag_size.x
|
||||
};
|
||||
|
||||
let y = rect.top().clamp(screen.top(), screen.bottom() - tag_size.y);
|
||||
|
||||
panel.tag_pos = egui::pos2(x, y);
|
||||
}
|
||||
}
|
||||
panel.visible = open && !hide_requested;
|
||||
} else {
|
||||
let response = egui::Area::new(egui::Id::new(format!("{id}_tag")))
|
||||
@@ -114,3 +565,24 @@ fn draw_floating_panel(
|
||||
panel.tag_pos = response.response.rect.min;
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_center_floating_panel(
|
||||
ctx: &egui::Context,
|
||||
panel: &mut FloatingPanelState,
|
||||
id: &'static str,
|
||||
top_offset: f32,
|
||||
add_contents: impl FnOnce(&mut egui::Ui),
|
||||
) {
|
||||
if !panel.visible {
|
||||
return;
|
||||
}
|
||||
|
||||
egui::Area::new(egui::Id::new(id))
|
||||
.anchor(egui::Align2::CENTER_TOP, egui::vec2(0.0, top_offset))
|
||||
.order(egui::Order::Foreground)
|
||||
.show(ctx, |ui| {
|
||||
panel_frame(ctx).show(ui, |ui| {
|
||||
add_contents(ui);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
11
src/utils.rs
Normal file
11
src/utils.rs
Normal file
@@ -0,0 +1,11 @@
|
||||
use anyhow;
|
||||
use serialport::available_ports;
|
||||
|
||||
pub fn serial_enum() -> anyhow::Result<Vec<String>> {
|
||||
let ports = available_ports()
|
||||
.map_err(|e| anyhow::anyhow!("available_ports failed: {}", e))?
|
||||
.into_iter()
|
||||
.map(|info| info.port_name)
|
||||
.collect();
|
||||
Ok(ports)
|
||||
}
|
||||
BIN
static/Hack-Bold.ttf
Normal file
BIN
static/Hack-Bold.ttf
Normal file
Binary file not shown.
BIN
static/cpu.png
Normal file
BIN
static/cpu.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.5 KiB |
Reference in New Issue
Block a user