Add Lucide icon font for the mobile UI

Bundle Lucide (ISC license) at assets/fonts/lucide.ttf and register it as the
egui font family "lucide" via mobile/icons.rs::install (called in EditorApp::new).
Icons are drawn as text with icons::font(size) + PUA codepoint constants.

Switch the stack headers (grip, maximize/minimize, node⇄instrument), the footer
chevron, and the transport play/pause to Lucide glyphs instead of relying on
egui's limited built-in emoji-icon-font.
This commit is contained in:
Skyler Lehmkuhl 2026-06-30 00:51:29 -04:00
parent 597697d0a6
commit 76e7963f03
7 changed files with 125 additions and 18 deletions

View File

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

View File

@ -1207,6 +1207,9 @@ impl EditorApp {
) -> Self { ) -> Self {
let current_layout = layouts[0].layout.clone(); let current_layout = layouts[0].layout.clone();
// Register the bundled Lucide icon font (used by the mobile UI; harmless on desktop).
mobile::icons::install(&cc.egui_ctx);
// Disable egui's "Unaligned" debug overlay (on by default in debug builds) // Disable egui's "Unaligned" debug overlay (on by default in debug builds)
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
cc.egui_ctx.style_mut(|style| style.debug.show_unaligned = false); cc.egui_ctx.style_mut(|style| style.debug.show_unaligned = false);

View File

@ -0,0 +1,47 @@
//! Lucide icon font (ISC license — see `assets/fonts/LICENSE-lucide.txt`).
#![allow(dead_code)] // a forward-looking icon palette; not all are wired up yet
//!
//! The font is embedded and registered as a named egui font family ("lucide"). Icons are drawn as
//! text using [`font`] for the `FontId`, with the codepoint constants below (Lucide's private-use
//! area). Run [`install`] once at startup.
use std::sync::Arc;
use eframe::egui;
/// The egui font-family name the Lucide glyphs live under.
pub const FAMILY: &str = "lucide";
/// Embed + register the Lucide icon font. Call once with the egui context at startup.
pub fn install(ctx: &egui::Context) {
let mut fonts = egui::FontDefinitions::default();
fonts.font_data.insert(
"lucide".to_owned(),
Arc::new(egui::FontData::from_static(include_bytes!(
"../../assets/fonts/lucide.ttf"
))),
);
fonts
.families
.insert(egui::FontFamily::Name(FAMILY.into()), vec!["lucide".to_owned()]);
ctx.set_fonts(fonts);
}
/// A `FontId` in the Lucide family at `size`.
pub fn font(size: f32) -> egui::FontId {
egui::FontId::new(size, egui::FontFamily::Name(FAMILY.into()))
}
// --- Codepoints (Lucide PUA). Add more as the mobile UI needs them. ---
pub const MAXIMIZE: &str = "\u{e112}";
pub const MINIMIZE: &str = "\u{e11a}";
pub const ARROW_LEFT_RIGHT: &str = "\u{e24a}";
pub const GRIP_HORIZONTAL: &str = "\u{e0ea}";
pub const CHEVRONS_UP: &str = "\u{e074}";
pub const PLAY: &str = "\u{e13c}";
pub const PAUSE: &str = "\u{e12e}";
pub const SETTINGS: &str = "\u{e154}";
pub const SEARCH: &str = "\u{e151}";
pub const PLUS: &str = "\u{e13d}";
pub const X: &str = "\u{e1b2}";
pub const MENU: &str = "\u{e115}";

View File

@ -17,6 +17,7 @@ use lightningbeam_core::pane::PaneType;
use crate::panes::NodePath; use crate::panes::NodePath;
use crate::RenderContext; use crate::RenderContext;
pub mod icons;
mod stack; mod stack;
mod surface; mod surface;
mod transport; mod transport;

View File

@ -18,7 +18,7 @@
use eframe::egui; use eframe::egui;
use super::{slot_path, MobileState, StackDrag, StackPane, STACK}; use super::{icons, slot_path, MobileState, StackDrag, StackPane, STACK};
use crate::RenderContext; use crate::RenderContext;
const N: usize = STACK.len(); const N: usize = STACK.len();
@ -242,33 +242,35 @@ fn draw_header(ui: &egui::Ui, hr: egui::Rect, sp: StackPane, show_instruments: b
p.rect_filled(hr, 0.0, C_HEADER); p.rect_filled(hr, 0.0, C_HEADER);
p.hline(hr.x_range(), hr.bottom(), egui::Stroke::new(1.0, C_LINE)); p.hline(hr.x_range(), hr.bottom(), egui::Stroke::new(1.0, C_LINE));
let cy = hr.center().y; let cy = hr.center().y;
// Grip dots on the left. // Grip glyph on the left (Lucide).
for i in 0..2 {
let x = hr.left() + 11.0 + i as f32 * 4.0;
p.circle_filled(egui::pos2(x, cy), 1.5, C_DIM);
}
p.text( p.text(
egui::pos2(hr.left() + 26.0, cy), egui::pos2(hr.left() + 15.0, cy),
egui::Align2::CENTER_CENTER,
icons::GRIP_HORIZONTAL,
icons::font(16.0),
C_DIM,
);
p.text(
egui::pos2(hr.left() + 32.0, cy),
egui::Align2::LEFT_CENTER, egui::Align2::LEFT_CENTER,
sp.label(show_instruments), sp.label(show_instruments),
egui::FontId::proportional(15.0), egui::FontId::proportional(15.0),
C_BRIGHT, C_BRIGHT,
); );
// Right-side buttons: fullscreen / restore rightmost, Node/Instrument toggle just left of it. // Right-side buttons: fullscreen / restore rightmost, Node/Instrument toggle just left of it.
// These glyphs are covered by egui's bundled emoji-icon-font (⛶ ▣ ⇄).
p.text( p.text(
egui::pos2(hr.right() - BTN_W * 0.5, cy), egui::pos2(hr.right() - BTN_W * 0.5, cy),
egui::Align2::CENTER_CENTER, egui::Align2::CENTER_CENTER,
if fullscreen { "" } else { "" }, if fullscreen { icons::MINIMIZE } else { icons::MAXIMIZE },
egui::FontId::proportional(18.0), icons::font(17.0),
C_DIM, C_DIM,
); );
if sp == StackPane::NodeInstrument { if sp == StackPane::NodeInstrument {
p.text( p.text(
egui::pos2(hr.right() - BTN_W * 1.5, cy), egui::pos2(hr.right() - BTN_W * 1.5, cy),
egui::Align2::CENTER_CENTER, egui::Align2::CENTER_CENTER,
"", icons::ARROW_LEFT_RIGHT,
egui::FontId::proportional(17.0), icons::font(17.0),
C_AMBER, C_AMBER,
); );
} }
@ -279,13 +281,23 @@ fn draw_footer(ui: &egui::Ui, fr: egui::Rect, at_end: bool) {
p.rect_filled(fr, 0.0, C_HEADER); p.rect_filled(fr, 0.0, C_HEADER);
p.hline(fr.x_range(), fr.top(), egui::Stroke::new(1.0, C_LINE)); p.hline(fr.x_range(), fr.top(), egui::Stroke::new(1.0, C_LINE));
let col = if at_end { C_LINE } else { C_DIM }; let col = if at_end { C_LINE } else { C_DIM };
p.text( let cy = fr.center().y;
fr.center(), let galley = p.layout_no_wrap(
egui::Align2::CENTER_CENTER, "pull up for more".to_string(),
"▲ pull up for more",
egui::FontId::proportional(11.0), egui::FontId::proportional(11.0),
col, col,
); );
let total_w = 22.0 + galley.size().x;
let start_x = fr.center().x - total_w * 0.5;
p.text(
egui::pos2(start_x + 8.0, cy),
egui::Align2::CENTER_CENTER,
icons::CHEVRONS_UP,
icons::font(14.0),
col,
);
let gy = cy - galley.size().y * 0.5;
p.galley(egui::pos2(start_x + 22.0, gy), galley, col);
} }
fn handle_key(h: Handle) -> (usize, usize) { fn handle_key(h: Handle) -> (usize, usize) {

View File

@ -3,6 +3,7 @@
use eframe::egui; use eframe::egui;
use super::icons;
use crate::panes::SharedPaneState; use crate::panes::SharedPaneState;
const C_PANEL: egui::Color32 = egui::Color32::from_rgb(0x1f, 0x24, 0x2c); const C_PANEL: egui::Color32 = egui::Color32::from_rgb(0x1f, 0x24, 0x2c);
@ -24,12 +25,12 @@ pub fn render(ui: &mut egui::Ui, rect: egui::Rect, shared: &mut SharedPaneState)
let btn_rect = egui::Rect::from_center_size(btn_center, egui::vec2(btn_r * 2.0, btn_r * 2.0)); let btn_rect = egui::Rect::from_center_size(btn_center, egui::vec2(btn_r * 2.0, btn_r * 2.0));
let btn_resp = ui.interact(btn_rect, ui.id().with("mobile_transport_play"), egui::Sense::click()); let btn_resp = ui.interact(btn_rect, ui.id().with("mobile_transport_play"), egui::Sense::click());
painter.circle_filled(btn_center, btn_r, C_AMBER); painter.circle_filled(btn_center, btn_r, C_AMBER);
let glyph = if *shared.is_playing { "" } else { "" }; let glyph = if *shared.is_playing { icons::PAUSE } else { icons::PLAY };
painter.text( painter.text(
btn_center, btn_center,
egui::Align2::CENTER_CENTER, egui::Align2::CENTER_CENTER,
glyph, glyph,
egui::FontId::proportional(16.0), icons::font(16.0),
C_DARK, C_DARK,
); );
if btn_resp.clicked() { if btn_resp.clicked() {