diff --git a/Engine/Cargo.lock b/Engine/Cargo.lock index 42990916..3694d707 100644 --- a/Engine/Cargo.lock +++ b/Engine/Cargo.lock @@ -1152,6 +1152,7 @@ name = "engine-desktop" version = "0.1.0" dependencies = [ "eframe", + "egui_extras", "engine-platform", ] diff --git a/Engine/THIRD_PARTY.md b/Engine/THIRD_PARTY.md index e4c1d9f1..6ccf23c4 100644 --- a/Engine/THIRD_PARTY.md +++ b/Engine/THIRD_PARTY.md @@ -24,7 +24,8 @@ license policy on the complete transitive graph. | `ogg`, `opus-pure` | Ogg codec detection and bundled pure Rust mono/stereo Opus decoding with encoder-delay and end-padding trimming | BSD-3-Clause | Validated container packets, bounded decoded PCM, corpus tests, and the isolated worker contain decoder failures | | `interprocess` | Local authenticated editor/runtime IPC transport | MIT OR Apache-2.0 | Versioned framing, one-use secrets, payload limits, deadlines, and process supervision wrap it | | `egui_tiles` | Dockable native editor layout | MIT OR Apache-2.0 | Only editor UI state depends on it; authoring state remains in testable engine models | -| Material Design Icons (Pictogrammers) | Embedded upstream SVG file-type, language, and transform-tool icons | Apache-2.0 | Icons are compiled into the editor, so rendering never depends on network availability | +| Material Design Icons (Pictogrammers) | Embedded upstream SVG script-language icons | Apache-2.0 | Icons are compiled into the editor, so rendering never depends on network availability | +| Lucide Icons | Embedded SVG interface, file-type, transform, playback, and window-control icons | ISC AND MIT | Icons are compiled into the desktop applications and tinted locally; no runtime network access | Development-only `tempfile` is used for isolated persistence and corruption fixtures and is not an engine runtime API. diff --git a/Engine/apps/editor/src/main.rs b/Engine/apps/editor/src/main.rs index c0c34348..27017d54 100644 --- a/Engine/apps/editor/src/main.rs +++ b/Engine/apps/editor/src/main.rs @@ -14,6 +14,7 @@ use engine_core::logging::{AsyncLogger, Language, LogConfig, LogStream, Severity use engine_core::{ ApplicationIdentity, ApplicationRole, AssetId, run_headless_smoke_from_arguments, }; +use engine_desktop::icons::Icon; use engine_editor::{ AuthoringDocument, ConsoleEntry, EditorCamera, EditorTab, EditorWorkspace, GizmoAxis, GizmoOperation, GizmoSettings, GizmoSpace, PickMesh, ProjectAccess, ProjectLock, @@ -1107,7 +1108,8 @@ impl EditorApp { .color(ui_theme::TEXT), ); if self.document.is_modified() { - ui.label(egui::RichText::new("● Unsaved").color(ui_theme::WARNING)); + ui.add(Icon::Circle.image(ui_theme::WARNING, 12.0)); + ui.colored_label(ui_theme::WARNING, "Unsaved"); } if self.document.is_read_only() { ui.colored_label(ui_theme::WARNING, "READ ONLY"); @@ -1115,7 +1117,11 @@ impl EditorApp { ui.separator(); if self.play.is_none() { if ui - .add(egui::Button::new("▶ Play").fill(egui::Color32::from_rgb(45, 100, 70))) + .add( + Icon::Play + .button("Play") + .fill(egui::Color32::from_rgb(45, 100, 70)), + ) .clicked() { self.start_play(self.active_play_mode); @@ -1133,7 +1139,11 @@ impl EditorApp { }); } else { if ui - .add(egui::Button::new("■ Stop").fill(egui::Color32::from_rgb(115, 53, 53))) + .add( + Icon::Square + .button("Stop") + .fill(egui::Color32::from_rgb(115, 53, 53)), + ) .clicked() { self.stop_play(); @@ -1143,7 +1153,11 @@ impl EditorApp { .as_ref() .is_some_and(|play| play.state() == RuntimeState::Paused); if ui - .button(if paused { "▶ Resume" } else { "Ⅱ Pause" }) + .add(if paused { + Icon::Play.button("Resume") + } else { + Icon::Pause.button("Pause") + }) .clicked() { self.control_play(if paused { @@ -1153,7 +1167,7 @@ impl EditorApp { }); } if ui - .add_enabled(paused, egui::Button::new("▹ Frame")) + .add_enabled(paused, Icon::StepForward.button("Frame")) .clicked() { self.control_play(ControlRequest::FrameAdvance); @@ -1313,15 +1327,9 @@ fn transform_mode_button( ui.painter().rect_filled(rect, 5.0, visuals.weak_bg_fill); let icon_source = match operation { - GizmoOperation::Translate => { - egui::include_image!("../../../assets/icons/material-design/axis-arrow.svg") - } - GizmoOperation::Rotate => { - egui::include_image!("../../../assets/icons/material-design/rotate-orbit.svg") - } - GizmoOperation::Scale => { - egui::include_image!("../../../assets/icons/material-design/resize.svg") - } + GizmoOperation::Translate => Icon::Move3d.source(), + GizmoOperation::Rotate => Icon::Rotate3d.source(), + GizmoOperation::Scale => Icon::Scaling.source(), }; let icon_rect = egui::Rect::from_center_size( egui::pos2(rect.center().x, rect.top() + 15.0), @@ -4153,7 +4161,7 @@ fn render_scene_hierarchy( ) .show_header(ui, |ui| { if snapshot.folder { - ui.label("📁"); + ui.add(Icon::Folder.image(ui.visuals().text_color(), 18.0)); } scene_entry_row( ui, @@ -4367,11 +4375,9 @@ fn render_project_directory( ) .show_header(ui, |ui| { ui.add( - egui::Image::new(egui::include_image!( - "../../../assets/icons/material-design/folder.svg" - )) - .fit_to_exact_size(egui::vec2(18.0, 18.0)) - .tint(text_color), + egui::Image::new(Icon::Folder.source()) + .fit_to_exact_size(egui::vec2(18.0, 18.0)) + .tint(text_color), ); action = project_entry_row( ui, @@ -5149,24 +5155,14 @@ fn project_file_icon(path: &Path) -> egui::ImageSource<'static> { Some("css") => { egui::include_image!("../../../assets/icons/material-design/language-css3.svg") } - Some("rscene") => { - egui::include_image!("../../../assets/icons/material-design/map-outline.svg") - } - Some("png" | "jpg" | "jpeg" | "tga" | "hdr") => { - egui::include_image!("../../../assets/icons/material-design/image-outline.svg") - } - Some("glb" | "gltf" | "obj") => { - egui::include_image!("../../../assets/icons/material-design/cube-outline.svg") - } + Some("rscene") => Icon::Map.source(), + Some("png" | "jpg" | "jpeg" | "tga" | "hdr") => Icon::Image.source(), + Some("glb" | "gltf" | "obj") => Icon::Box.source(), Some("wav" | "ogg" | "oga" | "opus" | "mp3" | "aac" | "m4a" | "flac") => { - egui::include_image!("../../../assets/icons/material-design/music-note-outline.svg") - } - Some("ron" | "toml" | "json") => { - egui::include_image!("../../../assets/icons/material-design/file-cog-outline.svg") - } - _ => { - egui::include_image!("../../../assets/icons/material-design/file-document-outline.svg") + Icon::FileMusic.source() } + Some("ron" | "toml" | "json") => Icon::FileCog.source(), + _ => Icon::FileText.source(), } } diff --git a/Engine/apps/editor/src/ui_theme.rs b/Engine/apps/editor/src/ui_theme.rs index d1dd382c..69141d91 100644 --- a/Engine/apps/editor/src/ui_theme.rs +++ b/Engine/apps/editor/src/ui_theme.rs @@ -1,4 +1,5 @@ use eframe::egui; +use engine_desktop::icons::Icon; pub const ACCENT: egui::Color32 = egui::Color32::from_rgb(211, 139, 77); pub const ACCENT_HOVER: egui::Color32 = egui::Color32::from_rgb(230, 158, 91); @@ -103,7 +104,7 @@ pub fn component_card( .show(ui, |ui| { ui.set_width(ui.available_width()); ui.horizontal(|ui| { - ui.label(egui::RichText::new("▼").size(10.0).color(MUTED)); + ui.add(Icon::ChevronDown.image(MUTED, 12.0)); ui.label(egui::RichText::new(title).strong().color(TEXT)); }); ui.add_space(5.0); diff --git a/Engine/apps/project-manager/src/main.rs b/Engine/apps/project-manager/src/main.rs index abaee72d..acdec233 100644 --- a/Engine/apps/project-manager/src/main.rs +++ b/Engine/apps/project-manager/src/main.rs @@ -9,6 +9,7 @@ use engine_core::logging::{AsyncLogger, LogConfig, LogStream, Severity}; use engine_core::{ ApplicationIdentity, ApplicationRole, EngineVersion, run_headless_smoke_from_arguments, }; +use engine_desktop::icons::Icon; use engine_platform::{ ConfigLoad, ConfigStore, NativePlatformPaths, NativeProcessLauncher, PlatformPaths, ProcessLauncher, ProcessRequest, StartupMarker, @@ -56,7 +57,10 @@ fn run() -> Result<(), String> { eframe::run_native( "org.rusticengine.project-manager", options, - Box::new(move |_context| Ok(Box::new(app))), + Box::new(move |context| { + engine_desktop::icons::install_loaders(&context.egui_ctx); + Ok(Box::new(app)) + }), ) .map_err(|error| format!("native launcher failed: {error}")) } @@ -354,7 +358,12 @@ impl eframe::App for ProjectManagerApp { ); ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { if ui - .add(ui_theme::primary_button("+ New project")) + .add( + Icon::Plus + .button("New project") + .fill(ui_theme::ACCENT) + .stroke(egui::Stroke::NONE), + ) .clicked() && self.model.catalog_writable() { diff --git a/Engine/assets/icons/lucide/LICENSE b/Engine/assets/icons/lucide/LICENSE new file mode 100644 index 00000000..718bb3f0 --- /dev/null +++ b/Engine/assets/icons/lucide/LICENSE @@ -0,0 +1,43 @@ +ISC License + +Copyright (c) 2026 Lucide Icons and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +--- + +The following Lucide icons are derived from the Feather project: + +airplay, alert-circle, alert-octagon, alert-triangle, aperture, arrow-down-circle, arrow-down-left, arrow-down-right, arrow-down, arrow-left-circle, arrow-left, arrow-right-circle, arrow-right, arrow-up-circle, arrow-up-left, arrow-up-right, arrow-up, at-sign, calendar, cast, check, chevron-down, chevron-left, chevron-right, chevron-up, chevrons-down, chevrons-left, chevrons-right, chevrons-up, circle, clipboard, clock, code, columns, command, compass, corner-down-left, corner-down-right, corner-left-down, corner-left-up, corner-right-down, corner-right-up, corner-up-left, corner-up-right, crosshair, database, divide-circle, divide-square, dollar-sign, download, external-link, feather, frown, hash, headphones, help-circle, info, italic, key, layout, life-buoy, link-2, link, loader, lock, log-in, log-out, maximize, meh, minimize, minimize-2, minus-circle, minus-square, minus, monitor, moon, more-horizontal, more-vertical, move, music, navigation-2, navigation, octagon, pause-circle, percent, plus-circle, plus-square, plus, power, radio, rss, search, server, share, shopping-bag, sidebar, smartphone, smile, square, table-2, tablet, target, terminal, trash-2, trash, triangle, tv, type, upload, x-circle, x-octagon, x-square, x, zoom-in, zoom-out + +The MIT License (MIT) (for the icons listed above) + +Copyright (c) 2013-present Cole Bemis + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Engine/assets/icons/lucide/SOURCE.md b/Engine/assets/icons/lucide/SOURCE.md new file mode 100644 index 00000000..17579261 --- /dev/null +++ b/Engine/assets/icons/lucide/SOURCE.md @@ -0,0 +1,11 @@ +# Lucide icon source + +These SVGs come from [Lucide](https://lucide.dev/), pinned to upstream +commit [`c29c276f16590cf017126671535ad8c792be64d8`](https://github.com/lucide-icons/lucide/tree/c29c276f16590cf017126671535ad8c792be64d8/icons). +The included `LICENSE` is the upstream ISC license with its inherited Feather MIT notice. + +Only `stroke="currentColor"` is changed to `stroke="#ffffff"` so egui's SVG +rasterizer and image tint can apply the active theme's foreground color. +The 24 × 24 view box, 2-pixel strokes, and round caps/joins remain upstream defaults. +Icons are compiled into the desktop applications; no runtime download or icon font +is needed. Existing script and language icons remain Material Design Icons. diff --git a/Engine/assets/icons/lucide/box.svg b/Engine/assets/icons/lucide/box.svg new file mode 100644 index 00000000..59e58537 --- /dev/null +++ b/Engine/assets/icons/lucide/box.svg @@ -0,0 +1,15 @@ + + + + + diff --git a/Engine/assets/icons/lucide/chevron-down.svg b/Engine/assets/icons/lucide/chevron-down.svg new file mode 100644 index 00000000..e9977744 --- /dev/null +++ b/Engine/assets/icons/lucide/chevron-down.svg @@ -0,0 +1,13 @@ + + + diff --git a/Engine/assets/icons/lucide/circle.svg b/Engine/assets/icons/lucide/circle.svg new file mode 100644 index 00000000..4d2bb3eb --- /dev/null +++ b/Engine/assets/icons/lucide/circle.svg @@ -0,0 +1,13 @@ + + + diff --git a/Engine/assets/icons/lucide/file-cog.svg b/Engine/assets/icons/lucide/file-cog.svg new file mode 100644 index 00000000..e0a1ec8d --- /dev/null +++ b/Engine/assets/icons/lucide/file-cog.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + diff --git a/Engine/assets/icons/lucide/file-music.svg b/Engine/assets/icons/lucide/file-music.svg new file mode 100644 index 00000000..bb3a0e65 --- /dev/null +++ b/Engine/assets/icons/lucide/file-music.svg @@ -0,0 +1,16 @@ + + + + + + diff --git a/Engine/assets/icons/lucide/file-text.svg b/Engine/assets/icons/lucide/file-text.svg new file mode 100644 index 00000000..0a3ae071 --- /dev/null +++ b/Engine/assets/icons/lucide/file-text.svg @@ -0,0 +1,17 @@ + + + + + + + diff --git a/Engine/assets/icons/lucide/folder.svg b/Engine/assets/icons/lucide/folder.svg new file mode 100644 index 00000000..acc04101 --- /dev/null +++ b/Engine/assets/icons/lucide/folder.svg @@ -0,0 +1,13 @@ + + + diff --git a/Engine/assets/icons/lucide/image.svg b/Engine/assets/icons/lucide/image.svg new file mode 100644 index 00000000..24d6c4e4 --- /dev/null +++ b/Engine/assets/icons/lucide/image.svg @@ -0,0 +1,15 @@ + + + + + diff --git a/Engine/assets/icons/lucide/map.svg b/Engine/assets/icons/lucide/map.svg new file mode 100644 index 00000000..6272b024 --- /dev/null +++ b/Engine/assets/icons/lucide/map.svg @@ -0,0 +1,15 @@ + + + + + diff --git a/Engine/assets/icons/lucide/maximize.svg b/Engine/assets/icons/lucide/maximize.svg new file mode 100644 index 00000000..f8b3caa5 --- /dev/null +++ b/Engine/assets/icons/lucide/maximize.svg @@ -0,0 +1,16 @@ + + + + + + diff --git a/Engine/assets/icons/lucide/minus.svg b/Engine/assets/icons/lucide/minus.svg new file mode 100644 index 00000000..92f6204c --- /dev/null +++ b/Engine/assets/icons/lucide/minus.svg @@ -0,0 +1,13 @@ + + + diff --git a/Engine/assets/icons/lucide/move-3d.svg b/Engine/assets/icons/lucide/move-3d.svg new file mode 100644 index 00000000..4034b7c6 --- /dev/null +++ b/Engine/assets/icons/lucide/move-3d.svg @@ -0,0 +1,16 @@ + + + + + + diff --git a/Engine/assets/icons/lucide/pause.svg b/Engine/assets/icons/lucide/pause.svg new file mode 100644 index 00000000..a0d9e6cd --- /dev/null +++ b/Engine/assets/icons/lucide/pause.svg @@ -0,0 +1,14 @@ + + + + diff --git a/Engine/assets/icons/lucide/play.svg b/Engine/assets/icons/lucide/play.svg new file mode 100644 index 00000000..e4b94308 --- /dev/null +++ b/Engine/assets/icons/lucide/play.svg @@ -0,0 +1,13 @@ + + + diff --git a/Engine/assets/icons/lucide/plus.svg b/Engine/assets/icons/lucide/plus.svg new file mode 100644 index 00000000..53e25e4a --- /dev/null +++ b/Engine/assets/icons/lucide/plus.svg @@ -0,0 +1,14 @@ + + + + diff --git a/Engine/assets/icons/lucide/rotate-3d.svg b/Engine/assets/icons/lucide/rotate-3d.svg new file mode 100644 index 00000000..4e48a4b6 --- /dev/null +++ b/Engine/assets/icons/lucide/rotate-3d.svg @@ -0,0 +1,15 @@ + + + + + diff --git a/Engine/assets/icons/lucide/scaling.svg b/Engine/assets/icons/lucide/scaling.svg new file mode 100644 index 00000000..ae40e794 --- /dev/null +++ b/Engine/assets/icons/lucide/scaling.svg @@ -0,0 +1,16 @@ + + + + + + diff --git a/Engine/assets/icons/lucide/square.svg b/Engine/assets/icons/lucide/square.svg new file mode 100644 index 00000000..9f219b0f --- /dev/null +++ b/Engine/assets/icons/lucide/square.svg @@ -0,0 +1,13 @@ + + + diff --git a/Engine/assets/icons/lucide/step-forward.svg b/Engine/assets/icons/lucide/step-forward.svg new file mode 100644 index 00000000..efd6d9dd --- /dev/null +++ b/Engine/assets/icons/lucide/step-forward.svg @@ -0,0 +1,14 @@ + + + + diff --git a/Engine/assets/icons/lucide/x.svg b/Engine/assets/icons/lucide/x.svg new file mode 100644 index 00000000..c8422eb6 --- /dev/null +++ b/Engine/assets/icons/lucide/x.svg @@ -0,0 +1,14 @@ + + + + diff --git a/Engine/assets/icons/material-design/SOURCE.md b/Engine/assets/icons/material-design/SOURCE.md index 31305e37..c5bc9842 100644 --- a/Engine/assets/icons/material-design/SOURCE.md +++ b/Engine/assets/icons/material-design/SOURCE.md @@ -1,6 +1,6 @@ # Material Design Icons source -These SVG files are downloaded directly from the +The retained script and programming-language SVG icons are downloaded directly from the [Iconify Material Design Icons collection](https://icon-sets.iconify.design/mdi/). Each request uses Iconify's `color=#FFFFFF` option so egui can tint the source glyph to the active light or dark theme foreground color without a black base @@ -8,3 +8,5 @@ fill suppressing the tint. The glyphs come from the Pictogrammers Material Design Icons pack. The included `LICENSE` is that upstream icon-pack license. + +Other interface icons use the bundled [Lucide pack](../lucide/SOURCE.md). diff --git a/Engine/assets/icons/material-design/axis-arrow.svg b/Engine/assets/icons/material-design/axis-arrow.svg deleted file mode 100644 index 3f02dce0..00000000 --- a/Engine/assets/icons/material-design/axis-arrow.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/Engine/assets/icons/material-design/cube-outline.svg b/Engine/assets/icons/material-design/cube-outline.svg deleted file mode 100644 index 4e45aec3..00000000 --- a/Engine/assets/icons/material-design/cube-outline.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/Engine/assets/icons/material-design/file-cog-outline.svg b/Engine/assets/icons/material-design/file-cog-outline.svg deleted file mode 100644 index 8ed5c8a8..00000000 --- a/Engine/assets/icons/material-design/file-cog-outline.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/Engine/assets/icons/material-design/file-document-outline.svg b/Engine/assets/icons/material-design/file-document-outline.svg deleted file mode 100644 index a91fe98c..00000000 --- a/Engine/assets/icons/material-design/file-document-outline.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/Engine/assets/icons/material-design/folder.svg b/Engine/assets/icons/material-design/folder.svg deleted file mode 100644 index 74410d97..00000000 --- a/Engine/assets/icons/material-design/folder.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/Engine/assets/icons/material-design/image-outline.svg b/Engine/assets/icons/material-design/image-outline.svg deleted file mode 100644 index e732bfc5..00000000 --- a/Engine/assets/icons/material-design/image-outline.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/Engine/assets/icons/material-design/map-outline.svg b/Engine/assets/icons/material-design/map-outline.svg deleted file mode 100644 index f1abb7f7..00000000 --- a/Engine/assets/icons/material-design/map-outline.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/Engine/assets/icons/material-design/music-note-outline.svg b/Engine/assets/icons/material-design/music-note-outline.svg deleted file mode 100644 index 1a1dc24f..00000000 --- a/Engine/assets/icons/material-design/music-note-outline.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/Engine/assets/icons/material-design/resize.svg b/Engine/assets/icons/material-design/resize.svg deleted file mode 100644 index 715549aa..00000000 --- a/Engine/assets/icons/material-design/resize.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/Engine/assets/icons/material-design/rotate-orbit.svg b/Engine/assets/icons/material-design/rotate-orbit.svg deleted file mode 100644 index d4570f79..00000000 --- a/Engine/assets/icons/material-design/rotate-orbit.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/Engine/crates/engine-desktop/Cargo.toml b/Engine/crates/engine-desktop/Cargo.toml index f52f2358..1152a93b 100644 --- a/Engine/crates/engine-desktop/Cargo.toml +++ b/Engine/crates/engine-desktop/Cargo.toml @@ -8,6 +8,7 @@ publish = false [dependencies] eframe.workspace = true +egui_extras.workspace = true engine-platform = { path = "../engine-platform", version = "0.1.0" } [lints] diff --git a/Engine/crates/engine-desktop/src/icons.rs b/Engine/crates/engine-desktop/src/icons.rs new file mode 100644 index 00000000..caeb6a4a --- /dev/null +++ b/Engine/crates/engine-desktop/src/icons.rs @@ -0,0 +1,78 @@ +//! Embedded Lucide interface icons shared by the editor and project manager. + +use eframe::egui; + +/// Registers image loaders for the embedded SVG interface assets. +pub fn install_loaders(context: &egui::Context) { + egui_extras::install_image_loaders(context); +} + +/// A bundled interface icon; script-language branding uses its existing assets. +#[derive(Clone, Copy)] +pub enum Icon { + Move3d, + Rotate3d, + Scaling, + Folder, + Map, + Image, + Box, + FileMusic, + FileCog, + FileText, + Play, + Square, + Pause, + StepForward, + Circle, + ChevronDown, + Plus, + X, + Maximize, + Minus, +} + +impl Icon { + /// Returns the embedded SVG, without accessing the network. + pub fn source(self) -> egui::ImageSource<'static> { + match self { + Self::Move3d => egui::include_image!("../../../assets/icons/lucide/move-3d.svg"), + Self::Rotate3d => egui::include_image!("../../../assets/icons/lucide/rotate-3d.svg"), + Self::Scaling => egui::include_image!("../../../assets/icons/lucide/scaling.svg"), + Self::Folder => egui::include_image!("../../../assets/icons/lucide/folder.svg"), + Self::Map => egui::include_image!("../../../assets/icons/lucide/map.svg"), + Self::Image => egui::include_image!("../../../assets/icons/lucide/image.svg"), + Self::Box => egui::include_image!("../../../assets/icons/lucide/box.svg"), + Self::FileMusic => egui::include_image!("../../../assets/icons/lucide/file-music.svg"), + Self::FileCog => egui::include_image!("../../../assets/icons/lucide/file-cog.svg"), + Self::FileText => egui::include_image!("../../../assets/icons/lucide/file-text.svg"), + Self::Play => egui::include_image!("../../../assets/icons/lucide/play.svg"), + Self::Square => egui::include_image!("../../../assets/icons/lucide/square.svg"), + Self::Pause => egui::include_image!("../../../assets/icons/lucide/pause.svg"), + Self::StepForward => { + egui::include_image!("../../../assets/icons/lucide/step-forward.svg") + } + Self::Circle => egui::include_image!("../../../assets/icons/lucide/circle.svg"), + Self::ChevronDown => { + egui::include_image!("../../../assets/icons/lucide/chevron-down.svg") + } + Self::Plus => egui::include_image!("../../../assets/icons/lucide/plus.svg"), + Self::X => egui::include_image!("../../../assets/icons/lucide/x.svg"), + Self::Maximize => egui::include_image!("../../../assets/icons/lucide/maximize.svg"), + Self::Minus => egui::include_image!("../../../assets/icons/lucide/minus.svg"), + } + } + + /// Uses a white SVG stroke so image tint matches the UI foreground. + pub fn image(self, color: egui::Color32, size: f32) -> egui::Image<'static> { + egui::Image::new(self.source()) + .fit_to_exact_size(egui::vec2(size, size)) + .tint(color) + } + + /// Keeps the text label available to accessibility tools. + pub fn button(self, label: &str) -> egui::Button<'static> { + egui::Button::image_and_text(self.image(egui::Color32::WHITE, 16.0), label.to_owned()) + .image_tint_follows_text_color(true) + } +} diff --git a/Engine/crates/engine-desktop/src/lib.rs b/Engine/crates/engine-desktop/src/lib.rs index 1a295515..963a1b37 100644 --- a/Engine/crates/engine-desktop/src/lib.rs +++ b/Engine/crates/engine-desktop/src/lib.rs @@ -1,5 +1,7 @@ //! Shared chrome and update notifications for the native desktop applications. +pub mod icons; + use eframe::egui::{self, Align, Layout, Sense, ViewportCommand}; use engine_platform::{DesktopUpdate, DesktopUpdateClient}; use std::time::{Duration, Instant}; @@ -161,6 +163,7 @@ fn draw_title_bar( if !custom && !update_available { return (false, None); } + icons::install_loaders(ui.ctx()); let context = ui.ctx().clone(); let mut update = false; let mut update_rect = None; @@ -206,13 +209,13 @@ fn draw_title_bar( ui.with_layout(Layout::right_to_left(Align::Center), |ui| { ui.spacing_mut().item_spacing.x = 0.0; if custom { - if caption_button(ui, "×", "Close").clicked() { + if caption_button(ui, icons::Icon::X, "Close").clicked() { context.send_viewport_cmd(ViewportCommand::Close); } - if caption_button(ui, "□", "Maximize or restore").clicked() { + if caption_button(ui, icons::Icon::Maximize, "Maximize or restore").clicked() { toggle_maximized(&context); } - if caption_button(ui, "−", "Minimize").clicked() { + if caption_button(ui, icons::Icon::Minus, "Minimize").clicked() { context.send_viewport_cmd(ViewportCommand::Minimized(true)); } } @@ -231,9 +234,17 @@ fn draw_title_bar( (update, update_rect) } -fn caption_button(ui: &mut egui::Ui, label: &str, tooltip: &str) -> egui::Response { - ui.add_sized([53.0, 24.0], egui::Button::new(label).frame(false)) - .on_hover_text(tooltip) +fn caption_button(ui: &mut egui::Ui, icon: icons::Icon, tooltip: &str) -> egui::Response { + let response = ui.add_sized( + [53.0, 24.0], + egui::Button::image(icon.image(egui::Color32::WHITE, 14.0)) + .image_tint_follows_text_color(true) + .frame(false), + ); + response.widget_info(|| { + egui::WidgetInfo::labeled(egui::WidgetType::Button, ui.is_enabled(), tooltip) + }); + response.on_hover_text(tooltip) } fn toggle_maximized(context: &egui::Context) { diff --git a/Engine/docs/ARCHITECTURE.md b/Engine/docs/ARCHITECTURE.md index 3a131ec5..bdfb87e7 100644 --- a/Engine/docs/ARCHITECTURE.md +++ b/Engine/docs/ARCHITECTURE.md @@ -265,6 +265,12 @@ an editor returns naturally to the still-running launcher when it was launcher- started. Opening a project obtains an advisory project lock; read-only open and explicit stale-lock recovery are supported. +The editor and Project Manager use bundled Lucide icons for interface controls, +folders, and non-script file types. Move, Rotate, and Scale retain their text labels; +Play, Stop, Pause, Resume, and Frame also keep their labels. Existing script and +programming-language icons are unchanged. Icons load from the installed application, +including offline; no icon font or separate download is required. + All play modes use a child `rustic-runtime` so game/runtime faults do not destroy the editor: diff --git a/Engine/installer/RusticGameEngine.iss b/Engine/installer/RusticGameEngine.iss index 302cd24f..3f2bbda9 100644 --- a/Engine/installer/RusticGameEngine.iss +++ b/Engine/installer/RusticGameEngine.iss @@ -75,6 +75,8 @@ Source: "..\licenses\ogg-BSD-3-Clause.txt"; DestDir: "{app}\licenses"; Flags: ig Source: "..\licenses\opus-pure-BSD-3-Clause.txt"; DestDir: "{app}\licenses"; Flags: ignoreversion Source: "..\licenses\opus-pure-ATTRIBUTION.md"; DestDir: "{app}\licenses"; Flags: ignoreversion Source: "..\licenses\Audio-Decoders-NOTICE.md"; DestDir: "{app}\licenses"; Flags: ignoreversion +Source: "..\assets\icons\lucide\LICENSE"; DestDir: "{app}\licenses"; DestName: "Lucide-LICENSE.txt"; Flags: ignoreversion +Source: "..\assets\icons\material-design\LICENSE"; DestDir: "{app}\licenses"; DestName: "Material-Design-Icons-LICENSE.txt"; Flags: ignoreversion #ifndef InstallerPreflight Source: "..\target\distribution\rustic-luau-host.exe"; DestDir: "{app}"; Flags: ignoreversion Source: "..\target\distribution\rustic-agent-backend.exe"; DestDir: "{app}"; Flags: ignoreversion