//! Shared hardware look for every plugin editor: palette, fonts, knobs, panels. use egui::{self, Color32, FontId, Pos2, Rect, RichText, Sense, Stroke, Vec2}; // Dark anodized hardware: graphite surfaces, one signal colour, steel data traces. pub const BG: Color32 = Color32::from_rgb(14, 15, 17); pub const PANEL: Color32 = Color32::from_rgb(25, 27, 31); pub const PANEL_TOP: Color32 = Color32::from_rgb(34, 36, 41); pub const EDGE: Color32 = Color32::from_rgb(44, 47, 53); pub const SCREEN: Color32 = Color32::from_rgb(9, 10, 11); pub const TEXT: Color32 = Color32::from_rgb(232, 233, 236); pub const MUTED: Color32 = Color32::from_rgb(140, 146, 156); pub const DIM: Color32 = Color32::from_rgb(70, 74, 82); /// Signal colour: cuts, active states, value arcs. pub const ACCENT: Color32 = Color32::from_rgb(255, 1, 96); /// Measured input and neutral data. pub const STEEL: Color32 = Color32::from_rgb(184, 192, 204); /// Focus / protect curve, distinct from cuts without competing with them. pub const FOCUS: Color32 = Color32::from_rgb(126, 167, 216); pub fn style(ctx: &egui::Context) { let mut fonts = egui::FontDefinitions::default(); fonts.font_data.insert( "regular".into(), egui::FontData::from_static(include_bytes!("../assets/RobotoCondensed-Regular.ttf")).into(), ); fonts.font_data.insert( "medium".into(), egui::FontData::from_static(include_bytes!("../assets/RobotoCondensed-Medium.ttf")).into(), ); fonts .families .get_mut(&egui::FontFamily::Proportional) .unwrap() .insert(0, "regular".into()); fonts.families.insert( egui::FontFamily::Name("medium".into()), vec![ "medium".into(), "Ubuntu-Light".into(), "NotoEmoji-Regular".into(), "emoji-icon-font".into(), ], ); ctx.set_fonts(fonts); let mut s = (*ctx.style()).clone(); let v = &mut s.visuals; *v = egui::Visuals::dark(); v.panel_fill = BG; v.window_fill = PANEL; v.extreme_bg_color = SCREEN; v.faint_bg_color = PANEL; v.override_text_color = Some(TEXT); v.selection.bg_fill = Color32::from_rgb(110, 6, 46); v.selection.stroke = Stroke::new(1.0_f32, ACCENT); v.hyperlink_color = ACCENT; v.window_stroke = Stroke::new(1.0_f32, EDGE); for (w, fill, stroke) in [ (&mut v.widgets.noninteractive, PANEL, EDGE), (&mut v.widgets.inactive, Color32::from_rgb(35, 38, 43), EDGE), ( &mut v.widgets.hovered, Color32::from_rgb(45, 48, 54), Color32::from_rgb(78, 82, 90), ), (&mut v.widgets.active, Color32::from_rgb(58, 18, 34), ACCENT), (&mut v.widgets.open, Color32::from_rgb(35, 38, 43), EDGE), ] { w.bg_fill = fill; w.weak_bg_fill = fill; w.bg_stroke = Stroke::new(1.0_f32, stroke); w.corner_radius = 3.into(); } v.widgets.noninteractive.fg_stroke = Stroke::new(1.0_f32, TEXT); v.widgets.inactive.fg_stroke = Stroke::new(1.0_f32, TEXT); v.slider_trailing_fill = true; s.spacing.item_spacing = Vec2::new(10., 8.); s.spacing.button_padding = Vec2::new(12., 6.); s.text_styles .insert(egui::TextStyle::Body, FontId::proportional(16.)); s.text_styles .insert(egui::TextStyle::Button, FontId::proportional(16.)); ctx.set_style(s); } /// Disc with a vertical light-to-dark gradient, like brushed metal lit from above. pub fn metal_disc(p: &egui::Painter, center: Pos2, radius: f32, top: Color32, bottom: Color32) { let mut mesh = egui::Mesh::default(); let mix = |t: f32| { let l = |a: u8, b: u8| (a as f32 + (b as f32 - a as f32) * t).round() as u8; Color32::from_rgb( l(top.r(), bottom.r()), l(top.g(), bottom.g()), l(top.b(), bottom.b()), ) }; mesh.colored_vertex(center, mix(0.5)); let n = 48; for i in 0..=n { let a = std::f32::consts::TAU * i as f32 / n as f32; let point = center + Vec2::angled(a) * radius; mesh.colored_vertex(point, mix((point.y - (center.y - radius)) / (2. * radius))); if i > 0 { mesh.add_triangle(0, i, i + 1); } } p.add(egui::Shape::mesh(mesh)); } #[allow(clippy::too_many_arguments)] pub fn rotary( ui: &mut egui::Ui, name: &str, value: &mut f32, range: std::ops::RangeInclusive, default: f32, unit: &str, size: f32, color: Color32, ) -> egui::Response { let low = *range.start(); let high = *range.end(); let (rect, response) = ui.allocate_exact_size(Vec2::splat(size), Sense::click_and_drag()); if response.drag_started() { response.request_focus(); } if response.dragged() { let delta = ui.input(|i| i.pointer.delta().y); let fine = if ui.input(|i| i.modifiers.shift) { 0.1 } else { 1. }; *value = (*value - delta * (high - low) / 180. * fine).clamp(low, high); } if response.double_clicked() { *value = default; } if response.has_focus() { let step = if ui.input(|i| i.modifiers.shift) { 0.1 } else { 1. }; if ui.input(|i| i.key_pressed(egui::Key::ArrowUp) || i.key_pressed(egui::Key::ArrowRight)) { *value = (*value + step).min(high); } if ui.input(|i| i.key_pressed(egui::Key::ArrowDown) || i.key_pressed(egui::Key::ArrowLeft)) { *value = (*value - step).max(low); } } let center = rect.center(); let radius = size * 0.46; let p = ui.painter(); let start = std::f32::consts::PI * 0.75; let span = std::f32::consts::PI * 1.5; let normalized = ((*value - low) / (high - low)).clamp(0., 1.); // Scale track and value arc around the knob. for (fraction, stroke) in [ (1., Stroke::new(2.5_f32, DIM)), (normalized, Stroke::new(2.5_f32, color)), ] { // A zero-length polyline tessellates to NaN vertices. if fraction < 1e-3 { continue; } let points: Vec<_> = (0..=64) .map(|i| { let a = start + span * fraction * i as f32 / 64.; center + Vec2::angled(a) * (radius - 1.5) }) .collect(); p.add(egui::Shape::line(points, stroke)); } // Machined body: drop shadow, brushed gradient skirt, flat cap. let body = radius - 7.; p.circle_filled( center + Vec2::new(0., 2.), body + 1., Color32::from_black_alpha(140), ); metal_disc( p, center, body, Color32::from_rgb(78, 82, 90), Color32::from_rgb(28, 30, 34), ); p.circle_stroke( center, body, Stroke::new(1.0_f32, Color32::from_rgb(10, 11, 12)), ); let cap = body * 0.74; metal_disc( p, center, cap, Color32::from_rgb(44, 47, 52), Color32::from_rgb(33, 35, 40), ); p.circle_stroke( center, cap, Stroke::new(1.0_f32, Color32::from_rgba_unmultiplied(255, 255, 255, 22)), ); let angle = start + span * normalized; p.line_segment( [ center + Vec2::angled(angle) * (cap * 0.35), center + Vec2::angled(angle) * (cap - 2.), ], Stroke::new(2.0_f32, TEXT), ); if response.has_focus() { p.circle_stroke(center, radius + 2., Stroke::new(1.0_f32, color)); } ui.label( RichText::new(name.to_uppercase()) .size(if size > 100. { 14. } else { 12. }) .color(MUTED), ); ui.add( egui::DragValue::new(value) .range(range) .speed(0.1) .suffix(unit), ); response } /// Vertical gradient rectangle, used for panel faces. pub fn gradient(rect: Rect, top: Color32, bottom: Color32) -> egui::Shape { let mut mesh = egui::Mesh::default(); for (pos, color) in [ (rect.left_top(), top), (rect.right_top(), top), (rect.right_bottom(), bottom), (rect.left_bottom(), bottom), ] { mesh.colored_vertex(pos, color); } mesh.add_triangle(0, 1, 2); mesh.add_triangle(0, 2, 3); egui::Shape::mesh(mesh) } /// Raised hardware panel: graphite face, hairline edge, faint top highlight. pub fn panel(ui: &mut egui::Ui, add: impl FnOnce(&mut egui::Ui) -> R) -> R { let face = ui.painter().add(egui::Shape::Noop); let inner = egui::Frame::new() .inner_margin(14.) .stroke(Stroke::new(1.0_f32, EDGE)) .show(ui, add); let r = inner.response.rect; ui.painter().set(face, gradient(r, PANEL_TOP, PANEL)); ui.painter().line_segment( [ r.left_top() + Vec2::new(1., 1.), r.right_top() + Vec2::new(-1., 1.), ], Stroke::new(1.0_f32, Color32::from_rgba_unmultiplied(255, 255, 255, 16)), ); inner.inner } pub fn caption(ui: &mut egui::Ui, text: &str) { ui.label(RichText::new(text).size(11.).color(MUTED)); } /// Small labelled hardware-style readout. pub fn readout(ui: &mut egui::Ui, label: &str, value: String, color: Color32) { ui.label(RichText::new(label).size(11.).color(MUTED)); ui.label(RichText::new(value).monospace().size(13.).color(color)); ui.add_space(10.); } /// Product header: accent mark, product name, subtitle, then right-aligned controls. pub fn header(ui: &mut egui::Ui, name: &str, subtitle: &str, right: impl FnOnce(&mut egui::Ui)) { ui.horizontal(|ui| { let (mark, _) = ui.allocate_exact_size(Vec2::new(4., 26.), Sense::hover()); ui.painter().rect_filled(mark, 1., ACCENT); ui.label( RichText::new(name) .font(FontId::new(26., egui::FontFamily::Name("medium".into()))) .color(TEXT), ); ui.label(RichText::new(subtitle).size(12.).color(MUTED)); ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), right); }); } /// Full-window backdrop gradient used behind every editor. pub fn backdrop(ui: &egui::Ui) { ui.painter() .add(gradient(ui.max_rect(), Color32::from_rgb(20, 21, 24), BG)); } /// Horizontal level bar in dBFS (−60..0) with a hairline frame. pub fn meter(ui: &mut egui::Ui, label: &str, db: f32, color: Color32, width: f32) { ui.horizontal(|ui| { ui.add_sized( Vec2::new(70., 16.), egui::Label::new(RichText::new(label).size(11.).color(MUTED)), ); let (rect, _) = ui.allocate_exact_size(Vec2::new(width, 10.), Sense::hover()); let p = ui.painter(); p.rect_filled(rect, 1., SCREEN); let t = ((db + 60.) / 60.).clamp(0., 1.); let mut fill = rect; fill.set_width(rect.width() * t); p.rect_filled(fill, 1., color); p.rect_stroke( rect, 1., Stroke::new(1.0_f32, EDGE), egui::StrokeKind::Inside, ); ui.label( RichText::new(if db <= -60. { "−∞".to_string() } else { format!("{db:.1} dB") }) .monospace() .size(12.) .color(TEXT), ); }); }