├── .gitignore ├── rustfmt.toml ├── Cargo.toml ├── egui_extras_xt ├── src │ ├── barcodes │ │ ├── mod.rs │ │ ├── datamatrix_widget.rs │ │ ├── qrcode_widget.rs │ │ └── barcode_widget.rs │ ├── ui │ │ ├── mod.rs │ │ ├── drag_rangeinclusive.rs │ │ ├── optional_value_widget.rs │ │ ├── rotated_label.rs │ │ ├── widgets_from_iter.rs │ │ ├── widgets_from_slice.rs │ │ ├── hyperlink_with_icon.rs │ │ ├── standard_buttons.rs │ │ └── about_window.rs │ ├── knobs │ │ ├── mod.rs │ │ └── audio_knob.rs │ ├── compasses │ │ ├── mod.rs │ │ ├── compass_axis_labels.rs │ │ └── compass_marker.rs │ ├── lib.rs │ ├── displays │ │ ├── mod.rs │ │ ├── segmented_display │ │ │ ├── mod.rs │ │ │ ├── display_metrics.rs │ │ │ └── widget.rs │ │ ├── led_display.rs │ │ ├── indicator_button.rs │ │ └── display_style.rs │ ├── filesystem │ │ ├── mod.rs │ │ ├── directory_cache.rs │ │ ├── path_symbol.rs │ │ └── breadcrumb_bar.rs │ └── hash.rs └── Cargo.toml ├── egui_extras_xt_example ├── src │ └── bin │ │ ├── widget_gallery │ │ ├── pages │ │ │ ├── welcome_page.rs │ │ │ ├── standard_buttons_page.rs │ │ │ ├── rotated_label_page.rs │ │ │ ├── hyperlink_with_icon_page.rs │ │ │ ├── qrcode_page.rs │ │ │ ├── datamatrix_page.rs │ │ │ ├── led_display_page.rs │ │ │ ├── directory_tree_view_page.rs │ │ │ ├── segmented_display_page.rs │ │ │ ├── indicator_button_page.rs │ │ │ ├── barcode_page.rs │ │ │ ├── thumbstick_widget_page.rs │ │ │ ├── audio_knob_page.rs │ │ │ ├── mod.rs │ │ │ ├── angle_knob_page.rs │ │ │ └── linear_compass_page.rs │ │ └── main.rs │ │ ├── about_window.rs │ │ ├── label_rotation_demo.rs │ │ ├── waveform_demo.rs │ │ ├── glyph_editor.rs │ │ ├── filesystem_widgets.rs │ │ ├── compass_widgets.rs │ │ ├── delorean_time_circuits.rs │ │ └── ui_extensions.rs └── Cargo.toml └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | imports_granularity = "Module" 2 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = [ 4 | "egui_extras_xt", 5 | "egui_extras_xt_example", 6 | ] 7 | -------------------------------------------------------------------------------- /egui_extras_xt/src/barcodes/mod.rs: -------------------------------------------------------------------------------- 1 | mod barcode_widget; 2 | mod datamatrix_widget; 3 | mod qrcode_widget; 4 | 5 | pub use barcode_widget::{BarcodeKind, BarcodeWidget}; 6 | pub use datamatrix_widget::DataMatrixWidget; 7 | pub use qrcode_widget::QrCodeWidget; 8 | -------------------------------------------------------------------------------- /egui_extras_xt/src/ui/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod about_window; 2 | pub mod drag_rangeinclusive; 3 | pub mod hyperlink_with_icon; 4 | pub mod optional_value_widget; 5 | pub mod rotated_label; 6 | pub mod standard_buttons; 7 | pub mod widgets_from_iter; 8 | pub mod widgets_from_slice; 9 | -------------------------------------------------------------------------------- /egui_extras_xt/src/knobs/mod.rs: -------------------------------------------------------------------------------- 1 | mod angle_knob; 2 | mod audio_knob; 3 | mod thumbstick_widget; 4 | 5 | pub use angle_knob::{AngleKnob, AngleKnobPreset}; 6 | pub use audio_knob::AudioKnob; 7 | pub use thumbstick_widget::{ThumbstickDeadZone, ThumbstickSnap, ThumbstickWidget}; 8 | -------------------------------------------------------------------------------- /egui_extras_xt/src/compasses/mod.rs: -------------------------------------------------------------------------------- 1 | mod compass_axis_labels; 2 | mod compass_marker; 3 | mod linear_compass; 4 | mod polar_compass; 5 | 6 | pub use compass_axis_labels::CompassAxisLabels; 7 | pub use compass_marker::{CompassMarker, CompassMarkerShape, DefaultCompassMarkerColor}; 8 | pub use linear_compass::LinearCompass; 9 | pub use polar_compass::{PolarCompass, PolarCompassOverflow}; 10 | -------------------------------------------------------------------------------- /egui_extras_xt/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod hash; 2 | 3 | pub mod common; 4 | 5 | #[cfg(feature = "barcodes")] 6 | pub mod barcodes; 7 | 8 | #[cfg(feature = "compasses")] 9 | pub mod compasses; 10 | 11 | #[cfg(feature = "displays")] 12 | pub mod displays; 13 | 14 | #[cfg(feature = "filesystem")] 15 | pub mod filesystem; 16 | 17 | #[cfg(feature = "knobs")] 18 | pub mod knobs; 19 | 20 | #[cfg(feature = "ui")] 21 | pub mod ui; 22 | -------------------------------------------------------------------------------- /egui_extras_xt_example/src/bin/widget_gallery/pages/welcome_page.rs: -------------------------------------------------------------------------------- 1 | use eframe::egui::Ui; 2 | 3 | use crate::pages::PageImpl; 4 | 5 | pub struct WelcomePage {} 6 | 7 | #[allow(clippy::derivable_impls)] 8 | impl Default for WelcomePage { 9 | fn default() -> WelcomePage { 10 | WelcomePage {} 11 | } 12 | } 13 | 14 | impl PageImpl for WelcomePage { 15 | fn ui(&mut self, ui: &mut Ui) { 16 | ui.label("TODO"); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /egui_extras_xt/src/displays/mod.rs: -------------------------------------------------------------------------------- 1 | mod display_style; 2 | mod indicator_button; 3 | mod led_display; 4 | mod waveform_display; 5 | 6 | pub mod segmented_display; 7 | 8 | pub use display_style::{DisplayStyle, DisplayStylePreset}; 9 | pub use indicator_button::{IndicatorButton, IndicatorButtonBehavior}; 10 | pub use led_display::LedDisplay; 11 | pub use segmented_display::{DisplayKind, DisplayMetrics, SegmentedDisplayWidget}; 12 | pub use waveform_display::{BufferLayout, SignalEdge, WaveformDisplayWidget}; 13 | -------------------------------------------------------------------------------- /egui_extras_xt/src/filesystem/mod.rs: -------------------------------------------------------------------------------- 1 | mod breadcrumb_bar; 2 | mod directory_cache; 3 | mod directory_tree_view; 4 | mod path_symbol; 5 | 6 | pub use breadcrumb_bar::BreadcrumbBar; 7 | pub use directory_tree_view::DirectoryTreeViewWidget; 8 | 9 | // ---------------------------------------------------------------------------- 10 | 11 | use egui::Ui; 12 | use std::path::Path; 13 | 14 | pub type DirectoryFilter<'a> = Box bool + 'a>; 15 | 16 | pub type DirectoryContextMenu<'a> = ( 17 | Box, 18 | Box bool + 'a>, 19 | ); 20 | 21 | pub type DirectoryHoverUi<'a> = ( 22 | Box, 23 | Box bool + 'a>, 24 | ); 25 | -------------------------------------------------------------------------------- /egui_extras_xt/src/ui/drag_rangeinclusive.rs: -------------------------------------------------------------------------------- 1 | use std::ops::RangeInclusive; 2 | 3 | use egui::{DragValue, Response, Ui}; 4 | use emath::Numeric; 5 | 6 | pub trait DragRangeInclusive { 7 | fn drag_rangeinclusive(&mut self, value: &mut RangeInclusive) -> Response; 8 | } 9 | 10 | impl DragRangeInclusive for Ui { 11 | fn drag_rangeinclusive(&mut self, value: &mut RangeInclusive) -> Response { 12 | self.group(|ui| { 13 | let (mut start, mut end) = (*value.start(), *value.end()); 14 | 15 | ui.add(DragValue::new(&mut start)); 16 | ui.add(DragValue::new(&mut end)); 17 | 18 | *value = start..=end; 19 | }) 20 | .response 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /egui_extras_xt/src/compasses/compass_axis_labels.rs: -------------------------------------------------------------------------------- 1 | pub struct CompassAxisLabels { 2 | pub(crate) inner: [String; 4], 3 | } 4 | 5 | impl From<[&str; 4]> for CompassAxisLabels { 6 | fn from(source: [&str; 4]) -> Self { 7 | CompassAxisLabels { 8 | inner: source.map(String::from), 9 | } 10 | } 11 | } 12 | 13 | impl CompassAxisLabels { 14 | pub fn from_slice(source: &[T]) -> CompassAxisLabels 15 | where 16 | T: ToString, 17 | { 18 | CompassAxisLabels { 19 | inner: source 20 | .iter() 21 | .map(T::to_string) 22 | .collect::>() 23 | .try_into() 24 | .unwrap(), 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /egui_extras_xt_example/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "egui_extras_xt_example" 3 | version = "0.1.0" 4 | authors = ["Nagy Tibor "] 5 | description = "Demo applications for egui_extras_xt" 6 | license = "MIT" 7 | edition = "2021" 8 | repository = "https://github.com/xTibor/egui_extras_xt" 9 | homepage = "https://github.com/xTibor/egui_extras_xt" 10 | publish = false 11 | 12 | default-run = "widget_gallery" 13 | 14 | [badges] 15 | maintenance = { status = "as-is" } 16 | 17 | [dependencies] 18 | egui_extras_xt = { path = "../egui_extras_xt/", features = ["barcodes", "compasses", "displays", "filesystem", "knobs", "ui"] } 19 | 20 | eframe = "0.31" 21 | itertools = "0.14.0" 22 | strum = { version = "0.27.1", features = ["derive"] } 23 | -------------------------------------------------------------------------------- /egui_extras_xt_example/src/bin/about_window.rs: -------------------------------------------------------------------------------- 1 | use eframe::egui; 2 | 3 | use egui_extras_xt::show_about_window; 4 | 5 | #[derive(Default)] 6 | struct AboutWindowExample { 7 | about_window_open: bool, 8 | } 9 | 10 | impl eframe::App for AboutWindowExample { 11 | fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { 12 | egui::CentralPanel::default().show(ctx, |ui| { 13 | ui.toggle_value(&mut self.about_window_open, "About"); 14 | }); 15 | 16 | show_about_window!(ctx, &mut self.about_window_open); 17 | } 18 | } 19 | 20 | fn main() -> Result<(), eframe::Error> { 21 | let options = eframe::NativeOptions { 22 | viewport: egui::ViewportBuilder::default().with_inner_size([640.0, 480.0]), 23 | ..Default::default() 24 | }; 25 | 26 | eframe::run_native( 27 | "About window example", 28 | options, 29 | Box::new(|_| Ok(Box::::default())), 30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /egui_extras_xt/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "egui_extras_xt" 3 | version = "0.1.0" 4 | authors = ["Nagy Tibor "] 5 | description = "Widget library for egui" 6 | license = "MIT" 7 | edition = "2021" 8 | repository = "https://github.com/xTibor/egui_extras_xt" 9 | homepage = "https://github.com/xTibor/egui_extras_xt" 10 | categories = ["gui"] 11 | keywords = ["ui", "gui", "egui", "widgets", "interface"] 12 | publish = false 13 | 14 | [badges] 15 | maintenance = { status = "as-is" } 16 | 17 | [dependencies] 18 | ecolor = "0.31" 19 | egui = "0.31" 20 | emath = "0.31" 21 | epaint = "0.31" 22 | itertools = "0.14.0" 23 | strum = { version = "0.27.1", features = ["derive"] } 24 | 25 | barcoders = { version = "2.0.0", optional = true } 26 | datamatrix = { version = "0.3.2", optional = true, default-features = false } 27 | qrcode = { version = "0.14.1", optional = true, default-features = false } 28 | 29 | [features] 30 | barcodes = ["dep:barcoders", "dep:datamatrix", "dep:qrcode"] 31 | compasses = [] 32 | displays = [] 33 | filesystem = [] 34 | knobs = [] 35 | ui = [] 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Nagy Tibor 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /egui_extras_xt_example/src/bin/widget_gallery/pages/standard_buttons_page.rs: -------------------------------------------------------------------------------- 1 | use eframe::egui::{Grid, Ui}; 2 | use egui_extras_xt::ui::standard_buttons::{ButtonKind, StandardButtons}; 3 | use egui_extras_xt::ui::widgets_from_iter::ComboBoxFromIter; 4 | use strum::IntoEnumIterator; 5 | 6 | use crate::pages::PageImpl; 7 | 8 | pub struct StandardButtonsPage { 9 | button_kind: ButtonKind, 10 | } 11 | 12 | impl Default for StandardButtonsPage { 13 | fn default() -> StandardButtonsPage { 14 | StandardButtonsPage { 15 | button_kind: ButtonKind::Ok, 16 | } 17 | } 18 | } 19 | 20 | impl PageImpl for StandardButtonsPage { 21 | fn ui(&mut self, ui: &mut Ui) { 22 | ui.standard_button(self.button_kind); 23 | ui.separator(); 24 | 25 | Grid::new("standard_buttons_properties") 26 | .num_columns(2) 27 | .spacing([20.0, 10.0]) 28 | .striped(true) 29 | .show(ui, |ui| { 30 | ui.label("Button kind"); 31 | ui.combobox_from_iter("", &mut self.button_kind, ButtonKind::iter()); 32 | ui.end_row(); 33 | }); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /egui_extras_xt_example/src/bin/widget_gallery/pages/rotated_label_page.rs: -------------------------------------------------------------------------------- 1 | use eframe::egui::{Grid, TextStyle, Ui}; 2 | use egui_extras_xt::ui::rotated_label::RotatedLabel; 3 | 4 | use crate::pages::PageImpl; 5 | 6 | pub struct RotatedLabelPage { 7 | text: String, 8 | angle: f32, 9 | } 10 | 11 | impl Default for RotatedLabelPage { 12 | fn default() -> RotatedLabelPage { 13 | RotatedLabelPage { 14 | text: "egui_extras_xt".to_owned(), 15 | angle: 0.0, 16 | } 17 | } 18 | } 19 | 20 | impl PageImpl for RotatedLabelPage { 21 | fn ui(&mut self, ui: &mut Ui) { 22 | ui.scope(|ui| { 23 | ui.style_mut().override_text_style = Some(TextStyle::Heading); 24 | ui.add(RotatedLabel::new(&self.text).angle(self.angle)); 25 | }); 26 | ui.separator(); 27 | 28 | Grid::new("rotated_label_properties") 29 | .num_columns(2) 30 | .spacing([20.0, 10.0]) 31 | .striped(true) 32 | .show(ui, |ui| { 33 | ui.label("Text"); 34 | ui.text_edit_singleline(&mut self.text); 35 | ui.end_row(); 36 | 37 | ui.label("Angle"); 38 | ui.drag_angle(&mut self.angle); 39 | ui.end_row(); 40 | }); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /egui_extras_xt/src/filesystem/directory_cache.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | use std::path::{Path, PathBuf}; 3 | use std::sync::Arc; 4 | 5 | use egui::util::cache::{ComputerMut, FrameCache}; 6 | use itertools::Itertools; 7 | 8 | // ---------------------------------------------------------------------------- 9 | 10 | type DirectoryCacheKey<'a> = &'a Path; 11 | type DirectoryCacheValue = Arc>>; 12 | 13 | pub type DirectoryCache<'a> = FrameCache; 14 | 15 | // ---------------------------------------------------------------------------- 16 | 17 | #[derive(Default)] 18 | pub struct DirectoryCacheComputer; 19 | 20 | impl<'a> ComputerMut, DirectoryCacheValue> for DirectoryCacheComputer { 21 | fn compute(&mut self, key: DirectoryCacheKey) -> DirectoryCacheValue { 22 | Arc::new(std::fs::read_dir(key).map(|read_dir| { 23 | read_dir 24 | .filter_map(Result::ok) 25 | .map(|dir_entry| dir_entry.path()) 26 | .sorted_by_key(|path| { 27 | ( 28 | !path.is_dir(), 29 | path.file_name().unwrap().to_string_lossy().to_lowercase(), 30 | ) 31 | }) 32 | .collect_vec() 33 | })) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /egui_extras_xt_example/src/bin/widget_gallery/pages/hyperlink_with_icon_page.rs: -------------------------------------------------------------------------------- 1 | use eframe::egui::{Grid, TextStyle, Ui}; 2 | use egui_extras_xt::ui::hyperlink_with_icon::HyperlinkWithIcon; 3 | 4 | use crate::pages::PageImpl; 5 | 6 | pub struct HyperlinkWithIconPage { 7 | label: String, 8 | url: String, 9 | } 10 | 11 | impl Default for HyperlinkWithIconPage { 12 | fn default() -> HyperlinkWithIconPage { 13 | HyperlinkWithIconPage { 14 | label: "egui_extras_xt".to_owned(), 15 | url: "https://github.com/xTibor/egui_extras_xt".to_owned(), 16 | } 17 | } 18 | } 19 | 20 | impl PageImpl for HyperlinkWithIconPage { 21 | fn ui(&mut self, ui: &mut Ui) { 22 | ui.scope(|ui| { 23 | ui.style_mut().override_text_style = Some(TextStyle::Heading); 24 | ui.hyperlink_with_icon_to(&self.label, &self.url); 25 | }); 26 | ui.separator(); 27 | 28 | Grid::new("hyperlink_with_icon_properties") 29 | .num_columns(2) 30 | .spacing([20.0, 10.0]) 31 | .striped(true) 32 | .show(ui, |ui| { 33 | ui.label("Label"); 34 | ui.text_edit_singleline(&mut self.label); 35 | ui.end_row(); 36 | 37 | ui.label("URL"); 38 | ui.text_edit_singleline(&mut self.url); 39 | ui.end_row(); 40 | }); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /egui_extras_xt/src/ui/optional_value_widget.rs: -------------------------------------------------------------------------------- 1 | use egui::{Response, Ui}; 2 | 3 | pub trait OptionalValueWidget { 4 | fn optional_value_widget( 5 | &mut self, 6 | value: &mut Option, 7 | add_contents: impl FnOnce(&mut Self, &mut T) -> Response, 8 | ) -> Response; 9 | } 10 | 11 | impl OptionalValueWidget for Ui { 12 | fn optional_value_widget( 13 | &mut self, 14 | value: &mut Option, 15 | add_contents: impl FnOnce(&mut Self, &mut T) -> Response, 16 | ) -> Response { 17 | self.group(|ui| { 18 | ui.horizontal(|ui| { 19 | let mut checkbox_state = value.is_some(); 20 | let mut response = ui.checkbox(&mut checkbox_state, ""); 21 | 22 | match (value.is_some(), checkbox_state) { 23 | (false, true) => *value = Some(T::default()), 24 | (true, false) => *value = None, 25 | _ => {} 26 | }; 27 | 28 | match value { 29 | Some(ref mut value) => { 30 | response = response.union(add_contents(ui, value)); 31 | } 32 | None => { 33 | let mut dummy_value = T::default(); 34 | ui.add_enabled_ui(false, |ui| add_contents(ui, &mut dummy_value)); 35 | } 36 | } 37 | 38 | response 39 | }) 40 | }) 41 | .inner 42 | .inner 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /egui_extras_xt/src/ui/rotated_label.rs: -------------------------------------------------------------------------------- 1 | use egui::{FontSelection, Pos2, Rect, Response, Sense, Ui, Widget}; 2 | use emath::Rot2; 3 | use epaint::TextShape; 4 | 5 | #[must_use = "You should put this widget in an ui with `ui.add(widget);`"] 6 | pub struct RotatedLabel { 7 | text: String, 8 | angle: f32, 9 | } 10 | 11 | impl RotatedLabel { 12 | pub fn new(text: impl ToString) -> Self { 13 | Self { 14 | text: text.to_string(), 15 | angle: 0.0, 16 | } 17 | } 18 | 19 | pub fn angle(mut self, angle: impl Into) -> Self { 20 | self.angle = angle.into(); 21 | self 22 | } 23 | } 24 | 25 | impl Widget for RotatedLabel { 26 | fn ui(self, ui: &mut Ui) -> Response { 27 | let text_color = ui.style().visuals.text_color(); 28 | 29 | let galley = { 30 | let font_id = FontSelection::Default.resolve(ui.style()); 31 | ui.painter().layout_no_wrap(self.text, font_id, text_color) 32 | }; 33 | 34 | let rotation = Rot2::from_angle(self.angle); 35 | 36 | let (rect, response) = { 37 | let bounding_rect = 38 | Rect::from_center_size(Pos2::ZERO, galley.size()).rotate_bb(rotation); 39 | ui.allocate_exact_size(bounding_rect.size(), Sense::hover()) 40 | }; 41 | 42 | if ui.is_rect_visible(rect) { 43 | let pos = rect.center() - (rotation * (galley.size() / 2.0)); 44 | 45 | ui.painter().add(TextShape { 46 | angle: self.angle, 47 | ..TextShape::new(pos, galley, text_color) 48 | }); 49 | } 50 | 51 | response 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /egui_extras_xt_example/src/bin/label_rotation_demo.rs: -------------------------------------------------------------------------------- 1 | use std::f64::consts::TAU; 2 | 3 | use eframe::egui; 4 | use eframe::egui::ecolor::Hsva; 5 | use eframe::egui::{CentralPanel, Context, Direction, Layout, TextStyle}; 6 | 7 | use egui_extras_xt::ui::rotated_label::RotatedLabel; 8 | 9 | struct LabelRotationDemo { 10 | angle: f32, 11 | hue: f32, 12 | } 13 | 14 | impl Default for LabelRotationDemo { 15 | fn default() -> Self { 16 | Self { 17 | angle: 0.0, 18 | hue: 0.0, 19 | } 20 | } 21 | } 22 | 23 | impl eframe::App for LabelRotationDemo { 24 | fn update(&mut self, ctx: &Context, _frame: &mut eframe::Frame) { 25 | CentralPanel::default().show(ctx, |ui| { 26 | ui.with_layout(Layout::centered_and_justified(Direction::TopDown), |ui| { 27 | ui.style_mut().override_text_style = Some(TextStyle::Heading); 28 | ui.style_mut().visuals.override_text_color = 29 | Some(Hsva::new(self.hue, 1.0, 1.0, 1.0).into()); 30 | 31 | ui.add( 32 | RotatedLabel::new( 33 | "You spin me right 'round, baby, right 'round\n\n\ 34 | Like a record, baby, right 'round, 'round, 'round", 35 | ) 36 | .angle(self.angle), 37 | ); 38 | }); 39 | }); 40 | 41 | self.angle = (ctx.input(|input| input.time) * TAU / 9.0) as f32; 42 | self.hue = ((ctx.input(|input| input.time) / 3.0) % 1.0) as f32; 43 | ctx.request_repaint(); 44 | } 45 | } 46 | 47 | fn main() -> Result<(), eframe::Error> { 48 | let options = eframe::NativeOptions { 49 | viewport: egui::ViewportBuilder::default().with_inner_size([500.0, 500.0]), 50 | ..Default::default() 51 | }; 52 | 53 | eframe::run_native( 54 | "You spin me round (like a record)", 55 | options, 56 | Box::new(|_| Ok(Box::::default())), 57 | ) 58 | } 59 | -------------------------------------------------------------------------------- /egui_extras_xt/src/hash.rs: -------------------------------------------------------------------------------- 1 | #[rustfmt::skip] 2 | const PEARSON_LOOKUP: [u8; 256] = [ 3 | 0xBC, 0xFA, 0x5E, 0x91, 0x55, 0x56, 0x4B, 0xFB, 4 | 0x52, 0xDD, 0xF4, 0x1E, 0x5C, 0xE6, 0x11, 0x37, 5 | 0x12, 0xFF, 0x46, 0xFD, 0x4D, 0xEB, 0x82, 0xA5, 6 | 0xE8, 0xEC, 0x28, 0xB1, 0x81, 0x3F, 0xED, 0xE3, 7 | 0xEE, 0x13, 0x47, 0x97, 0xF9, 0x8F, 0x98, 0x3E, 8 | 0x92, 0xB2, 0xA1, 0x3B, 0x79, 0xA7, 0x58, 0xBE, 9 | 0x19, 0x0A, 0x34, 0x3D, 0x04, 0x7F, 0xB3, 0x77, 10 | 0xA6, 0xAD, 0x6C, 0xAA, 0xC9, 0x0F, 0x07, 0x40, 11 | 0x18, 0x6D, 0xDE, 0xF6, 0x7A, 0x62, 0x2D, 0xDF, 12 | 0x9C, 0x9F, 0x80, 0x17, 0xCF, 0xE5, 0x89, 0x71, 13 | 0xD9, 0x64, 0x8A, 0xBD, 0x2E, 0x42, 0xE7, 0x15, 14 | 0x7C, 0x25, 0xD3, 0x3C, 0xCE, 0x05, 0x2A, 0x4E, 15 | 0xC3, 0x1C, 0x7E, 0x88, 0x44, 0x43, 0xC6, 0x0C, 16 | 0x45, 0x02, 0x49, 0x01, 0xA0, 0x6E, 0x29, 0x93, 17 | 0x84, 0xEA, 0x2C, 0x1F, 0x86, 0xA3, 0x38, 0x95, 18 | 0xB5, 0xEF, 0x87, 0x4C, 0xF5, 0x99, 0x68, 0xA8, 19 | 0xFE, 0x53, 0x0B, 0x85, 0x7D, 0xC8, 0x00, 0x8C, 20 | 0x30, 0x66, 0x72, 0xC4, 0xAF, 0x73, 0x33, 0xE0, 21 | 0x51, 0xCB, 0xDA, 0xC5, 0x26, 0x0D, 0xE9, 0xD0, 22 | 0xCA, 0xE2, 0xA9, 0xC2, 0x4F, 0xBF, 0x9B, 0x1D, 23 | 0x8E, 0xA4, 0x10, 0x16, 0x3A, 0x2B, 0x60, 0x08, 24 | 0x4A, 0x21, 0x32, 0x1B, 0xA2, 0x75, 0xCC, 0xCD, 25 | 0xF8, 0x5A, 0x5D, 0x57, 0xF1, 0x09, 0x74, 0xD2, 26 | 0x94, 0x9D, 0x76, 0xBB, 0xC1, 0xD4, 0xAE, 0x50, 27 | 0xB0, 0x90, 0x69, 0x65, 0x9E, 0xF3, 0x24, 0xBA, 28 | 0xF7, 0xF2, 0x61, 0x31, 0xC0, 0x8D, 0xC7, 0x8B, 29 | 0xB4, 0x48, 0x36, 0xE4, 0x14, 0x9A, 0xD5, 0x2F, 30 | 0x41, 0xB9, 0xD6, 0x39, 0xD7, 0x23, 0xFC, 0x0E, 31 | 0x63, 0x22, 0xB8, 0xF0, 0x5B, 0xE1, 0x70, 0x83, 32 | 0x1A, 0x35, 0x78, 0x5F, 0x54, 0x20, 0x7B, 0x06, 33 | 0xAB, 0xB7, 0x59, 0x6F, 0xD1, 0xDB, 0xAC, 0x6B, 34 | 0x03, 0xD8, 0x27, 0xB6, 0xDC, 0x96, 0x67, 0x6A, 35 | ]; 36 | 37 | pub trait PearsonHash { 38 | fn pearson_hash(&self) -> u8; 39 | } 40 | 41 | impl PearsonHash for str { 42 | fn pearson_hash(&self) -> u8 { 43 | self.bytes() 44 | .fold(0, |hash, b| PEARSON_LOOKUP[(hash ^ b) as usize]) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /egui_extras_xt/src/displays/segmented_display/mod.rs: -------------------------------------------------------------------------------- 1 | mod display_metrics; 2 | mod widget; 3 | 4 | mod nine_segment; 5 | mod seven_segment; 6 | mod sixteen_segment; 7 | 8 | use strum::{Display, EnumIter}; 9 | 10 | pub use display_metrics::{DisplayMetrics, DisplayMetricsPreset}; 11 | pub use widget::SegmentedDisplayWidget; 12 | 13 | use egui::Pos2; 14 | 15 | // ---------------------------------------------------------------------------- 16 | 17 | pub type DisplayGlyph = u16; 18 | 19 | #[derive(Clone, Copy, Debug, Default)] 20 | pub struct DisplayDigit { 21 | pub glyph: DisplayGlyph, 22 | pub dot: bool, 23 | pub colon: bool, 24 | pub apostrophe: bool, 25 | } 26 | 27 | // ---------------------------------------------------------------------------- 28 | 29 | #[non_exhaustive] 30 | #[derive(Clone, Copy, Debug, Display, EnumIter, Eq, PartialEq)] 31 | pub enum DisplayKind { 32 | #[strum(to_string = "7-segment")] 33 | SevenSegment, 34 | 35 | #[strum(to_string = "9-segment")] 36 | NineSegment, 37 | 38 | #[strum(to_string = "16-segment")] 39 | SixteenSegment, 40 | } 41 | 42 | impl DisplayKind { 43 | #[must_use] 44 | pub(crate) fn display_impl(&self) -> Box { 45 | match *self { 46 | DisplayKind::SevenSegment => Box::new(seven_segment::SevenSegment), 47 | DisplayKind::NineSegment => Box::new(nine_segment::NineSegment), 48 | DisplayKind::SixteenSegment => Box::new(sixteen_segment::SixteenSegment), 49 | } 50 | } 51 | 52 | #[must_use] 53 | pub fn segment_count(&self) -> usize { 54 | self.display_impl().segment_count() 55 | } 56 | } 57 | 58 | // ---------------------------------------------------------------------------- 59 | 60 | pub(crate) trait DisplayImpl { 61 | fn segment_count(&self) -> usize; 62 | 63 | fn glyph(&self, c: char) -> Option; 64 | 65 | fn geometry( 66 | &self, 67 | digit_width: f32, 68 | digit_height: f32, 69 | segment_thickness: f32, 70 | segment_spacing: f32, 71 | digit_median: f32, 72 | ) -> Vec>; 73 | } 74 | -------------------------------------------------------------------------------- /egui_extras_xt_example/src/bin/widget_gallery/pages/qrcode_page.rs: -------------------------------------------------------------------------------- 1 | use eframe::egui::{DragValue, Grid, Ui}; 2 | use eframe::epaint::Color32; 3 | use egui_extras_xt::barcodes::QrCodeWidget; 4 | 5 | use crate::pages::PageImpl; 6 | 7 | pub struct QrCodePage { 8 | value: String, 9 | module_size: usize, 10 | quiet_zone: usize, 11 | foreground_color: Color32, 12 | background_color: Color32, 13 | } 14 | 15 | impl Default for QrCodePage { 16 | fn default() -> QrCodePage { 17 | QrCodePage { 18 | value: "egui_extras_xt".to_owned(), 19 | module_size: 6, 20 | quiet_zone: 4, 21 | foreground_color: Color32::BLACK, 22 | background_color: Color32::WHITE, 23 | } 24 | } 25 | } 26 | 27 | impl PageImpl for QrCodePage { 28 | fn ui(&mut self, ui: &mut Ui) { 29 | ui.add( 30 | QrCodeWidget::new(&self.value) 31 | .module_size(self.module_size) 32 | .quiet_zone(self.quiet_zone) 33 | .foreground_color(self.foreground_color) 34 | .background_color(self.background_color), 35 | ); 36 | ui.separator(); 37 | 38 | Grid::new("qrcode_properties") 39 | .num_columns(2) 40 | .spacing([20.0, 10.0]) 41 | .striped(true) 42 | .show(ui, |ui| { 43 | ui.label("Value"); 44 | ui.text_edit_singleline(&mut self.value); 45 | ui.end_row(); 46 | 47 | ui.label("Module size"); 48 | ui.add(DragValue::new(&mut self.module_size)); 49 | ui.end_row(); 50 | 51 | ui.label("Quiet zone"); 52 | ui.add(DragValue::new(&mut self.quiet_zone)); 53 | ui.end_row(); 54 | 55 | ui.label("Foreground color"); 56 | ui.color_edit_button_srgba(&mut self.foreground_color); 57 | ui.end_row(); 58 | 59 | ui.label("Background color"); 60 | ui.color_edit_button_srgba(&mut self.background_color); 61 | ui.end_row(); 62 | }); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /egui_extras_xt_example/src/bin/widget_gallery/pages/datamatrix_page.rs: -------------------------------------------------------------------------------- 1 | use eframe::egui::{DragValue, Grid, Ui}; 2 | use eframe::epaint::Color32; 3 | use egui_extras_xt::barcodes::DataMatrixWidget; 4 | 5 | use crate::pages::PageImpl; 6 | 7 | pub struct DataMatrixPage { 8 | value: String, 9 | module_size: usize, 10 | quiet_zone: usize, 11 | foreground_color: Color32, 12 | background_color: Color32, 13 | } 14 | 15 | impl Default for DataMatrixPage { 16 | fn default() -> DataMatrixPage { 17 | DataMatrixPage { 18 | value: "egui_extras_xt".to_owned(), 19 | module_size: 6, 20 | quiet_zone: 1, 21 | foreground_color: Color32::BLACK, 22 | background_color: Color32::WHITE, 23 | } 24 | } 25 | } 26 | 27 | impl PageImpl for DataMatrixPage { 28 | fn ui(&mut self, ui: &mut Ui) { 29 | ui.add( 30 | DataMatrixWidget::new(&self.value) 31 | .module_size(self.module_size) 32 | .quiet_zone(self.quiet_zone) 33 | .foreground_color(self.foreground_color) 34 | .background_color(self.background_color), 35 | ); 36 | ui.separator(); 37 | 38 | Grid::new("datamatrix_properties") 39 | .num_columns(2) 40 | .spacing([20.0, 10.0]) 41 | .striped(true) 42 | .show(ui, |ui| { 43 | ui.label("Value"); 44 | ui.text_edit_singleline(&mut self.value); 45 | ui.end_row(); 46 | 47 | ui.label("Module size"); 48 | ui.add(DragValue::new(&mut self.module_size)); 49 | ui.end_row(); 50 | 51 | ui.label("Quiet zone"); 52 | ui.add(DragValue::new(&mut self.quiet_zone)); 53 | ui.end_row(); 54 | 55 | ui.label("Foreground color"); 56 | ui.color_edit_button_srgba(&mut self.foreground_color); 57 | ui.end_row(); 58 | 59 | ui.label("Background color"); 60 | ui.color_edit_button_srgba(&mut self.background_color); 61 | ui.end_row(); 62 | }); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /egui_extras_xt_example/src/bin/widget_gallery/pages/led_display_page.rs: -------------------------------------------------------------------------------- 1 | use std::ops::RangeInclusive; 2 | 3 | use eframe::egui::{DragValue, Grid, Ui}; 4 | use egui_extras_xt::displays::{DisplayStyle, DisplayStylePreset, LedDisplay}; 5 | use egui_extras_xt::ui::drag_rangeinclusive::DragRangeInclusive; 6 | 7 | use crate::pages::ui::display_style_ui; 8 | use crate::pages::PageImpl; 9 | 10 | pub struct LedDisplayPage { 11 | value: f32, 12 | diameter: f32, 13 | padding: f32, 14 | range: RangeInclusive, 15 | style: DisplayStyle, 16 | style_preset: DisplayStylePreset, 17 | animated: bool, 18 | } 19 | 20 | impl Default for LedDisplayPage { 21 | fn default() -> LedDisplayPage { 22 | LedDisplayPage { 23 | value: 1.0, 24 | diameter: 16.0, 25 | padding: 0.25, 26 | range: 0.0..=1.0, 27 | style: DisplayStylePreset::Default.style(), 28 | style_preset: DisplayStylePreset::Default, 29 | animated: true, 30 | } 31 | } 32 | } 33 | 34 | impl PageImpl for LedDisplayPage { 35 | fn ui(&mut self, ui: &mut Ui) { 36 | ui.add( 37 | LedDisplay::new(self.value) 38 | .diameter(self.diameter) 39 | .padding(self.padding) 40 | .range(self.range.clone()) 41 | .style(self.style) 42 | .animated(self.animated), 43 | ); 44 | ui.separator(); 45 | 46 | Grid::new("led_display_properties") 47 | .num_columns(2) 48 | .spacing([20.0, 10.0]) 49 | .striped(true) 50 | .show(ui, |ui| { 51 | ui.label("Value"); 52 | ui.add(DragValue::new(&mut self.value)); 53 | ui.end_row(); 54 | 55 | ui.label("Diameter"); 56 | ui.add(DragValue::new(&mut self.diameter)); 57 | ui.end_row(); 58 | 59 | ui.label("Padding"); 60 | ui.add(DragValue::new(&mut self.padding)); 61 | ui.end_row(); 62 | 63 | ui.label("Range"); 64 | ui.drag_rangeinclusive(&mut self.range); 65 | ui.end_row(); 66 | 67 | ui.label("Style"); 68 | display_style_ui(ui, &mut self.style, &mut self.style_preset); 69 | ui.end_row(); 70 | 71 | ui.label("Animated"); 72 | ui.checkbox(&mut self.animated, ""); 73 | ui.end_row(); 74 | }); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /egui_extras_xt/src/displays/segmented_display/display_metrics.rs: -------------------------------------------------------------------------------- 1 | use strum::{Display, EnumIter}; 2 | 3 | #[derive(Clone, Copy)] 4 | pub struct DisplayMetrics { 5 | pub segment_spacing: f32, 6 | pub segment_thickness: f32, 7 | 8 | pub digit_median: f32, 9 | pub digit_ratio: f32, 10 | pub digit_shearing: f32, 11 | pub digit_spacing: f32, 12 | 13 | pub margin_horizontal: f32, 14 | pub margin_vertical: f32, 15 | 16 | pub colon_separation: f32, 17 | } 18 | 19 | impl Default for DisplayMetrics { 20 | fn default() -> Self { 21 | DisplayMetricsPreset::Default.metrics() 22 | } 23 | } 24 | 25 | // ---------------------------------------------------------------------------- 26 | 27 | #[non_exhaustive] 28 | #[derive(Clone, Copy, Display, EnumIter, Eq, PartialEq)] 29 | pub enum DisplayMetricsPreset { 30 | #[strum(to_string = "Default")] 31 | Default, 32 | 33 | #[strum(to_string = "Wide")] 34 | Wide, 35 | 36 | #[strum(to_string = "Calculator")] 37 | Calculator, 38 | } 39 | 40 | impl DisplayMetricsPreset { 41 | #[must_use] 42 | pub fn metrics(&self) -> DisplayMetrics { 43 | match *self { 44 | DisplayMetricsPreset::Default => DisplayMetrics { 45 | segment_spacing: 0.01, 46 | segment_thickness: 0.1, 47 | digit_median: -0.05, 48 | digit_ratio: 0.6, 49 | digit_shearing: 0.1, 50 | digit_spacing: 0.35, 51 | margin_horizontal: 0.3, 52 | margin_vertical: 0.1, 53 | colon_separation: 0.25, 54 | }, 55 | DisplayMetricsPreset::Wide => DisplayMetrics { 56 | segment_spacing: 0.02, 57 | segment_thickness: 0.12, 58 | digit_median: -0.05, 59 | digit_ratio: 1.0, 60 | digit_shearing: 0.1, 61 | digit_spacing: 0.20, 62 | margin_horizontal: 0.3, 63 | margin_vertical: 0.1, 64 | colon_separation: 0.25, 65 | }, 66 | DisplayMetricsPreset::Calculator => DisplayMetrics { 67 | segment_spacing: 0.01, 68 | segment_thickness: 0.11, 69 | digit_median: -0.05, 70 | digit_ratio: 0.4, 71 | digit_shearing: 0.1, 72 | digit_spacing: 0.38, 73 | margin_horizontal: 0.3, 74 | margin_vertical: 0.1, 75 | colon_separation: 0.25, 76 | }, 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /egui_extras_xt_example/src/bin/widget_gallery/pages/directory_tree_view_page.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use eframe::egui::{Grid, Ui}; 4 | use egui_extras_xt::filesystem::DirectoryTreeViewWidget; 5 | use egui_extras_xt::ui::optional_value_widget::OptionalValueWidget; 6 | 7 | use crate::pages::PageImpl; 8 | 9 | use crate::pages::ui::pathbuf_ui; 10 | 11 | pub struct DirectoryTreeViewPage { 12 | root_path: PathBuf, 13 | selected_path: Option, 14 | force_selected_open: bool, 15 | hide_file_extensions: bool, 16 | file_selectable: bool, 17 | directory_selectable: bool, 18 | } 19 | 20 | impl Default for DirectoryTreeViewPage { 21 | fn default() -> DirectoryTreeViewPage { 22 | DirectoryTreeViewPage { 23 | root_path: PathBuf::from("/"), 24 | selected_path: None, 25 | force_selected_open: false, 26 | hide_file_extensions: false, 27 | file_selectable: true, 28 | directory_selectable: false, 29 | } 30 | } 31 | } 32 | 33 | impl PageImpl for DirectoryTreeViewPage { 34 | fn ui(&mut self, ui: &mut Ui) { 35 | ui.add_sized( 36 | [300.0, 300.0], 37 | DirectoryTreeViewWidget::new(&mut self.selected_path, &self.root_path) 38 | .force_selected_open(self.force_selected_open) 39 | .hide_file_extensions(self.hide_file_extensions) 40 | .file_selectable(self.file_selectable) 41 | .directory_selectable(self.directory_selectable), 42 | ); 43 | ui.separator(); 44 | 45 | Grid::new("directory_tree_view_properties") 46 | .num_columns(2) 47 | .spacing([20.0, 10.0]) 48 | .striped(true) 49 | .show(ui, |ui| { 50 | ui.label("Root path"); 51 | pathbuf_ui(ui, &mut self.root_path); 52 | ui.end_row(); 53 | 54 | ui.label("Selected path"); 55 | ui.optional_value_widget(&mut self.selected_path, pathbuf_ui); 56 | ui.end_row(); 57 | 58 | ui.label("Force selected path open"); 59 | ui.checkbox(&mut self.force_selected_open, ""); 60 | ui.end_row(); 61 | 62 | ui.label("Hide file extensions"); 63 | ui.checkbox(&mut self.hide_file_extensions, ""); 64 | ui.end_row(); 65 | 66 | ui.label("Selectable files"); 67 | ui.checkbox(&mut self.file_selectable, ""); 68 | ui.end_row(); 69 | 70 | ui.label("Selectable directories"); 71 | ui.checkbox(&mut self.directory_selectable, ""); 72 | ui.end_row(); 73 | }); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /egui_extras_xt_example/src/bin/waveform_demo.rs: -------------------------------------------------------------------------------- 1 | use std::f32::consts::TAU; 2 | 3 | use eframe::egui::{self, DragValue}; 4 | 5 | use egui_extras_xt::displays::{BufferLayout, WaveformDisplayWidget}; 6 | 7 | const BUFFER_SIZE: usize = 1024; 8 | const OUTPUT_FREQUENCY: usize = 44100; 9 | 10 | struct WaveformDemoApp { 11 | enabled: bool, 12 | buffer: [f32; BUFFER_SIZE], 13 | left_frequency: f32, 14 | right_frequency: f32, 15 | phase: f32, 16 | } 17 | 18 | impl Default for WaveformDemoApp { 19 | fn default() -> Self { 20 | let mut tmp = Self { 21 | enabled: true, 22 | buffer: [0.0; BUFFER_SIZE], 23 | left_frequency: 440.0, 24 | right_frequency: 440.0, 25 | phase: 0.0, 26 | }; 27 | tmp.regenerate_buffer(); 28 | tmp 29 | } 30 | } 31 | 32 | impl WaveformDemoApp { 33 | #[allow(clippy::iter_skip_zero)] 34 | fn regenerate_buffer(&mut self) { 35 | for (index, sample) in self.buffer.iter_mut().skip(0).step_by(2).enumerate() { 36 | let q = index as f32 * (self.left_frequency / OUTPUT_FREQUENCY as f32) + self.phase; 37 | *sample = (q % 1.0) * 2.0 - 1.0; 38 | } 39 | 40 | for (index, sample) in self.buffer.iter_mut().skip(1).step_by(2).enumerate() { 41 | let q = index as f32 * (self.right_frequency / OUTPUT_FREQUENCY as f32) + self.phase; 42 | *sample = (q * TAU).sin(); 43 | } 44 | } 45 | } 46 | 47 | impl eframe::App for WaveformDemoApp { 48 | fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { 49 | egui::CentralPanel::default().show(ctx, |ui| { 50 | ui.horizontal(|ui| { 51 | if ui.add(DragValue::new(&mut self.left_frequency)).changed() { 52 | self.regenerate_buffer(); 53 | } 54 | 55 | if ui.add(DragValue::new(&mut self.right_frequency)).changed() { 56 | self.regenerate_buffer(); 57 | } 58 | 59 | if ui.add(DragValue::new(&mut self.phase).speed(0.1)).changed() { 60 | self.regenerate_buffer(); 61 | } 62 | }); 63 | 64 | ui.separator(); 65 | 66 | ui.add( 67 | WaveformDisplayWidget::new(&mut self.enabled) 68 | .track_name("Track #1") 69 | .channels(2) 70 | .channel_names(&["Left", "Right"]) 71 | .buffer(&self.buffer) 72 | .buffer_layout(BufferLayout::Interleaved), 73 | ); 74 | 75 | ui.separator(); 76 | egui::ScrollArea::both().show(ui, |ui| { 77 | ctx.settings_ui(ui); 78 | }); 79 | }); 80 | } 81 | } 82 | 83 | fn main() -> Result<(), eframe::Error> { 84 | let options = eframe::NativeOptions { 85 | viewport: egui::ViewportBuilder::default().with_inner_size([640.0, 480.0]), 86 | ..Default::default() 87 | }; 88 | 89 | eframe::run_native( 90 | "Waveform Display", 91 | options, 92 | Box::new(|_| Ok(Box::::default())), 93 | ) 94 | } 95 | -------------------------------------------------------------------------------- /egui_extras_xt/src/displays/led_display.rs: -------------------------------------------------------------------------------- 1 | use std::ops::RangeInclusive; 2 | 3 | use egui::{self, remap_clamp, Response, Sense, StrokeKind, Ui, Widget}; 4 | use emath::Vec2; 5 | use epaint::Stroke; 6 | 7 | use crate::displays::{DisplayStyle, DisplayStylePreset}; 8 | 9 | #[must_use = "You should put this widget in an ui with `ui.add(widget);`"] 10 | pub struct LedDisplay { 11 | value: f32, 12 | diameter: f32, 13 | padding: f32, 14 | range: RangeInclusive, 15 | style: DisplayStyle, 16 | animated: bool, 17 | } 18 | 19 | impl LedDisplay { 20 | pub fn new(value: f32) -> Self { 21 | Self { 22 | value, 23 | diameter: 16.0, 24 | padding: 0.25, 25 | range: 0.0..=1.0, 26 | style: DisplayStylePreset::Default.style(), 27 | animated: true, 28 | } 29 | } 30 | 31 | pub fn from_bool(value: bool) -> Self { 32 | Self::new(if value { 1.0 } else { 0.0 }) 33 | } 34 | 35 | pub fn diameter(mut self, diameter: impl Into) -> Self { 36 | self.diameter = diameter.into(); 37 | self 38 | } 39 | 40 | pub fn padding(mut self, padding: impl Into) -> Self { 41 | self.padding = padding.into(); 42 | self 43 | } 44 | 45 | pub fn range(mut self, range: RangeInclusive) -> Self { 46 | self.range = range; 47 | self 48 | } 49 | 50 | pub fn style(mut self, style: DisplayStyle) -> Self { 51 | self.style = style; 52 | self 53 | } 54 | 55 | pub fn style_preset(mut self, preset: DisplayStylePreset) -> Self { 56 | self.style = preset.style(); 57 | self 58 | } 59 | 60 | pub fn animated(mut self, animated: bool) -> Self { 61 | self.animated = animated; 62 | self 63 | } 64 | } 65 | 66 | impl Widget for LedDisplay { 67 | fn ui(self, ui: &mut Ui) -> Response { 68 | let desired_size = Vec2::splat(self.diameter + self.padding * self.diameter); 69 | 70 | let (rect, response) = ui.allocate_exact_size(desired_size, Sense::hover()); 71 | 72 | if ui.is_rect_visible(rect) { 73 | let value = remap_clamp( 74 | if self.animated { 75 | ui.ctx() 76 | .animate_value_with_time(response.id, self.value, 0.1) 77 | } else { 78 | self.value 79 | }, 80 | self.range, 81 | 0.0..=1.0, 82 | ); 83 | 84 | ui.painter().rect( 85 | rect, 86 | ui.style().visuals.noninteractive().corner_radius, 87 | self.style.background_color, 88 | Stroke::NONE, 89 | StrokeKind::Middle, 90 | ); 91 | 92 | ui.painter().circle( 93 | rect.center(), 94 | self.diameter / 2.0, 95 | self.style.foreground_color_blend(value), 96 | self.style.foreground_stroke_blend(value), 97 | ); 98 | } 99 | 100 | response 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /egui_extras_xt/src/ui/widgets_from_iter.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | 3 | use egui::{ComboBox, Response, Ui, WidgetText}; 4 | 5 | // ---------------------------------------------------------------------------- 6 | 7 | pub trait SelectableValueFromIter { 8 | fn selectable_value_from_iter( 9 | &mut self, 10 | current_value: &mut Value, 11 | values: impl Iterator, 12 | ) -> Response; 13 | } 14 | 15 | impl SelectableValueFromIter for Ui 16 | where 17 | Value: PartialEq + Display + Copy, 18 | { 19 | fn selectable_value_from_iter( 20 | &mut self, 21 | current_value: &mut Value, 22 | values: impl Iterator, 23 | ) -> Response { 24 | values 25 | .map(|value| self.selectable_value(current_value, value, format!("{value}"))) 26 | .reduce(|result, response| result.union(response)) 27 | .unwrap_or_else(|| { 28 | self.colored_label(self.style().visuals.error_fg_color, "\u{1F525} No items") 29 | }) 30 | } 31 | } 32 | 33 | // ---------------------------------------------------------------------------- 34 | 35 | pub trait RadioValueFromIter { 36 | fn radio_value_from_iter( 37 | &mut self, 38 | current_value: &mut Value, 39 | values: impl Iterator, 40 | ) -> Response; 41 | } 42 | 43 | impl RadioValueFromIter for Ui 44 | where 45 | Value: PartialEq + Display + Copy, 46 | { 47 | fn radio_value_from_iter( 48 | &mut self, 49 | current_value: &mut Value, 50 | values: impl Iterator, 51 | ) -> Response { 52 | values 53 | .map(|value| self.radio_value(current_value, value, format!("{value}"))) 54 | .reduce(|result, response| result.union(response)) 55 | .unwrap_or_else(|| { 56 | self.colored_label(self.style().visuals.error_fg_color, "\u{1F525} No items") 57 | }) 58 | } 59 | } 60 | 61 | // ---------------------------------------------------------------------------- 62 | 63 | pub trait ComboBoxFromIter { 64 | fn combobox_from_iter( 65 | &mut self, 66 | label: impl Into, 67 | current_value: &mut Value, 68 | values: impl Iterator, 69 | ) -> Response; 70 | } 71 | 72 | impl ComboBoxFromIter for Ui 73 | where 74 | Value: PartialEq + Display + Copy, 75 | { 76 | fn combobox_from_iter( 77 | &mut self, 78 | label: impl Into, 79 | current_value: &mut Value, 80 | values: impl Iterator, 81 | ) -> Response { 82 | let combobox_response = ComboBox::from_label(label) 83 | .selected_text(format!("{current_value}")) 84 | .show_ui(self, |ui| { 85 | values 86 | .map(|value| ui.selectable_value(current_value, value, format!("{value}"))) 87 | .reduce(|result, response| result.union(response)) 88 | .unwrap_or_else(|| { 89 | ui.colored_label(ui.style().visuals.error_fg_color, "\u{1F525} No items") 90 | }) 91 | }); 92 | 93 | combobox_response 94 | .inner 95 | .unwrap_or(combobox_response.response) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /egui_extras_xt/src/ui/widgets_from_slice.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | 3 | use egui::{ComboBox, Response, Ui, WidgetText}; 4 | 5 | // ---------------------------------------------------------------------------- 6 | 7 | pub trait SelectableValueFromSlice<'a, Value> { 8 | fn selectable_value_from_slice( 9 | &mut self, 10 | current_value: &mut Value, 11 | values: &'a [Value], 12 | ) -> Response; 13 | } 14 | 15 | impl<'a, Value> SelectableValueFromSlice<'a, Value> for Ui 16 | where 17 | Value: PartialEq + Display + Clone, 18 | { 19 | fn selectable_value_from_slice( 20 | &mut self, 21 | current_value: &mut Value, 22 | values: &'a [Value], 23 | ) -> Response { 24 | values 25 | .iter() 26 | .map(|value| self.selectable_value(current_value, value.clone(), format!("{value}"))) 27 | .reduce(|result, response| result.union(response)) 28 | .unwrap_or_else(|| { 29 | self.colored_label(self.style().visuals.error_fg_color, "\u{1F525} No items") 30 | }) 31 | } 32 | } 33 | 34 | // ---------------------------------------------------------------------------- 35 | 36 | pub trait RadioValueFromSlice<'a, Value> { 37 | fn radio_value_from_slice( 38 | &mut self, 39 | current_value: &mut Value, 40 | values: &'a [Value], 41 | ) -> Response; 42 | } 43 | 44 | impl<'a, Value> RadioValueFromSlice<'a, Value> for Ui 45 | where 46 | Value: PartialEq + Display + Clone, 47 | { 48 | fn radio_value_from_slice( 49 | &mut self, 50 | current_value: &mut Value, 51 | values: &'a [Value], 52 | ) -> Response { 53 | values 54 | .iter() 55 | .map(|value| self.radio_value(current_value, value.clone(), format!("{value}"))) 56 | .reduce(|result, response| result.union(response)) 57 | .unwrap_or_else(|| { 58 | self.colored_label(self.style().visuals.error_fg_color, "\u{1F525} No items") 59 | }) 60 | } 61 | } 62 | 63 | // ---------------------------------------------------------------------------- 64 | 65 | pub trait ComboBoxFromSlice<'a, Value> { 66 | fn combobox_from_slice( 67 | &mut self, 68 | label: impl Into, 69 | current_value: &mut Value, 70 | values: &'a [Value], 71 | ) -> Response; 72 | } 73 | 74 | impl<'a, Value> ComboBoxFromSlice<'a, Value> for Ui 75 | where 76 | Value: PartialEq + Display + Clone, 77 | { 78 | fn combobox_from_slice( 79 | &mut self, 80 | label: impl Into, 81 | current_value: &mut Value, 82 | values: &'a [Value], 83 | ) -> Response { 84 | let combobox_response = ComboBox::from_label(label) 85 | .selected_text(format!("{current_value}")) 86 | .show_ui(self, |ui| { 87 | values 88 | .iter() 89 | .map(|value| { 90 | ui.selectable_value(current_value, value.clone(), format!("{value}")) 91 | }) 92 | .reduce(|result, response| result.union(response)) 93 | .unwrap_or_else(|| { 94 | ui.colored_label(ui.style().visuals.error_fg_color, "\u{1F525} No items") 95 | }) 96 | }); 97 | 98 | combobox_response 99 | .inner 100 | .unwrap_or(combobox_response.response) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /egui_extras_xt_example/src/bin/widget_gallery/pages/segmented_display_page.rs: -------------------------------------------------------------------------------- 1 | use eframe::egui::{DragValue, Grid, Ui}; 2 | use egui_extras_xt::displays::segmented_display::DisplayMetricsPreset; 3 | use egui_extras_xt::displays::{ 4 | DisplayKind, DisplayMetrics, DisplayStyle, DisplayStylePreset, SegmentedDisplayWidget, 5 | }; 6 | use egui_extras_xt::ui::widgets_from_iter::SelectableValueFromIter; 7 | use strum::IntoEnumIterator; 8 | 9 | use crate::pages::ui::{display_metrics_ui, display_style_ui}; 10 | use crate::pages::PageImpl; 11 | 12 | pub struct SegmentedDisplayPage { 13 | value: String, 14 | display_kind: DisplayKind, 15 | digit_height: f32, 16 | metrics: DisplayMetrics, 17 | metrics_preset: DisplayMetricsPreset, 18 | style: DisplayStyle, 19 | style_preset: DisplayStylePreset, 20 | show_dots: bool, 21 | show_colons: bool, 22 | show_apostrophes: bool, 23 | } 24 | 25 | impl Default for SegmentedDisplayPage { 26 | fn default() -> SegmentedDisplayPage { 27 | SegmentedDisplayPage { 28 | value: "EGUI_EXTRAS_XT".to_owned(), 29 | display_kind: DisplayKind::SixteenSegment, 30 | digit_height: 80.0, 31 | metrics: DisplayMetricsPreset::Default.metrics(), 32 | metrics_preset: DisplayMetricsPreset::Default, 33 | style: DisplayStylePreset::Default.style(), 34 | style_preset: DisplayStylePreset::Default, 35 | show_dots: true, 36 | show_colons: true, 37 | show_apostrophes: true, 38 | } 39 | } 40 | } 41 | 42 | impl PageImpl for SegmentedDisplayPage { 43 | fn ui(&mut self, ui: &mut Ui) { 44 | ui.add( 45 | SegmentedDisplayWidget::new(self.display_kind) 46 | .digit_height(self.digit_height) 47 | .metrics(self.metrics) 48 | .style(self.style) 49 | .show_dots(self.show_dots) 50 | .show_colons(self.show_colons) 51 | .show_apostrophes(self.show_apostrophes) 52 | .push_string(&self.value), 53 | ); 54 | ui.separator(); 55 | 56 | Grid::new("segmented_display_properties") 57 | .num_columns(2) 58 | .spacing([20.0, 10.0]) 59 | .striped(true) 60 | .show(ui, |ui| { 61 | ui.label("Value"); 62 | ui.text_edit_singleline(&mut self.value); 63 | ui.end_row(); 64 | 65 | ui.label("Display kind"); 66 | ui.horizontal(|ui| { 67 | ui.selectable_value_from_iter(&mut self.display_kind, DisplayKind::iter()); 68 | }); 69 | ui.end_row(); 70 | 71 | ui.label("Digit height"); 72 | ui.add(DragValue::new(&mut self.digit_height)); 73 | ui.end_row(); 74 | 75 | ui.label("Metrics"); 76 | display_metrics_ui(ui, &mut self.metrics, &mut self.metrics_preset); 77 | ui.end_row(); 78 | 79 | ui.label("Style"); 80 | display_style_ui(ui, &mut self.style, &mut self.style_preset); 81 | ui.end_row(); 82 | 83 | ui.label("Show dots"); 84 | ui.checkbox(&mut self.show_dots, ""); 85 | ui.end_row(); 86 | 87 | ui.label("Show colons"); 88 | ui.checkbox(&mut self.show_colons, ""); 89 | ui.end_row(); 90 | 91 | ui.label("Show apostrophes"); 92 | ui.checkbox(&mut self.show_apostrophes, ""); 93 | ui.end_row(); 94 | }); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /egui_extras_xt_example/src/bin/widget_gallery/pages/indicator_button_page.rs: -------------------------------------------------------------------------------- 1 | use eframe::egui::{DragValue, Grid, Ui}; 2 | use egui_extras_xt::displays::{ 3 | DisplayStyle, DisplayStylePreset, IndicatorButton, IndicatorButtonBehavior, 4 | }; 5 | use egui_extras_xt::ui::widgets_from_iter::SelectableValueFromIter; 6 | use strum::IntoEnumIterator; 7 | 8 | use crate::pages::ui::display_style_ui; 9 | use crate::pages::PageImpl; 10 | 11 | pub struct IndicatorButtonPage { 12 | value: bool, 13 | width: f32, 14 | height: f32, 15 | label: String, 16 | style: DisplayStyle, 17 | style_preset: DisplayStylePreset, 18 | animated: bool, 19 | interactive: bool, 20 | margin: f32, 21 | behavior: IndicatorButtonBehavior, 22 | } 23 | 24 | impl Default for IndicatorButtonPage { 25 | fn default() -> IndicatorButtonPage { 26 | IndicatorButtonPage { 27 | value: false, 28 | width: 64.0, 29 | height: 40.0, 30 | label: "TEST".to_owned(), 31 | style: DisplayStylePreset::Default.style(), 32 | style_preset: DisplayStylePreset::Default, 33 | animated: true, 34 | interactive: true, 35 | margin: 0.2, 36 | behavior: IndicatorButtonBehavior::Toggle, 37 | } 38 | } 39 | } 40 | 41 | impl PageImpl for IndicatorButtonPage { 42 | fn ui(&mut self, ui: &mut Ui) { 43 | ui.add( 44 | IndicatorButton::new(&mut self.value) 45 | .width(self.width) 46 | .height(self.height) 47 | .label(&self.label) 48 | .style(self.style) 49 | .animated(self.animated) 50 | .interactive(self.interactive) 51 | .margin(self.margin) 52 | .behavior(self.behavior), 53 | ); 54 | ui.separator(); 55 | 56 | Grid::new("indicator_button_properties") 57 | .num_columns(2) 58 | .spacing([20.0, 10.0]) 59 | .striped(true) 60 | .show(ui, |ui| { 61 | ui.label("Value"); 62 | ui.checkbox(&mut self.value, ""); 63 | ui.end_row(); 64 | 65 | ui.label("Width"); 66 | ui.add(DragValue::new(&mut self.width)); 67 | ui.end_row(); 68 | 69 | ui.label("Height"); 70 | ui.add(DragValue::new(&mut self.height)); 71 | ui.end_row(); 72 | 73 | ui.label("Label"); 74 | ui.text_edit_singleline(&mut self.label); 75 | ui.end_row(); 76 | 77 | ui.label("Style"); 78 | display_style_ui(ui, &mut self.style, &mut self.style_preset); 79 | ui.end_row(); 80 | 81 | ui.label("Animated"); 82 | ui.checkbox(&mut self.animated, ""); 83 | ui.end_row(); 84 | 85 | ui.label("Interactive"); 86 | ui.checkbox(&mut self.interactive, ""); 87 | ui.end_row(); 88 | 89 | ui.label("Margin"); 90 | ui.add(DragValue::new(&mut self.margin)); 91 | ui.end_row(); 92 | 93 | ui.label("Behavior"); 94 | ui.horizontal(|ui| { 95 | ui.selectable_value_from_iter( 96 | &mut self.behavior, 97 | IndicatorButtonBehavior::iter(), 98 | ); 99 | }); 100 | ui.end_row(); 101 | }); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /egui_extras_xt/src/filesystem/path_symbol.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::OsStr; 2 | use std::path::Path; 3 | 4 | #[cfg(unix)] 5 | use std::os::unix::prelude::PermissionsExt; 6 | 7 | pub trait PathSymbol { 8 | fn symbol(&self) -> char; 9 | } 10 | 11 | impl PathSymbol for Path { 12 | fn symbol(&self) -> char { 13 | if self.is_symlink() { 14 | '\u{2BA9}' 15 | } else if self.is_dir() { 16 | if self.parent().is_none() { 17 | // Root directory 18 | '\u{1F5A5}' 19 | } else { 20 | // Normal directory 21 | '\u{1F5C0}' 22 | } 23 | } else { 24 | // Executables 25 | #[cfg(unix)] 26 | if let Ok(metadata) = self.metadata() { 27 | if metadata.permissions().mode() & 0o111 != 0 { 28 | return '\u{2699}'; 29 | } 30 | } 31 | 32 | let file_extension = self 33 | .extension() 34 | .map(OsStr::to_string_lossy) 35 | .map(|s| s.to_lowercase()); 36 | 37 | #[allow(clippy::match_same_arms)] 38 | match file_extension.as_deref() { 39 | // Plain text 40 | Some( 41 | "asm" | "c" | "conf" | "cpp" | "css" | "glsl" | "h" | "htm" | "html" | "inc" 42 | | "inf" | "ini" | "js" | "json" | "log" | "lua" | "md" | "pas" | "pp" | "py" 43 | | "rs" | "s" | "toml" | "txt" | "xml" | "yml", 44 | ) => '\u{1F5B9}', 45 | // Rich text 46 | Some("doc" | "docx" | "pdf" | "rtf") => '\u{1F5BB}', 47 | // Images 48 | Some( 49 | "bmp" | "gif" | "jpe" | "jpeg" | "jpg" | "jxl" | "kra" | "pam" | "pbm" | "pgm" 50 | | "png" | "pnm" | "ppm" | "qoi" | "svg" | "svgz" | "webp", 51 | ) => '\u{1F5BC}', 52 | // Video 53 | Some("avi" | "mkv" | "mp4" | "ogv" | "webm") => '\u{1F39E}', 54 | // Audio 55 | Some( 56 | "flac" | "it" | "m4a" | "mid" | "mmp" | "mmpz" | "mod" | "mp3" | "mscz" | "oga" 57 | | "ogg" | "opus" | "s3m" | "sid" | "sng" | "wav" | "wma" | "xm", 58 | ) => '\u{266B}', 59 | // Archives 60 | Some("7z" | "arj" | "cab" | "gz" | "rar" | "tar" | "wad" | "xz" | "zip") => { 61 | '\u{1F4E6}' 62 | } 63 | // SoundFont files 64 | Some("sbk" | "sf2" | "sf3" | "sfark" | "dls") => '\u{1F4E6}', 65 | // Compact disc images 66 | Some("iso") => '\u{1F4BF}', 67 | // Floppy disk images 68 | Some("d64" | "dsk") => '\u{1F4BE}', 69 | // Video game ROM images 70 | Some("3ds" | "cia" | "gba" | "nds" | "nes" | "sfc" | "smc") => '\u{1F3AE}', 71 | // Video game save files 72 | Some("sav" | "save") => '\u{1F4BE}', 73 | // Video game patch files 74 | Some("bps" | "ips" | "ups") => '\u{229E}', 75 | // Harddisk images 76 | Some("hdi" | "hdm" | "vdi") => '\u{1F5B4}', 77 | // Fonts 78 | Some("otb" | "otf" | "ttf" | "woff" | "woff2") => '\u{1F5DB}', 79 | // Binaries 80 | Some( 81 | "appimage" | "bat" | "com" | "dll" | "exe" | "love" | "o" | "ppu" | "ps1" 82 | | "sh" | "so", 83 | ) => '\u{2699}', 84 | // Unknown 85 | _ => '\u{1F5CB}', 86 | } 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /egui_extras_xt_example/src/bin/glyph_editor.rs: -------------------------------------------------------------------------------- 1 | use eframe::egui::{self}; 2 | use eframe::epaint::Vec2; 3 | 4 | use strum::IntoEnumIterator; 5 | 6 | use egui_extras_xt::displays::segmented_display::{DisplayDigit, DisplayGlyph}; 7 | use egui_extras_xt::displays::{DisplayKind, SegmentedDisplayWidget}; 8 | use egui_extras_xt::ui::standard_buttons::StandardButtons; 9 | use egui_extras_xt::ui::widgets_from_iter::SelectableValueFromIter; 10 | 11 | struct GlyphEditorApp { 12 | display_kind: DisplayKind, 13 | digit: DisplayDigit, 14 | } 15 | 16 | impl Default for GlyphEditorApp { 17 | fn default() -> Self { 18 | Self { 19 | display_kind: DisplayKind::SixteenSegment, 20 | digit: DisplayDigit::default(), 21 | } 22 | } 23 | } 24 | 25 | impl eframe::App for GlyphEditorApp { 26 | fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { 27 | egui::CentralPanel::default().show(ctx, |ui| { 28 | ui.horizontal(|ui| { 29 | ui.add( 30 | SegmentedDisplayWidget::new(self.display_kind) 31 | .digit_height(192.0) 32 | .push_digit(self.digit), 33 | ); 34 | 35 | ui.separator(); 36 | 37 | ui.vertical(|ui| { 38 | ui.horizontal(|ui| { 39 | ui.selectable_value_from_iter(&mut self.display_kind, DisplayKind::iter()); 40 | }); 41 | 42 | ui.horizontal(|ui| { 43 | if ui.reset_button().clicked() { 44 | self.digit = DisplayDigit::default(); 45 | } 46 | 47 | { 48 | let hex_value = format!("0x{:04X}", self.digit.glyph); 49 | if ui 50 | .button(&hex_value) 51 | .on_hover_text("\u{1F5D0} Copy to clipboard") 52 | .clicked() 53 | { 54 | ctx.copy_text(hex_value); 55 | } 56 | } 57 | }); 58 | }) 59 | }); 60 | 61 | ui.separator(); 62 | 63 | ui.horizontal_wrapped(|ui| { 64 | ui.spacing_mut().item_spacing = Vec2::ZERO; 65 | 66 | for segment_index in (0..self.display_kind.segment_count()).rev() { 67 | let mut segment_state = ((self.digit.glyph >> segment_index) & 0x01) != 0x00; 68 | 69 | if ui 70 | .add( 71 | SegmentedDisplayWidget::new(self.display_kind) 72 | .digit_height(64.0) 73 | .push_digit(DisplayDigit { 74 | glyph: 1 << segment_index, 75 | ..Default::default() 76 | }), 77 | ) 78 | .clicked() 79 | { 80 | segment_state = !segment_state; 81 | } 82 | 83 | self.digit.glyph = (self.digit.glyph & !(1 << segment_index)) 84 | | ((segment_state as DisplayGlyph) << segment_index); 85 | } 86 | }); 87 | }); 88 | } 89 | } 90 | 91 | fn main() -> Result<(), eframe::Error> { 92 | let options = eframe::NativeOptions { 93 | viewport: egui::ViewportBuilder::default().with_inner_size([570.0, 410.0]), 94 | ..Default::default() 95 | }; 96 | 97 | eframe::run_native( 98 | "Glyph Editor", 99 | options, 100 | Box::new(|_| Ok(Box::::default())), 101 | ) 102 | } 103 | -------------------------------------------------------------------------------- /egui_extras_xt_example/src/bin/widget_gallery/pages/barcode_page.rs: -------------------------------------------------------------------------------- 1 | use eframe::egui::{DragValue, Grid, Ui}; 2 | use eframe::epaint::Color32; 3 | use egui_extras_xt::barcodes::{BarcodeKind, BarcodeWidget}; 4 | use egui_extras_xt::ui::widgets_from_iter::ComboBoxFromIter; 5 | use strum::IntoEnumIterator; 6 | 7 | use crate::pages::PageImpl; 8 | 9 | pub struct BarcodePage { 10 | value: String, 11 | barcode_kind: BarcodeKind, 12 | bar_width: usize, 13 | bar_height: f32, 14 | horizontal_padding: f32, 15 | vertical_padding: f32, 16 | label: String, 17 | label_height: f32, 18 | label_top_margin: f32, 19 | foreground_color: Color32, 20 | background_color: Color32, 21 | } 22 | 23 | impl Default for BarcodePage { 24 | fn default() -> BarcodePage { 25 | BarcodePage { 26 | value: "01189998819991197253".to_owned(), 27 | barcode_kind: BarcodeKind::Code39, 28 | bar_width: 2, 29 | bar_height: 64.0, 30 | horizontal_padding: 50.0, 31 | vertical_padding: 10.0, 32 | label: "egui_extras_xt".to_owned(), 33 | label_height: 20.0, 34 | label_top_margin: 4.0, 35 | foreground_color: Color32::BLACK, 36 | background_color: Color32::WHITE, 37 | } 38 | } 39 | } 40 | 41 | impl PageImpl for BarcodePage { 42 | fn ui(&mut self, ui: &mut Ui) { 43 | ui.add( 44 | BarcodeWidget::new(&self.value) 45 | .barcode_kind(self.barcode_kind) 46 | .bar_width(self.bar_width) 47 | .bar_height(self.bar_height) 48 | .horizontal_padding(self.horizontal_padding) 49 | .vertical_padding(self.vertical_padding) 50 | .label(&self.label) 51 | .label_height(self.label_height) 52 | .label_top_margin(self.label_top_margin) 53 | .foreground_color(self.foreground_color) 54 | .background_color(self.background_color), 55 | ); 56 | ui.separator(); 57 | 58 | Grid::new("barcode_properties") 59 | .num_columns(2) 60 | .spacing([20.0, 10.0]) 61 | .striped(true) 62 | .show(ui, |ui| { 63 | ui.label("Value"); 64 | ui.text_edit_singleline(&mut self.value); 65 | ui.end_row(); 66 | 67 | ui.label("Barcode kind"); 68 | ui.combobox_from_iter("", &mut self.barcode_kind, BarcodeKind::iter()); 69 | ui.end_row(); 70 | 71 | ui.label("Bar width"); 72 | ui.add(DragValue::new(&mut self.bar_width)); 73 | ui.end_row(); 74 | 75 | ui.label("Bar height"); 76 | ui.add(DragValue::new(&mut self.bar_height)); 77 | ui.end_row(); 78 | 79 | ui.label("Horizontal padding"); 80 | ui.add(DragValue::new(&mut self.horizontal_padding)); 81 | ui.end_row(); 82 | 83 | ui.label("Vertical padding"); 84 | ui.add(DragValue::new(&mut self.vertical_padding)); 85 | ui.end_row(); 86 | 87 | ui.label("Label"); 88 | ui.text_edit_singleline(&mut self.label); 89 | ui.end_row(); 90 | 91 | ui.label("Label height"); 92 | ui.add(DragValue::new(&mut self.label_height)); 93 | ui.end_row(); 94 | 95 | ui.label("Label top margin"); 96 | ui.add(DragValue::new(&mut self.label_top_margin)); 97 | ui.end_row(); 98 | 99 | ui.label("Foreground color"); 100 | ui.color_edit_button_srgba(&mut self.foreground_color); 101 | ui.end_row(); 102 | 103 | ui.label("Background color"); 104 | ui.color_edit_button_srgba(&mut self.background_color); 105 | ui.end_row(); 106 | }); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /egui_extras_xt_example/src/bin/widget_gallery/pages/thumbstick_widget_page.rs: -------------------------------------------------------------------------------- 1 | use std::ops::RangeInclusive; 2 | 3 | use eframe::egui::{DragValue, Grid, Ui}; 4 | use egui_extras_xt::knobs::{ThumbstickDeadZone, ThumbstickSnap, ThumbstickWidget}; 5 | use egui_extras_xt::ui::drag_rangeinclusive::DragRangeInclusive; 6 | 7 | use crate::pages::ui::{thumbstick_dead_zone_ui, thumbstick_snap_ui}; 8 | use crate::pages::PageImpl; 9 | 10 | pub struct ThumbstickWidgetPage { 11 | position: (f32, f32), 12 | range_x: RangeInclusive, 13 | range_y: RangeInclusive, 14 | precision: f32, 15 | interactive: bool, 16 | diameter: f32, 17 | animated: bool, 18 | auto_center: bool, 19 | show_axes: bool, 20 | snap: ThumbstickSnap, 21 | dead_zone: ThumbstickDeadZone, 22 | } 23 | 24 | impl Default for ThumbstickWidgetPage { 25 | fn default() -> ThumbstickWidgetPage { 26 | ThumbstickWidgetPage { 27 | position: (0.0, 0.0), 28 | range_x: -1.0..=1.0, 29 | range_y: -1.0..=1.0, 30 | precision: 1.0, 31 | interactive: true, 32 | diameter: 96.0, 33 | animated: true, 34 | auto_center: true, 35 | show_axes: true, 36 | snap: ThumbstickSnap::None, 37 | dead_zone: ThumbstickDeadZone::None, 38 | } 39 | } 40 | } 41 | 42 | impl PageImpl for ThumbstickWidgetPage { 43 | fn ui(&mut self, ui: &mut Ui) { 44 | ui.add( 45 | ThumbstickWidget::new(&mut self.position) 46 | .range_x(self.range_x.clone()) 47 | .range_y(self.range_y.clone()) 48 | .precision(self.precision) 49 | .interactive(self.interactive) 50 | .diameter(self.diameter) 51 | .animated(self.animated) 52 | .auto_center(self.auto_center) 53 | .show_axes(self.show_axes) 54 | .snap(self.snap) 55 | .dead_zone(self.dead_zone), 56 | ); 57 | ui.separator(); 58 | 59 | Grid::new("thumbstick_widget_properties") 60 | .num_columns(2) 61 | .spacing([20.0, 10.0]) 62 | .striped(true) 63 | .show(ui, |ui| { 64 | ui.label("X position"); 65 | ui.add(DragValue::new(&mut self.position.0)); 66 | ui.end_row(); 67 | 68 | ui.label("Y position"); 69 | ui.add(DragValue::new(&mut self.position.1)); 70 | ui.end_row(); 71 | 72 | ui.label("X range"); 73 | ui.drag_rangeinclusive(&mut self.range_x); 74 | ui.end_row(); 75 | 76 | ui.label("Y range"); 77 | ui.drag_rangeinclusive(&mut self.range_y); 78 | ui.end_row(); 79 | 80 | ui.label("Precision"); 81 | ui.add(DragValue::new(&mut self.precision)); 82 | ui.end_row(); 83 | 84 | ui.label("Interactive"); 85 | ui.checkbox(&mut self.interactive, ""); 86 | ui.end_row(); 87 | 88 | ui.label("Diameter"); 89 | ui.add(DragValue::new(&mut self.diameter)); 90 | ui.end_row(); 91 | 92 | ui.label("Animated"); 93 | ui.checkbox(&mut self.animated, ""); 94 | ui.end_row(); 95 | 96 | ui.label("Auto-center"); 97 | ui.checkbox(&mut self.auto_center, ""); 98 | ui.end_row(); 99 | 100 | ui.label("Show axes"); 101 | ui.checkbox(&mut self.show_axes, ""); 102 | ui.end_row(); 103 | 104 | ui.label("Snap"); 105 | thumbstick_snap_ui(ui, &mut self.snap); 106 | ui.end_row(); 107 | 108 | ui.label("Dead zone"); 109 | thumbstick_dead_zone_ui(ui, &mut self.dead_zone); 110 | ui.end_row(); 111 | }); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /egui_extras_xt/src/barcodes/datamatrix_widget.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Borrow; 2 | use std::sync::Arc; 3 | 4 | use egui::util::cache::{ComputerMut, FrameCache}; 5 | use egui::{vec2, Color32, Rect, Response, Sense, Stroke, StrokeKind, Ui, Vec2, Widget}; 6 | use emath::GuiRounding; 7 | 8 | use datamatrix::data::DataEncodingError; 9 | use datamatrix::placement::Bitmap; 10 | use datamatrix::{DataMatrix, SymbolList}; 11 | 12 | // ---------------------------------------------------------------------------- 13 | 14 | type DataMatrixCacheKey<'a> = &'a str; 15 | type DataMatrixCacheValue = Arc, DataEncodingError>>; 16 | 17 | #[derive(Default)] 18 | struct DataMatrixComputer; 19 | 20 | impl<'a> ComputerMut, DataMatrixCacheValue> for DataMatrixComputer { 21 | fn compute(&mut self, key: DataMatrixCacheKey) -> DataMatrixCacheValue { 22 | Arc::new( 23 | DataMatrix::encode_str(key, SymbolList::default()) 24 | .map(|datamatrix| datamatrix.bitmap()), 25 | ) 26 | } 27 | } 28 | 29 | type DataMatrixCache<'a> = FrameCache; 30 | 31 | // ---------------------------------------------------------------------------- 32 | 33 | #[must_use = "You should put this widget in an ui with `ui.add(widget);`"] 34 | pub struct DataMatrixWidget<'a> { 35 | value: &'a str, 36 | module_size: usize, 37 | quiet_zone: usize, 38 | foreground_color: Color32, 39 | background_color: Color32, 40 | } 41 | 42 | impl<'a> DataMatrixWidget<'a> { 43 | pub fn new(value: &'a str) -> Self { 44 | Self { 45 | value, 46 | module_size: 6, 47 | quiet_zone: 1, 48 | foreground_color: Color32::BLACK, 49 | background_color: Color32::WHITE, 50 | } 51 | } 52 | 53 | pub fn module_size(mut self, module_size: impl Into) -> Self { 54 | self.module_size = module_size.into(); 55 | self 56 | } 57 | 58 | pub fn quiet_zone(mut self, quiet_zone: impl Into) -> Self { 59 | self.quiet_zone = quiet_zone.into(); 60 | self 61 | } 62 | 63 | pub fn foreground_color(mut self, foreground_color: impl Into) -> Self { 64 | self.foreground_color = foreground_color.into(); 65 | self 66 | } 67 | 68 | pub fn background_color(mut self, background_color: impl Into) -> Self { 69 | self.background_color = background_color.into(); 70 | self 71 | } 72 | } 73 | 74 | impl<'a> Widget for DataMatrixWidget<'a> { 75 | fn ui(self, ui: &mut Ui) -> Response { 76 | let cached_bitmap = ui.memory_mut(|memory| { 77 | let cache = memory.caches.cache::>(); 78 | cache.get(self.value) 79 | }); 80 | 81 | if let Ok(bitmap) = cached_bitmap.borrow() { 82 | let module_size = self.module_size as f32 / ui.ctx().pixels_per_point(); 83 | 84 | let desired_size = vec2( 85 | (bitmap.width() + self.quiet_zone * 2) as f32, 86 | (bitmap.height() + self.quiet_zone * 2) as f32, 87 | ) * module_size; 88 | 89 | let (rect, response) = ui.allocate_exact_size(desired_size, Sense::hover()); 90 | 91 | if ui.is_rect_visible(rect) { 92 | ui.painter().rect( 93 | rect, 94 | ui.style().visuals.noninteractive().corner_radius, 95 | self.background_color, 96 | Stroke::NONE, 97 | StrokeKind::Middle, 98 | ); 99 | 100 | bitmap 101 | .pixels() 102 | .map(|(x, y)| { 103 | Rect::from_min_size( 104 | (rect.left_top() + Vec2::splat(self.quiet_zone as f32 * module_size)) 105 | .round_to_pixels(ui.pixels_per_point()) 106 | + vec2(x as f32, y as f32) * module_size, 107 | Vec2::splat(module_size), 108 | ) 109 | }) 110 | .for_each(|module_rect| { 111 | ui.painter() 112 | .rect_filled(module_rect, 0.0, self.foreground_color); 113 | }); 114 | } 115 | 116 | response 117 | } else { 118 | ui.colored_label( 119 | ui.style().visuals.error_fg_color, 120 | "\u{1F525} Failed to render data matrix code", 121 | ) 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /egui_extras_xt/src/barcodes/qrcode_widget.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Borrow; 2 | use std::sync::Arc; 3 | 4 | use egui::util::cache::{ComputerMut, FrameCache}; 5 | use egui::{vec2, Color32, Rect, Response, Sense, Stroke, StrokeKind, Ui, Vec2, Widget}; 6 | use emath::GuiRounding; 7 | 8 | use qrcode::{Color, QrCode, QrResult}; 9 | 10 | // ---------------------------------------------------------------------------- 11 | 12 | type QrCodeCacheKey<'a> = &'a str; 13 | type QrCodeCacheValue = Arc>; 14 | 15 | #[derive(Default)] 16 | struct QrCodeComputer; 17 | 18 | impl<'a> ComputerMut, QrCodeCacheValue> for QrCodeComputer { 19 | fn compute(&mut self, key: QrCodeCacheKey) -> QrCodeCacheValue { 20 | Arc::new(QrCode::new(key)) 21 | } 22 | } 23 | 24 | type QrCodeCache<'a> = FrameCache; 25 | 26 | // ---------------------------------------------------------------------------- 27 | 28 | #[must_use = "You should put this widget in an ui with `ui.add(widget);`"] 29 | pub struct QrCodeWidget<'a> { 30 | value: &'a str, 31 | module_size: usize, 32 | quiet_zone: usize, 33 | foreground_color: Color32, 34 | background_color: Color32, 35 | } 36 | 37 | impl<'a> QrCodeWidget<'a> { 38 | pub fn new(value: &'a str) -> Self { 39 | Self { 40 | value, 41 | module_size: 6, 42 | quiet_zone: 4, 43 | foreground_color: Color32::BLACK, 44 | background_color: Color32::WHITE, 45 | } 46 | } 47 | 48 | pub fn module_size(mut self, module_size: impl Into) -> Self { 49 | self.module_size = module_size.into(); 50 | self 51 | } 52 | 53 | pub fn quiet_zone(mut self, quiet_zone: impl Into) -> Self { 54 | self.quiet_zone = quiet_zone.into(); 55 | self 56 | } 57 | 58 | pub fn foreground_color(mut self, foreground_color: impl Into) -> Self { 59 | self.foreground_color = foreground_color.into(); 60 | self 61 | } 62 | 63 | pub fn background_color(mut self, background_color: impl Into) -> Self { 64 | self.background_color = background_color.into(); 65 | self 66 | } 67 | } 68 | 69 | impl<'a> Widget for QrCodeWidget<'a> { 70 | fn ui(self, ui: &mut Ui) -> Response { 71 | let cached_qr_code = ui.memory_mut(|memory| { 72 | let cache = memory.caches.cache::>(); 73 | cache.get(self.value) 74 | }); 75 | 76 | if let Ok(qr_code) = cached_qr_code.borrow() { 77 | let module_size = self.module_size as f32 / ui.ctx().pixels_per_point(); 78 | 79 | let desired_size = 80 | Vec2::splat((qr_code.width() + self.quiet_zone * 2) as f32 * module_size); 81 | 82 | let (rect, response) = ui.allocate_exact_size(desired_size, Sense::hover()); 83 | 84 | if ui.is_rect_visible(rect) { 85 | ui.painter().rect( 86 | rect, 87 | ui.style().visuals.noninteractive().corner_radius, 88 | self.background_color, 89 | Stroke::NONE, 90 | StrokeKind::Middle, 91 | ); 92 | 93 | qr_code 94 | .to_colors() 95 | .into_iter() 96 | .enumerate() 97 | .filter(|&(_module_index, module_value)| module_value == Color::Dark) 98 | .map(|(module_index, _module_value)| { 99 | ( 100 | module_index % qr_code.width(), 101 | module_index / qr_code.width(), 102 | ) 103 | }) 104 | .map(|(x, y)| { 105 | Rect::from_min_size( 106 | (rect.left_top() + Vec2::splat(self.quiet_zone as f32 * module_size)) 107 | .round_to_pixels(ui.pixels_per_point()) 108 | + vec2(x as f32, y as f32) * module_size, 109 | Vec2::splat(module_size), 110 | ) 111 | }) 112 | .for_each(|module_rect| { 113 | ui.painter() 114 | .rect_filled(module_rect, 0.0, self.foreground_color); 115 | }); 116 | } 117 | 118 | response 119 | } else { 120 | ui.colored_label( 121 | ui.style().visuals.error_fg_color, 122 | "\u{1F525} Failed to render QR code", 123 | ) 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /egui_extras_xt_example/src/bin/widget_gallery/pages/audio_knob_page.rs: -------------------------------------------------------------------------------- 1 | use std::ops::RangeInclusive; 2 | 3 | use eframe::egui::{DragValue, Grid, Ui}; 4 | use egui_extras_xt::common::{Orientation, WidgetShape, Winding}; 5 | use egui_extras_xt::knobs::AudioKnob; 6 | use egui_extras_xt::ui::drag_rangeinclusive::DragRangeInclusive; 7 | use egui_extras_xt::ui::optional_value_widget::OptionalValueWidget; 8 | use egui_extras_xt::ui::widgets_from_iter::SelectableValueFromIter; 9 | use strum::IntoEnumIterator; 10 | 11 | use crate::pages::ui::{widget_orientation_ui, widget_shape_ui}; 12 | use crate::pages::PageImpl; 13 | 14 | pub struct AudioKnobPage { 15 | value: f32, 16 | interactive: bool, 17 | diameter: f32, 18 | drag_length: f32, 19 | winding: Winding, 20 | orientation: Orientation, 21 | range: RangeInclusive, 22 | spread: f32, 23 | thickness: f32, 24 | shape: WidgetShape, 25 | animated: bool, 26 | snap: Option, 27 | shift_snap: Option, 28 | } 29 | 30 | impl Default for AudioKnobPage { 31 | fn default() -> AudioKnobPage { 32 | AudioKnobPage { 33 | value: 0.0, 34 | interactive: true, 35 | diameter: 32.0, 36 | drag_length: 1.0, 37 | orientation: Orientation::Top, 38 | winding: Winding::Clockwise, 39 | range: 0.0..=1.0, 40 | spread: 1.0, 41 | thickness: 0.66, 42 | shape: WidgetShape::Squircle(4.0), 43 | animated: true, 44 | snap: None, 45 | shift_snap: None, 46 | } 47 | } 48 | } 49 | 50 | impl PageImpl for AudioKnobPage { 51 | fn ui(&mut self, ui: &mut Ui) { 52 | ui.add( 53 | AudioKnob::new(&mut self.value) 54 | .interactive(self.interactive) 55 | .diameter(self.diameter) 56 | .drag_length(self.drag_length) 57 | .orientation(self.orientation) 58 | .winding(self.winding) 59 | .range(self.range.clone()) 60 | .spread(self.spread) 61 | .thickness(self.thickness) 62 | .shape(self.shape.clone()) 63 | .animated(self.animated) 64 | .snap(self.snap) 65 | .shift_snap(self.shift_snap), 66 | ); 67 | ui.separator(); 68 | 69 | Grid::new("audio_knob_properties") 70 | .num_columns(2) 71 | .spacing([20.0, 10.0]) 72 | .striped(true) 73 | .show(ui, |ui| { 74 | ui.label("Value"); 75 | ui.add(DragValue::new(&mut self.value)); 76 | ui.end_row(); 77 | 78 | ui.label("Interactive"); 79 | ui.checkbox(&mut self.interactive, ""); 80 | ui.end_row(); 81 | 82 | ui.label("Diameter"); 83 | ui.add(DragValue::new(&mut self.diameter)); 84 | ui.end_row(); 85 | 86 | ui.label("Drag length"); 87 | ui.add(DragValue::new(&mut self.drag_length)); 88 | ui.end_row(); 89 | 90 | ui.label("Winding"); 91 | ui.horizontal(|ui| { 92 | ui.selectable_value_from_iter(&mut self.winding, Winding::iter()); 93 | }); 94 | ui.end_row(); 95 | 96 | ui.label("Orientation"); 97 | widget_orientation_ui(ui, &mut self.orientation); 98 | ui.end_row(); 99 | 100 | ui.label("Range"); 101 | ui.drag_rangeinclusive(&mut self.range); 102 | ui.end_row(); 103 | 104 | ui.label("Spread"); 105 | ui.add(DragValue::new(&mut self.spread)); 106 | ui.end_row(); 107 | 108 | ui.label("Thickness"); 109 | ui.add(DragValue::new(&mut self.thickness)); 110 | ui.end_row(); 111 | 112 | ui.label("Shape"); 113 | widget_shape_ui(ui, &mut self.shape); 114 | ui.end_row(); 115 | 116 | ui.label("Animated"); 117 | ui.checkbox(&mut self.animated, ""); 118 | ui.end_row(); 119 | 120 | ui.label("Snap"); 121 | ui.optional_value_widget(&mut self.snap, |ui, value| ui.add(DragValue::new(value))); 122 | ui.end_row(); 123 | 124 | ui.label("Shift snap"); 125 | ui.optional_value_widget(&mut self.shift_snap, |ui, value| { 126 | ui.add(DragValue::new(value)) 127 | }); 128 | ui.end_row(); 129 | }); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /egui_extras_xt_example/src/bin/widget_gallery/pages/mod.rs: -------------------------------------------------------------------------------- 1 | mod ui; 2 | 3 | use eframe::egui::Ui; 4 | use strum::{Display, EnumIter, EnumProperty}; 5 | 6 | // ---------------------------------------------------------------------------- 7 | 8 | mod angle_knob_page; 9 | use angle_knob_page::AngleKnobPage; 10 | 11 | mod audio_knob_page; 12 | use audio_knob_page::AudioKnobPage; 13 | 14 | mod barcode_page; 15 | use barcode_page::BarcodePage; 16 | 17 | mod datamatrix_page; 18 | use datamatrix_page::DataMatrixPage; 19 | 20 | mod directory_tree_view_page; 21 | use directory_tree_view_page::DirectoryTreeViewPage; 22 | 23 | mod hyperlink_with_icon_page; 24 | use hyperlink_with_icon_page::HyperlinkWithIconPage; 25 | 26 | mod indicator_button_page; 27 | use indicator_button_page::IndicatorButtonPage; 28 | 29 | mod led_display_page; 30 | use led_display_page::LedDisplayPage; 31 | 32 | mod linear_compass_page; 33 | use linear_compass_page::LinearCompassPage; 34 | 35 | mod polar_compass_page; 36 | use polar_compass_page::PolarCompassPage; 37 | 38 | mod qrcode_page; 39 | use qrcode_page::QrCodePage; 40 | 41 | mod rotated_label_page; 42 | use rotated_label_page::RotatedLabelPage; 43 | 44 | mod segmented_display_page; 45 | use segmented_display_page::SegmentedDisplayPage; 46 | 47 | mod standard_buttons_page; 48 | use standard_buttons_page::StandardButtonsPage; 49 | 50 | mod thumbstick_widget_page; 51 | use thumbstick_widget_page::ThumbstickWidgetPage; 52 | 53 | mod welcome_page; 54 | use welcome_page::WelcomePage; 55 | 56 | // ---------------------------------------------------------------------------- 57 | 58 | pub trait PageImpl { 59 | fn ui(&mut self, ui: &mut Ui); 60 | } 61 | 62 | #[allow(clippy::enum_variant_names)] 63 | #[derive(Clone, Copy, Display, EnumIter, EnumProperty, Eq, Hash, PartialEq)] 64 | pub enum PageId { 65 | #[strum(to_string = "AngleKnob")] 66 | #[strum(props(feature = "knobs"))] 67 | AngleKnobPage, 68 | 69 | #[strum(to_string = "AudioKnob")] 70 | #[strum(props(feature = "knobs"))] 71 | AudioKnobPage, 72 | 73 | #[strum(to_string = "BarcodeWidget")] 74 | #[strum(props(feature = "barcodes"))] 75 | BarcodePage, 76 | 77 | #[strum(to_string = "DataMatrixWidget")] 78 | #[strum(props(feature = "barcodes"))] 79 | DataMatrixPage, 80 | 81 | #[strum(to_string = "DirectoryTreeView")] 82 | #[strum(props(feature = "filesystem"))] 83 | DirectoryTreeViewPage, 84 | 85 | #[strum(to_string = "HyperlinkWithIcon")] 86 | #[strum(props(feature = "ui"))] 87 | HyperlinkWithIconPage, 88 | 89 | #[strum(to_string = "IndicatorButton")] 90 | #[strum(props(feature = "displays"))] 91 | IndicatorButtonPage, 92 | 93 | #[strum(to_string = "LedDisplay")] 94 | #[strum(props(feature = "displays"))] 95 | LedDisplayPage, 96 | 97 | #[strum(to_string = "LinearCompass")] 98 | #[strum(props(feature = "compasses"))] 99 | LinearCompassPage, 100 | 101 | #[strum(to_string = "PolarCompass")] 102 | #[strum(props(feature = "compasses"))] 103 | PolarCompassPage, 104 | 105 | #[strum(to_string = "QrCodeWidget")] 106 | #[strum(props(feature = "barcodes"))] 107 | QrCodePage, 108 | 109 | #[strum(to_string = "RotatedLabel")] 110 | #[strum(props(feature = "ui"))] 111 | RotatedLabelPage, 112 | 113 | #[strum(to_string = "SegmentedDisplayWidget")] 114 | #[strum(props(feature = "displays"))] 115 | SegmentedDisplayPage, 116 | 117 | #[strum(to_string = "StandardButtons")] 118 | #[strum(props(feature = "ui"))] 119 | StandardButtonsPage, 120 | 121 | #[strum(to_string = "ThumbstickWidget")] 122 | #[strum(props(feature = "knobs"))] 123 | ThumbstickWidgetPage, 124 | 125 | #[strum(to_string = "Welcome")] 126 | WelcomePage, 127 | } 128 | 129 | impl PageId { 130 | pub fn create_page(&self) -> Box { 131 | match *self { 132 | PageId::AngleKnobPage => Box::::default(), 133 | PageId::AudioKnobPage => Box::::default(), 134 | PageId::BarcodePage => Box::::default(), 135 | PageId::DataMatrixPage => Box::::default(), 136 | PageId::DirectoryTreeViewPage => Box::::default(), 137 | PageId::HyperlinkWithIconPage => Box::::default(), 138 | PageId::IndicatorButtonPage => Box::::default(), 139 | PageId::LedDisplayPage => Box::::default(), 140 | PageId::LinearCompassPage => Box::::default(), 141 | PageId::PolarCompassPage => Box::::default(), 142 | PageId::QrCodePage => Box::::default(), 143 | PageId::RotatedLabelPage => Box::::default(), 144 | PageId::SegmentedDisplayPage => Box::::default(), 145 | PageId::StandardButtonsPage => Box::::default(), 146 | PageId::ThumbstickWidgetPage => Box::::default(), 147 | PageId::WelcomePage => Box::::default(), 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /egui_extras_xt/src/ui/hyperlink_with_icon.rs: -------------------------------------------------------------------------------- 1 | use egui::{Hyperlink, Response, Ui, Widget}; 2 | 3 | pub trait HyperlinkWithIcon { 4 | fn hyperlink_with_icon(&mut self, url: impl ToString) -> Response; 5 | fn hyperlink_with_icon_to(&mut self, label: impl ToString, url: impl ToString) -> Response; 6 | } 7 | 8 | #[rustfmt::skip] 9 | fn hyperlink_icon(url: &str) -> char { 10 | for &(u, icon) in &[ 11 | // Warnings 12 | ("ftp:", '\u{26A0}' ), 13 | ("http:", '\u{26A0}' ), 14 | ("telnet:", '\u{26A0}' ), 15 | 16 | // URI schemes 17 | ("appstream:", '\u{1F4E6}'), 18 | ("apt:", '\u{1F4E6}'), 19 | ("fax:", '\u{1F4E0}'), 20 | ("fb:", '\u{E604}' ), 21 | ("file:", '\u{1F5C1}'), 22 | ("flatpak:", '\u{1F4E6}'), 23 | ("gemini:", '\u{264A}' ), 24 | ("geo:", '\u{1F5FA}'), 25 | ("git:", '\u{E625}' ), 26 | ("info:", '\u{2139}' ), 27 | ("ipp:", '\u{1F5B6}'), 28 | ("ipps:", '\u{1F5B6}'), 29 | ("irc:", '\u{1F4AC}'), 30 | ("irc6:", '\u{1F4AC}'), 31 | ("ircs:", '\u{1F4AC}'), 32 | ("itms-apps:", '\u{F8FF}' ), 33 | ("ldap:", '\u{1F4D5}'), 34 | ("ldaps:", '\u{1F4D5}'), 35 | ("mailto:", '\u{1F4E7}'), 36 | ("maps:", '\u{1F5FA}'), 37 | ("market:", '\u{E618}' ), 38 | ("message:", '\u{1F4E7}'), 39 | ("ms-", '\u{E61F}' ), // Not a typo. 40 | ("nfs:", '\u{1F5C1}'), 41 | ("pkg:", '\u{1F4E6}'), 42 | ("rpm:", '\u{1F4E6}'), 43 | ("sftp:", '\u{1F5C1}'), 44 | ("sip:", '\u{1F4DE}'), 45 | ("sips:", '\u{1F4DE}'), 46 | ("skype:", '\u{E613}' ), 47 | ("smb:", '\u{1F5C1}'), 48 | ("sms:", '\u{2709}' ), 49 | ("snap:", '\u{1F4E6}'), 50 | ("ssh:", '\u{1F5A5}'), 51 | ("steam:", '\u{E623}' ), 52 | ("tel:", '\u{1F4DE}'), 53 | 54 | // Websites 55 | ("https://apps.apple.com/", '\u{F8FF}' ), 56 | ("https://crates.io/", '\u{1F4E6}'), 57 | ("https://docs.rs/", '\u{1F4DA}'), 58 | ("https://drive.google.com/", '\u{E62F}' ), 59 | ("https://play.google.com/store/apps/", '\u{E618}' ), 60 | ("https://soundcloud.com/", '\u{E627}' ), 61 | ("https://stackoverflow.com/", '\u{E601}' ), 62 | ("https://steamcommunity.com/", '\u{E623}' ), 63 | ("https://store.steampowered.com/", '\u{E623}' ), 64 | ("https://twitter.com/", '\u{E603}' ), 65 | ("https://vimeo.com/", '\u{E602}' ), 66 | ("https://www.dropbox.com/", '\u{E610}' ), 67 | ("https://www.facebook.com/", '\u{E604}' ), 68 | ("https://www.instagram.com/", '\u{E60F}' ), 69 | ("https://www.paypal.com/", '\u{E616}' ), 70 | ("https://www.youtube.com/", '\u{E636}' ), 71 | ("https://youtu.be/", '\u{E636}' ), 72 | 73 | // Generic git rules 74 | ("https://git.", '\u{E625}' ), 75 | ("https://cgit.", '\u{E625}' ), 76 | ("https://gitlab.", '\u{E625}' ), 77 | 78 | // Non-exhaustive list of some git instances not covered by the generic rules 79 | ("https://bitbucket.org/", '\u{E625}' ), 80 | ("https://code.qt.io/", '\u{E625}' ), 81 | ("https://code.videolan.org/", '\u{E625}' ), 82 | ("https://framagit.org/", '\u{E625}' ), 83 | ("https://gitee.com/", '\u{E625}' ), 84 | ("https://github.com/", '\u{E624}' ), 85 | ("https://invent.kde.org/", '\u{E625}' ), 86 | ("https://salsa.debian.org/", '\u{E625}' ), 87 | 88 | // Discord and friends have no symbols in the default emoji font. 89 | ] { 90 | if url.starts_with(u) { 91 | return icon; 92 | } 93 | } 94 | 95 | if url.starts_with("https://") { 96 | for &(u, icon) in &[ 97 | (".github.io/", '\u{E624}'), 98 | (".gitlab.io/", '\u{E625}'), 99 | (".reddit.com/", '\u{E628}'), 100 | ] { 101 | if url.contains(u) { 102 | return icon; 103 | } 104 | } 105 | 106 | // Generic web link 107 | return '\u{1F30D}'; 108 | } 109 | 110 | // Unknown link type 111 | '\u{2BA9}' 112 | } 113 | 114 | impl HyperlinkWithIcon for Ui { 115 | fn hyperlink_with_icon(&mut self, url: impl ToString) -> Response { 116 | Hyperlink::from_label_and_url( 117 | format!("{} {}", hyperlink_icon(&url.to_string()), url.to_string()), 118 | url, 119 | ) 120 | .ui(self) 121 | } 122 | 123 | fn hyperlink_with_icon_to(&mut self, label: impl ToString, url: impl ToString) -> Response { 124 | Hyperlink::from_label_and_url( 125 | format!("{} {}", hyperlink_icon(&url.to_string()), label.to_string()), 126 | url, 127 | ) 128 | .ui(self) 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /egui_extras_xt_example/src/bin/filesystem_widgets.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::OsStr; 2 | use std::path::{Path, PathBuf}; 3 | 4 | use eframe::egui::{self, Ui}; 5 | use egui_extras_xt::filesystem::{BreadcrumbBar, DirectoryTreeViewWidget}; 6 | 7 | struct FilesystemWidgetsExample { 8 | root_path: PathBuf, 9 | selected_path: Option, 10 | } 11 | 12 | impl Default for FilesystemWidgetsExample { 13 | fn default() -> Self { 14 | Self { 15 | root_path: PathBuf::from(".").canonicalize().unwrap(), 16 | selected_path: None, 17 | } 18 | } 19 | } 20 | 21 | impl FilesystemWidgetsExample { 22 | fn directory_filter(path: &Path) -> bool { 23 | !path 24 | .file_name() 25 | .and_then(OsStr::to_str) 26 | .unwrap() 27 | .starts_with('.') 28 | } 29 | 30 | fn file_extensions() -> &'static [&'static str] { 31 | &["rs", "toml"] 32 | } 33 | 34 | fn directory_context_menu_contents(ui: &mut Ui, path: &Path) { 35 | ui.strong("Directory context menu"); 36 | ui.label(path.to_str().unwrap()); 37 | } 38 | 39 | fn directory_context_menu_enabled(_path: &Path) -> bool { 40 | true 41 | } 42 | 43 | fn file_context_menu_contents(ui: &mut Ui, path: &Path) { 44 | ui.strong("File context menu"); 45 | ui.label(path.to_str().unwrap()); 46 | } 47 | 48 | fn file_context_menu_enabled(_path: &Path) -> bool { 49 | true 50 | } 51 | 52 | fn directory_hover_ui_contents(ui: &mut Ui, path: &Path) { 53 | ui.strong("Directory hover ui"); 54 | ui.label(path.to_str().unwrap()); 55 | } 56 | 57 | fn directory_hover_ui_enabled(_path: &Path) -> bool { 58 | true 59 | } 60 | 61 | fn file_hover_ui_contents(ui: &mut Ui, path: &Path) { 62 | ui.strong("File hover ui"); 63 | ui.label(path.to_str().unwrap()); 64 | } 65 | 66 | fn file_hover_ui_enabled(_path: &Path) -> bool { 67 | true 68 | } 69 | } 70 | 71 | impl eframe::App for FilesystemWidgetsExample { 72 | fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { 73 | egui::CentralPanel::default().show(ctx, |ui| { 74 | if let Some(selected_path) = &mut self.selected_path { 75 | let breadcrum_bar_response = ui.add( 76 | BreadcrumbBar::new(selected_path, &self.root_path) 77 | .directory_filter(Self::directory_filter) 78 | .file_extensions(Self::file_extensions()) 79 | .directory_context_menu( 80 | Self::directory_context_menu_contents, 81 | Self::directory_context_menu_enabled, 82 | ) 83 | .file_context_menu( 84 | Self::file_context_menu_contents, 85 | Self::file_context_menu_enabled, 86 | ) 87 | .directory_hover_ui( 88 | Self::directory_hover_ui_contents, 89 | Self::directory_hover_ui_enabled, 90 | ) 91 | .file_hover_ui(Self::file_hover_ui_contents, Self::file_hover_ui_enabled) 92 | .hide_file_extensions(false), 93 | ); 94 | ui.separator(); 95 | 96 | if breadcrum_bar_response.changed() { 97 | println!("New path selected: {:?}", selected_path); 98 | } 99 | } 100 | 101 | let directory_tree_response = ui.add( 102 | DirectoryTreeViewWidget::new(&mut self.selected_path, &self.root_path) 103 | .directory_filter(Self::directory_filter) 104 | .file_extensions(Self::file_extensions()) 105 | .directory_selectable(true) 106 | .file_selectable(true) 107 | .directory_context_menu( 108 | Self::directory_context_menu_contents, 109 | Self::directory_context_menu_enabled, 110 | ) 111 | .file_context_menu( 112 | Self::file_context_menu_contents, 113 | Self::file_context_menu_enabled, 114 | ) 115 | .directory_hover_ui( 116 | Self::directory_hover_ui_contents, 117 | Self::directory_hover_ui_enabled, 118 | ) 119 | .file_hover_ui(Self::file_hover_ui_contents, Self::file_hover_ui_enabled) 120 | .hide_file_extensions(false) 121 | .force_selected_open(false), 122 | ); 123 | 124 | if directory_tree_response.changed() { 125 | println!("New path selected: {:?}", self.selected_path); 126 | } 127 | }); 128 | } 129 | } 130 | 131 | fn main() -> Result<(), eframe::Error> { 132 | let options = eframe::NativeOptions { 133 | viewport: egui::ViewportBuilder::default().with_inner_size([320.0, 480.0]), 134 | ..Default::default() 135 | }; 136 | 137 | eframe::run_native( 138 | "Filesystem widgets example", 139 | options, 140 | Box::new(|_| Ok(Box::::default())), 141 | ) 142 | } 143 | -------------------------------------------------------------------------------- /egui_extras_xt_example/src/bin/compass_widgets.rs: -------------------------------------------------------------------------------- 1 | use eframe::egui::{self, global_theme_preference_switch}; 2 | 3 | use egui_extras_xt::compasses::{CompassMarker, CompassMarkerShape, LinearCompass, PolarCompass}; 4 | use egui_extras_xt::ui::standard_buttons::StandardButtons; 5 | 6 | struct GpsPosition(f32, f32); 7 | 8 | impl GpsPosition { 9 | fn from_degrees(lat: f32, lon: f32) -> Self { 10 | Self(lat.to_radians(), lon.to_radians()) 11 | } 12 | 13 | fn bearing_to(&self, target: &GpsPosition) -> f32 { 14 | let y = (target.1 - self.1).sin() * target.0.cos(); 15 | let x = self.0.cos() * target.0.sin() 16 | - self.0.sin() * target.0.cos() * (target.1 - self.1).cos(); 17 | y.atan2(x) 18 | } 19 | 20 | fn distance_to(&self, target: &GpsPosition) -> f32 { 21 | let a = ((target.0 - self.0) / 2.0).sin().powf(2.0) 22 | + self.0.cos() * target.0.cos() * ((target.1 - self.1) / 2.0).sin().powf(2.0); 23 | a.sqrt().atan2((1.0 - a).sqrt()) * 12734.0 24 | } 25 | } 26 | 27 | struct CompassWidgetsExample { 28 | heading: f32, 29 | gps_position: GpsPosition, 30 | targets: Vec<(GpsPosition, String)>, 31 | } 32 | 33 | impl Default for CompassWidgetsExample { 34 | fn default() -> Self { 35 | macro_rules! target { 36 | ($lat:expr, $lon:expr, $name:expr) => { 37 | (GpsPosition::from_degrees($lat, $lon), String::from($name)) 38 | }; 39 | } 40 | 41 | Self { 42 | heading: 0.0, 43 | gps_position: GpsPosition::from_degrees(47.0829, 17.9787), 44 | #[rustfmt::skip] 45 | targets: vec![ 46 | target!(47.2307, 16.6212, "Szombathely" ), 47 | target!(46.8331, 16.8469, "Zalaegerszeg" ), 48 | target!(47.6744, 17.6492, "Győr" ), 49 | target!(46.3536, 17.7968, "Kaposvár" ), 50 | target!(47.0942, 17.9065, "Veszprém" ), 51 | target!(46.0763, 18.2280, "Pécs" ), 52 | target!(47.5765, 18.3988, "Tatabánya" ), 53 | target!(47.1913, 18.4097, "Székesfehérvár"), 54 | target!(46.3495, 18.6990, "Szekszárd" ), 55 | target!(47.4979, 19.0402, "Budapest" ), 56 | target!(46.9080, 19.6931, "Kecskemét" ), 57 | target!(48.0999, 19.8049, "Salgótarján" ), 58 | target!(46.2507, 20.1516, "Szeged" ), 59 | target!(47.1769, 20.1843, "Szolnok" ), 60 | target!(47.9026, 20.3771, "Eger" ), 61 | target!(48.1032, 20.7779, "Miskolc" ), 62 | target!(46.6747, 21.0864, "Békéscsaba" ), 63 | target!(47.5314, 21.6242, "Debrecen" ), 64 | target!(47.9555, 21.7166, "Nyíregyháza" ), 65 | ], 66 | } 67 | } 68 | } 69 | 70 | impl eframe::App for CompassWidgetsExample { 71 | fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { 72 | egui::CentralPanel::default().show(ctx, |ui| { 73 | ui.horizontal(|ui| { 74 | global_theme_preference_switch(ui); 75 | ui.heading("Compass widgets example"); 76 | 77 | if ui.reset_button().clicked() { 78 | *self = Self::default(); 79 | } 80 | }); 81 | 82 | ui.separator(); 83 | 84 | ui.horizontal(|ui| { 85 | ui.drag_angle(&mut self.gps_position.0); 86 | ui.drag_angle(&mut self.gps_position.1); 87 | }); 88 | 89 | let markers = self 90 | .targets 91 | .iter() 92 | .map(|(target_gps_position, target_name)| { 93 | CompassMarker::new(self.gps_position.bearing_to(target_gps_position)) 94 | .distance(self.gps_position.distance_to(target_gps_position)) 95 | .label(target_name) 96 | }) 97 | .collect::>(); 98 | 99 | ui.add( 100 | PolarCompass::new(&mut self.heading) 101 | .interactive(true) 102 | .axis_labels(["N", "E", "S", "W"].into()) 103 | .markers(&markers) 104 | .diameter(512.0) 105 | .show_marker_labels(true) 106 | .show_marker_lines(true) 107 | .default_marker_shape(CompassMarkerShape::Star(5, 0.5)) 108 | .max_distance(1000.0), 109 | ); 110 | 111 | ui.add( 112 | LinearCompass::new(&mut self.heading) 113 | .interactive(true) 114 | .axis_labels(["N", "E", "S", "W"].into()) 115 | .width(512.0 + 24.0 * 2.0) 116 | .default_marker_shape(CompassMarkerShape::Star(5, 0.5)) 117 | .markers(&markers), 118 | ); 119 | }); 120 | } 121 | } 122 | 123 | fn main() -> Result<(), eframe::Error> { 124 | let options = eframe::NativeOptions { 125 | viewport: egui::ViewportBuilder::default().with_inner_size([580.0, 680.0]), 126 | ..Default::default() 127 | }; 128 | 129 | eframe::run_native( 130 | "Compass widgets example", 131 | options, 132 | Box::new(|_| Ok(Box::::default())), 133 | ) 134 | } 135 | -------------------------------------------------------------------------------- /egui_extras_xt_example/src/bin/widget_gallery/pages/angle_knob_page.rs: -------------------------------------------------------------------------------- 1 | use eframe::egui::{DragValue, Grid, Ui}; 2 | use egui_extras_xt::common::{Orientation, WidgetShape, Winding, WrapMode}; 3 | use egui_extras_xt::knobs::{AngleKnob, AngleKnobPreset}; 4 | use egui_extras_xt::ui::optional_value_widget::OptionalValueWidget; 5 | use egui_extras_xt::ui::standard_buttons::StandardButtons; 6 | use egui_extras_xt::ui::widgets_from_iter::{ComboBoxFromIter, SelectableValueFromIter}; 7 | use strum::IntoEnumIterator; 8 | 9 | use crate::pages::ui::{widget_orientation_ui, widget_shape_ui}; 10 | use crate::pages::PageImpl; 11 | 12 | pub struct AngleKnobPage { 13 | value: f32, 14 | interactive: bool, 15 | diameter: f32, 16 | preset: AngleKnobPreset, 17 | orientation: Orientation, 18 | winding: Winding, 19 | wrap: WrapMode, 20 | shape: WidgetShape, 21 | min: Option, 22 | max: Option, 23 | snap: Option, 24 | shift_snap: Option, 25 | animated: bool, 26 | show_axes: bool, 27 | axis_count: usize, 28 | } 29 | 30 | impl Default for AngleKnobPage { 31 | fn default() -> AngleKnobPage { 32 | AngleKnobPage { 33 | value: 0.0, 34 | preset: AngleKnobPreset::AdobePhotoshop, 35 | interactive: true, 36 | diameter: 32.0, 37 | orientation: Orientation::Top, 38 | winding: Winding::Clockwise, 39 | wrap: WrapMode::Unsigned, 40 | shape: WidgetShape::Circle, 41 | min: None, 42 | max: None, 43 | snap: None, 44 | shift_snap: Some(15.0f32.to_radians()), 45 | animated: false, 46 | show_axes: true, 47 | axis_count: 4, 48 | } 49 | } 50 | } 51 | 52 | impl PageImpl for AngleKnobPage { 53 | fn ui(&mut self, ui: &mut Ui) { 54 | ui.add( 55 | AngleKnob::new(&mut self.value) 56 | .interactive(self.interactive) 57 | .diameter(self.diameter) 58 | .orientation(self.orientation) 59 | .winding(self.winding) 60 | .shape(self.shape.clone()) 61 | .wrap(self.wrap) 62 | .min(self.min) 63 | .max(self.max) 64 | .snap(self.snap) 65 | .shift_snap(self.shift_snap) 66 | .animated(self.animated) 67 | .show_axes(self.show_axes) 68 | .axis_count(self.axis_count), 69 | ); 70 | ui.separator(); 71 | 72 | Grid::new("angle_knob_properties") 73 | .num_columns(2) 74 | .spacing([20.0, 10.0]) 75 | .striped(true) 76 | .show(ui, |ui| { 77 | ui.label("Value"); 78 | ui.drag_angle(&mut self.value); 79 | ui.end_row(); 80 | 81 | ui.label("Interactive"); 82 | ui.checkbox(&mut self.interactive, ""); 83 | ui.end_row(); 84 | 85 | ui.label("Diameter"); 86 | ui.add(DragValue::new(&mut self.diameter)); 87 | ui.end_row(); 88 | 89 | ui.label("Preset"); 90 | ui.horizontal(|ui| { 91 | if ui 92 | .combobox_from_iter("", &mut self.preset, AngleKnobPreset::iter()) 93 | .changed() 94 | { 95 | (self.orientation, self.winding, self.wrap) = self.preset.properties(); 96 | } 97 | 98 | if ui.reset_button().clicked() { 99 | (self.orientation, self.winding, self.wrap) = self.preset.properties(); 100 | } 101 | }); 102 | ui.end_row(); 103 | 104 | ui.label("Orientation"); 105 | widget_orientation_ui(ui, &mut self.orientation); 106 | ui.end_row(); 107 | 108 | ui.label("Winding"); 109 | ui.horizontal(|ui| { 110 | ui.selectable_value_from_iter(&mut self.winding, Winding::iter()); 111 | }); 112 | ui.end_row(); 113 | 114 | ui.label("Wrap"); 115 | ui.horizontal(|ui| { 116 | ui.selectable_value_from_iter(&mut self.wrap, WrapMode::iter()); 117 | }); 118 | ui.end_row(); 119 | 120 | ui.label("Shape"); 121 | widget_shape_ui(ui, &mut self.shape); 122 | ui.end_row(); 123 | 124 | ui.label("Minimum"); 125 | ui.optional_value_widget(&mut self.min, Ui::drag_angle); 126 | ui.end_row(); 127 | 128 | ui.label("Maximum"); 129 | ui.optional_value_widget(&mut self.max, Ui::drag_angle); 130 | ui.end_row(); 131 | 132 | ui.label("Snap"); 133 | ui.optional_value_widget(&mut self.snap, Ui::drag_angle); 134 | ui.end_row(); 135 | 136 | ui.label("Shift snap"); 137 | ui.optional_value_widget(&mut self.shift_snap, Ui::drag_angle); 138 | ui.end_row(); 139 | 140 | ui.label("Animated"); 141 | ui.checkbox(&mut self.animated, ""); 142 | ui.end_row(); 143 | 144 | ui.label("Show axes"); 145 | ui.checkbox(&mut self.show_axes, ""); 146 | ui.end_row(); 147 | 148 | ui.label("Axis count"); 149 | ui.add(DragValue::new(&mut self.axis_count)); 150 | ui.end_row(); 151 | }); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /egui_extras_xt/src/ui/standard_buttons.rs: -------------------------------------------------------------------------------- 1 | use egui::{Response, Ui}; 2 | use strum::{Display, EnumIter}; 3 | 4 | #[derive(Clone, Copy, Debug, EnumIter, PartialEq, Display)] 5 | pub enum ButtonKind { 6 | #[strum(to_string = "\u{2714} OK")] 7 | Ok, 8 | 9 | #[strum(to_string = "\u{1F6AB} Cancel")] 10 | Cancel, 11 | 12 | #[strum(to_string = "\u{2714} Apply")] 13 | Apply, 14 | 15 | #[strum(to_string = "\u{1F504} Reset")] 16 | Reset, 17 | 18 | #[strum(to_string = "\u{1F5C1} Open")] 19 | Open, 20 | 21 | #[strum(to_string = "\u{1F4BE} Save")] 22 | Save, 23 | 24 | #[strum(to_string = "\u{1F4BE} Save As...")] 25 | SaveAs, 26 | 27 | #[strum(to_string = "\u{1F5D9} Close")] 28 | Close, 29 | 30 | #[strum(to_string = "\u{1F5D1} Delete")] 31 | Delete, 32 | 33 | #[strum(to_string = "\u{25B6} Play")] 34 | Play, 35 | 36 | #[strum(to_string = "\u{23F8} Pause")] 37 | Pause, 38 | 39 | #[strum(to_string = "\u{23F9} Stop")] 40 | Stop, 41 | 42 | #[strum(to_string = "\u{23FA} Record")] 43 | Record, 44 | 45 | #[strum(to_string = "\u{23ED} Next")] 46 | Next, 47 | 48 | #[strum(to_string = "\u{23EE} Previous")] 49 | Previous, 50 | 51 | #[strum(to_string = "\u{26F6} Full Screen")] 52 | FullScreen, 53 | 54 | #[strum(to_string = "\u{1F3B2} Random")] 55 | Random, 56 | 57 | #[strum(to_string = "\u{270F} Edit")] 58 | Edit, 59 | 60 | #[strum(to_string = "\u{2605} Favorite")] 61 | Favorite, 62 | 63 | #[strum(to_string = "\u{2606} Unfavorite")] 64 | Unfavorite, 65 | 66 | #[strum(to_string = "\u{1F507} Mute")] 67 | Mute, 68 | 69 | #[strum(to_string = "\u{1F50A} Unmute")] 70 | Unmute, 71 | 72 | #[strum(to_string = "\u{1F512} Lock")] 73 | Lock, 74 | 75 | #[strum(to_string = "\u{1F513} Unlock")] 76 | Unlock, 77 | 78 | #[strum(to_string = "\u{1F503} Refresh")] 79 | Refresh, 80 | 81 | #[strum(to_string = "\u{1F5CB} New")] 82 | New, 83 | 84 | #[strum(to_string = "\u{1F5D0} Copy")] 85 | Copy, 86 | 87 | #[strum(to_string = "\u{1F4CB} Paste")] 88 | Paste, 89 | 90 | #[strum(to_string = "\u{2702} Cut")] 91 | Cut, 92 | } 93 | 94 | pub trait StandardButtons { 95 | fn standard_button(&mut self, button_kind: ButtonKind) -> Response; 96 | 97 | fn ok_button(&mut self) -> Response { 98 | self.standard_button(ButtonKind::Ok) 99 | } 100 | 101 | fn cancel_button(&mut self) -> Response { 102 | self.standard_button(ButtonKind::Cancel) 103 | } 104 | 105 | fn apply_button(&mut self) -> Response { 106 | self.standard_button(ButtonKind::Apply) 107 | } 108 | 109 | fn reset_button(&mut self) -> Response { 110 | self.standard_button(ButtonKind::Reset) 111 | } 112 | 113 | fn open_button(&mut self) -> Response { 114 | self.standard_button(ButtonKind::Open) 115 | } 116 | 117 | fn save_button(&mut self) -> Response { 118 | self.standard_button(ButtonKind::Save) 119 | } 120 | 121 | fn save_as_button(&mut self) -> Response { 122 | self.standard_button(ButtonKind::SaveAs) 123 | } 124 | 125 | fn close_button(&mut self) -> Response { 126 | self.standard_button(ButtonKind::Close) 127 | } 128 | 129 | fn delete_button(&mut self) -> Response { 130 | self.standard_button(ButtonKind::Delete) 131 | } 132 | 133 | fn play_button(&mut self) -> Response { 134 | self.standard_button(ButtonKind::Play) 135 | } 136 | 137 | fn pause_button(&mut self) -> Response { 138 | self.standard_button(ButtonKind::Pause) 139 | } 140 | 141 | fn stop_button(&mut self) -> Response { 142 | self.standard_button(ButtonKind::Stop) 143 | } 144 | 145 | fn record_button(&mut self) -> Response { 146 | self.standard_button(ButtonKind::Record) 147 | } 148 | 149 | fn next_button(&mut self) -> Response { 150 | self.standard_button(ButtonKind::Next) 151 | } 152 | 153 | fn previous_button(&mut self) -> Response { 154 | self.standard_button(ButtonKind::Previous) 155 | } 156 | 157 | fn full_screen_button(&mut self) -> Response { 158 | self.standard_button(ButtonKind::FullScreen) 159 | } 160 | 161 | fn random_button(&mut self) -> Response { 162 | self.standard_button(ButtonKind::Random) 163 | } 164 | 165 | fn edit_button(&mut self) -> Response { 166 | self.standard_button(ButtonKind::Edit) 167 | } 168 | 169 | fn favorite_button(&mut self) -> Response { 170 | self.standard_button(ButtonKind::Favorite) 171 | } 172 | 173 | fn unfavorite_button(&mut self) -> Response { 174 | self.standard_button(ButtonKind::Unfavorite) 175 | } 176 | 177 | fn mute_button(&mut self) -> Response { 178 | self.standard_button(ButtonKind::Mute) 179 | } 180 | 181 | fn unmute_button(&mut self) -> Response { 182 | self.standard_button(ButtonKind::Unmute) 183 | } 184 | 185 | fn lock_button(&mut self) -> Response { 186 | self.standard_button(ButtonKind::Lock) 187 | } 188 | 189 | fn unlock_button(&mut self) -> Response { 190 | self.standard_button(ButtonKind::Unlock) 191 | } 192 | 193 | fn refresh_button(&mut self) -> Response { 194 | self.standard_button(ButtonKind::Refresh) 195 | } 196 | 197 | fn new_button(&mut self) -> Response { 198 | self.standard_button(ButtonKind::New) 199 | } 200 | 201 | fn copy_button(&mut self) -> Response { 202 | self.standard_button(ButtonKind::Copy) 203 | } 204 | 205 | fn paste_button(&mut self) -> Response { 206 | self.standard_button(ButtonKind::Paste) 207 | } 208 | 209 | fn cut_button(&mut self) -> Response { 210 | self.standard_button(ButtonKind::Cut) 211 | } 212 | } 213 | 214 | impl StandardButtons for Ui { 215 | fn standard_button(&mut self, button_kind: ButtonKind) -> Response { 216 | self.button(button_kind.to_string()) 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /egui_extras_xt/src/ui/about_window.rs: -------------------------------------------------------------------------------- 1 | use egui::{Context, Vec2, Window}; 2 | 3 | use crate::ui::hyperlink_with_icon::HyperlinkWithIcon; 4 | 5 | // ---------------------------------------------------------------------------- 6 | 7 | #[doc(hidden)] 8 | pub struct PackageInfo { 9 | pub name: &'static str, 10 | pub version: &'static str, 11 | pub authors: &'static str, 12 | pub description: Option<&'static str>, 13 | pub homepage: Option<&'static str>, 14 | pub repository: Option<&'static str>, 15 | pub license: Option<&'static str>, 16 | pub license_file: Option<&'static str>, 17 | } 18 | 19 | #[doc(hidden)] 20 | #[macro_export] 21 | macro_rules! package_info { 22 | () => {{ 23 | macro_rules! option_env_some { 24 | ( $x:expr ) => { 25 | match option_env!($x) { 26 | Some("") => None, 27 | opt => opt, 28 | } 29 | }; 30 | } 31 | 32 | $crate::ui::about_window::PackageInfo { 33 | name: env!("CARGO_PKG_NAME"), 34 | version: env!("CARGO_PKG_VERSION"), 35 | authors: env!("CARGO_PKG_AUTHORS"), 36 | description: option_env_some!("CARGO_PKG_DESCRIPTION"), 37 | homepage: option_env_some!("CARGO_PKG_HOMEPAGE"), 38 | repository: option_env_some!("CARGO_PKG_REPOSITORY"), 39 | license: option_env_some!("CARGO_PKG_LICENSE"), 40 | license_file: option_env_some!("CARGO_PKG_LICENSE_FILE"), 41 | } 42 | }}; 43 | } 44 | 45 | impl PackageInfo { 46 | fn authors(&self) -> impl Iterator)> { 47 | self.authors.split(':').map(|author_line| { 48 | let author_parts = author_line 49 | .split(|c| ['<', '>'].contains(&c)) 50 | .map(str::trim) 51 | .collect::>(); 52 | (author_parts[0], author_parts.get(1).copied()) 53 | }) 54 | } 55 | } 56 | 57 | // ---------------------------------------------------------------------------- 58 | 59 | #[macro_export] 60 | macro_rules! show_about_window { 61 | ($ctx:expr, $open:expr) => {{ 62 | $crate::ui::about_window::show_about_window_impl($ctx, $open, &$crate::package_info!()); 63 | }}; 64 | } 65 | 66 | #[doc(hidden)] 67 | pub fn show_about_window_impl(ctx: &Context, open: &mut bool, package_info: &PackageInfo) { 68 | Window::new("About") 69 | .open(open) 70 | .resizable(false) 71 | .collapsible(false) 72 | .show(ctx, |ui| { 73 | ui.heading(package_info.name); 74 | ui.label(format!("Version {}", package_info.version)); 75 | 76 | ui.separator(); 77 | 78 | if let Some(description) = package_info.description { 79 | ui.label(description); 80 | ui.separator(); 81 | } 82 | 83 | ui.horizontal(|ui| { 84 | if let Some(homepage) = package_info.homepage { 85 | ui.hyperlink_with_icon_to("Home page", homepage); 86 | } 87 | 88 | if let Some(repository) = package_info.repository { 89 | ui.hyperlink_with_icon_to("Repository", repository); 90 | } 91 | }); 92 | 93 | ui.separator(); 94 | 95 | ui.collapsing("Authors", |ui| { 96 | ui.horizontal(|ui| { 97 | for (author_name, author_email) in package_info.authors() { 98 | if let Some(author_email) = author_email { 99 | if !["noreply@", "no-reply@", "@users.noreply."] 100 | .iter() 101 | .any(|no_reply| author_email.contains(no_reply)) 102 | { 103 | ui.hyperlink_with_icon_to( 104 | author_name, 105 | format!("mailto:{author_email:}"), 106 | ); 107 | } else { 108 | ui.label(format!("\u{1F464} {author_name:}")); 109 | } 110 | } else { 111 | ui.label(format!("\u{1F464} {author_name:}")); 112 | } 113 | } 114 | }); 115 | 116 | // (!) Rust incremental compilation bug: 117 | // When the 'license' field is changed in the crate's Cargo.toml, 118 | // source files that include that field through `env!()` macros 119 | // are not picked up for recompilation. 120 | // Always do `cargo clean` + full rebuild when changing Cargo.toml metadata. 121 | if let Some(license) = package_info.license { 122 | ui.separator(); 123 | ui.horizontal(|ui| { 124 | ui.spacing_mut().item_spacing = Vec2::splat(0.0); 125 | ui.label("License: "); 126 | 127 | license.split_whitespace().for_each(|s| match s { 128 | operator @ ("OR" | "AND" | "WITH") => { 129 | ui.label(format!(" {} ", operator.to_lowercase())); 130 | } 131 | license => { 132 | ui.hyperlink_with_icon_to( 133 | license, 134 | format!("https://spdx.org/licenses/{license:}.html"), 135 | ); 136 | } 137 | }); 138 | }); 139 | }; 140 | 141 | if let Some(license_file) = package_info.license_file { 142 | ui.separator(); 143 | ui.label(format!( 144 | "License: See the {license_file:} file for details." 145 | )); 146 | }; 147 | }); 148 | }); 149 | } 150 | -------------------------------------------------------------------------------- /egui_extras_xt_example/src/bin/widget_gallery/main.rs: -------------------------------------------------------------------------------- 1 | mod pages; 2 | 3 | use std::collections::HashMap; 4 | 5 | use eframe::egui::panel::Side; 6 | use eframe::egui::{self, TextEdit}; 7 | 8 | use egui_extras_xt::show_about_window; 9 | use egui_extras_xt::ui::standard_buttons::StandardButtons; 10 | 11 | use itertools::Itertools; 12 | use strum::{EnumProperty, IntoEnumIterator}; 13 | 14 | use pages::{PageId, PageImpl}; 15 | 16 | struct WidgetGallery { 17 | // Pages 18 | current_page: PageId, 19 | pages: HashMap>, 20 | search_query: String, 21 | 22 | // Sub-windows 23 | settings_window_open: bool, 24 | inspector_window_open: bool, 25 | memory_window_open: bool, 26 | about_window_open: bool, 27 | } 28 | 29 | impl Default for WidgetGallery { 30 | fn default() -> Self { 31 | Self { 32 | // Pages 33 | current_page: PageId::WelcomePage, 34 | pages: HashMap::from_iter( 35 | PageId::iter().map(|page_id| (page_id, page_id.create_page())), 36 | ), 37 | search_query: String::new(), 38 | 39 | // Sub-windows 40 | settings_window_open: false, 41 | inspector_window_open: false, 42 | memory_window_open: false, 43 | about_window_open: false, 44 | } 45 | } 46 | } 47 | 48 | impl eframe::App for WidgetGallery { 49 | fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { 50 | egui::TopBottomPanel::top("mainmenu").show(ctx, |ui| { 51 | ui.horizontal(|ui| { 52 | ui.menu_button("Debug", |ui| { 53 | ui.checkbox(&mut self.settings_window_open, "\u{1F527} Settings"); 54 | ui.checkbox(&mut self.inspector_window_open, "\u{1F50D} Inspection"); 55 | ui.checkbox(&mut self.memory_window_open, "\u{1F4DD} Memory"); 56 | }); 57 | 58 | if ui.button("About").clicked() { 59 | self.about_window_open = true; 60 | } 61 | }); 62 | }); 63 | 64 | // egui layout bug: SidePanel width gets progressively fucked when dragging 65 | // the main window between screens with different PPI. 66 | // SidePanel resizing is also fucked, it's mirroring mouse movements along 67 | // along the left edge of the window (SidePanel `.abs()` bug). 68 | egui::SidePanel::new(Side::Left, "sidepanel").show(ctx, |ui| { 69 | ui.horizontal(|ui| { 70 | ui.add( 71 | TextEdit::singleline(&mut self.search_query) 72 | .desired_width(150.0) 73 | .hint_text("\u{1F50D} Search..."), 74 | ); 75 | ui.add_enabled_ui(!self.search_query.is_empty(), |ui| { 76 | // Default font doesn't have "\u{232B}" 77 | if ui.button("\u{1F5D9}").clicked() { 78 | self.search_query.clear(); 79 | } 80 | }); 81 | }); 82 | ui.separator(); 83 | 84 | egui::ScrollArea::vertical().show(ui, |ui| { 85 | PageId::iter() 86 | .map(|page_id| (page_id, page_id.get_str("feature"))) 87 | .filter(|(page_id, _feature)| { 88 | let q = self.search_query.to_lowercase(); 89 | page_id.to_string().to_lowercase().contains(&q) 90 | }) 91 | .sorted_by_key(|(_page_id, feature)| *feature) 92 | .chunk_by(|(_page_id, feature)| *feature) 93 | .into_iter() 94 | .for_each(|(feature, pages)| { 95 | if let Some(feature) = feature { 96 | ui.label(format!("\u{1F4E6} {feature:}")); 97 | } 98 | pages 99 | .sorted_by_key(|(page_id, _feature)| page_id.to_string()) 100 | .for_each(|(page_id, _feature)| { 101 | ui.selectable_value( 102 | &mut self.current_page, 103 | page_id, 104 | page_id.to_string(), 105 | ); 106 | }); 107 | 108 | ui.separator(); 109 | }); 110 | }); 111 | }); 112 | 113 | egui::CentralPanel::default().show(ctx, |ui| { 114 | ui.heading(self.current_page.to_string()); 115 | if let Some(feature) = self.current_page.get_str("feature") { 116 | ui.label(format!("\u{1F4E6} {feature:}")) 117 | .on_hover_text("Cargo feature"); 118 | } 119 | ui.separator(); 120 | 121 | egui::ScrollArea::both().show(ui, |ui| { 122 | self.pages 123 | .get_mut(&self.current_page) 124 | .expect("failed to get page") 125 | .ui(ui); 126 | 127 | ui.separator(); 128 | 129 | if ui.reset_button().clicked() { 130 | self.pages 131 | .insert(self.current_page, self.current_page.create_page()); 132 | } 133 | }); 134 | }); 135 | 136 | egui::Window::new("\u{1F527} Settings") 137 | .open(&mut self.settings_window_open) 138 | .vscroll(true) 139 | .show(ctx, |ui| { 140 | ctx.settings_ui(ui); 141 | }); 142 | 143 | egui::Window::new("\u{1F50D} Inspection") 144 | .open(&mut self.inspector_window_open) 145 | .vscroll(true) 146 | .show(ctx, |ui| { 147 | ctx.inspection_ui(ui); 148 | }); 149 | 150 | egui::Window::new("\u{1F4DD} Memory") 151 | .open(&mut self.memory_window_open) 152 | .resizable(false) 153 | .show(ctx, |ui| { 154 | ctx.memory_ui(ui); 155 | }); 156 | 157 | show_about_window!(ctx, &mut self.about_window_open); 158 | } 159 | } 160 | 161 | fn main() -> Result<(), eframe::Error> { 162 | let options = eframe::NativeOptions { 163 | viewport: egui::ViewportBuilder::default().with_inner_size([800.0, 600.0]), 164 | ..Default::default() 165 | }; 166 | 167 | eframe::run_native( 168 | "Widget Gallery", 169 | options, 170 | Box::new(|_| Ok(Box::::default())), 171 | ) 172 | } 173 | -------------------------------------------------------------------------------- /egui_extras_xt_example/src/bin/delorean_time_circuits.rs: -------------------------------------------------------------------------------- 1 | use eframe::egui; 2 | 3 | use egui_extras_xt::displays::{DisplayStylePreset, LedDisplay, SegmentedDisplayWidget}; 4 | 5 | struct DateTime(String, usize, usize, bool, usize, usize); 6 | 7 | struct TimeCircuitSegment { 8 | label: String, 9 | datetime: DateTime, 10 | style_preset: DisplayStylePreset, 11 | } 12 | 13 | struct TimeCircuitsExample { 14 | time_circuit_segments: Vec, 15 | } 16 | 17 | impl Default for TimeCircuitsExample { 18 | fn default() -> Self { 19 | Self { 20 | time_circuit_segments: vec![ 21 | TimeCircuitSegment { 22 | label: "DESTINATION TIME".to_owned(), 23 | datetime: DateTime("JAN".to_owned(), 1, 1885, true, 12, 0), 24 | style_preset: DisplayStylePreset::DeLoreanRed, 25 | }, 26 | TimeCircuitSegment { 27 | label: "PRESENT TIME".to_owned(), 28 | datetime: DateTime("NOV".to_owned(), 12, 1955, false, 9, 28), 29 | style_preset: DisplayStylePreset::DeLoreanGreen, 30 | }, 31 | TimeCircuitSegment { 32 | label: "LAST TIME DEPARTED".to_owned(), 33 | datetime: DateTime("OCT".to_owned(), 27, 1985, true, 2, 42), 34 | style_preset: DisplayStylePreset::DeLoreanAmber, 35 | }, 36 | ], 37 | } 38 | } 39 | } 40 | 41 | impl eframe::App for TimeCircuitsExample { 42 | fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { 43 | egui::CentralPanel::default().show(ctx, |ui| { 44 | for TimeCircuitSegment { 45 | label, 46 | datetime: DateTime(month, day, year, ampm, hour, minute), 47 | style_preset, 48 | } in &self.time_circuit_segments 49 | { 50 | ui.group(|ui| { 51 | egui::Grid::new(label).min_col_width(20.0).show(ui, |ui| { 52 | ui.vertical_centered(|ui| ui.label("MONTH")); 53 | ui.vertical_centered(|ui| ui.label("DAY")); 54 | ui.vertical_centered(|ui| ui.label("YEAR")); 55 | ui.vertical_centered(|ui| ui.label("")); 56 | ui.vertical_centered(|ui| ui.label("HOUR")); 57 | ui.vertical_centered(|ui| ui.label("")); 58 | ui.vertical_centered(|ui| ui.label("MIN")); 59 | ui.end_row(); 60 | 61 | ui.add( 62 | SegmentedDisplayWidget::sixteen_segment(month) 63 | .style_preset(*style_preset) 64 | .show_dots(false) 65 | .show_colons(false) 66 | .show_apostrophes(false) 67 | .digit_height(64.0), 68 | ); 69 | ui.add( 70 | SegmentedDisplayWidget::seven_segment(format!("{day:02}")) 71 | .style_preset(*style_preset) 72 | .show_dots(true) 73 | .show_colons(false) 74 | .show_apostrophes(false) 75 | .digit_height(64.0), 76 | ); 77 | ui.add( 78 | SegmentedDisplayWidget::seven_segment(format!("{year:04}")) 79 | .style_preset(*style_preset) 80 | .show_dots(true) 81 | .show_colons(false) 82 | .show_apostrophes(false) 83 | .digit_height(64.0), 84 | ); 85 | 86 | ui.vertical_centered(|ui| { 87 | ui.label("AM"); 88 | ui.add( 89 | LedDisplay::from_bool(!ampm) 90 | .style_preset(*style_preset) 91 | .diameter(12.0), 92 | ); 93 | ui.label("PM"); 94 | ui.add( 95 | LedDisplay::from_bool(*ampm) 96 | .style_preset(*style_preset) 97 | .diameter(12.0), 98 | ); 99 | }); 100 | 101 | ui.add( 102 | SegmentedDisplayWidget::seven_segment(format!("{hour:02}")) 103 | .style_preset(*style_preset) 104 | .show_dots(true) 105 | .show_colons(false) 106 | .show_apostrophes(false) 107 | .digit_height(64.0), 108 | ); 109 | 110 | ui.vertical_centered(|ui| { 111 | ui.add_space(15.0); 112 | ui.add( 113 | LedDisplay::from_bool(true) 114 | .style_preset(*style_preset) 115 | .diameter(12.0), 116 | ); 117 | ui.add_space(10.0); 118 | ui.add( 119 | LedDisplay::from_bool(true) 120 | .style_preset(*style_preset) 121 | .diameter(12.0), 122 | ); 123 | }); 124 | 125 | ui.add( 126 | SegmentedDisplayWidget::seven_segment(format!("{minute:02}")) 127 | .style_preset(*style_preset) 128 | .show_dots(true) 129 | .show_colons(false) 130 | .show_apostrophes(false) 131 | .digit_height(64.0), 132 | ); 133 | ui.end_row(); 134 | }); 135 | 136 | ui.shrink_width_to_current(); 137 | ui.vertical_centered(|ui| { 138 | ui.heading(label); 139 | }); 140 | }); 141 | } 142 | }); 143 | } 144 | } 145 | 146 | fn main() -> Result<(), eframe::Error> { 147 | let options = eframe::NativeOptions { 148 | viewport: egui::ViewportBuilder::default().with_inner_size([878.0, 422.0]), 149 | ..Default::default() 150 | }; 151 | 152 | eframe::run_native( 153 | "DeLorean Time Circuits", 154 | options, 155 | Box::new(|_| Ok(Box::::default())), 156 | ) 157 | } 158 | -------------------------------------------------------------------------------- /egui_extras_xt/src/knobs/audio_knob.rs: -------------------------------------------------------------------------------- 1 | use std::f32::consts::TAU; 2 | use std::ops::RangeInclusive; 3 | 4 | use egui::{self, Response, Sense, Ui, Widget}; 5 | use emath::{remap_clamp, Vec2}; 6 | 7 | use crate::common::{Orientation, WidgetShape, Winding}; 8 | 9 | // ---------------------------------------------------------------------------- 10 | 11 | /// Combined into one function (rather than two) to make it easier 12 | /// for the borrow checker. 13 | type GetSetValue<'a> = Box) -> f32>; 14 | 15 | fn get(get_set_value: &mut GetSetValue<'_>) -> f32 { 16 | (get_set_value)(None) 17 | } 18 | 19 | fn set(get_set_value: &mut GetSetValue<'_>, value: f32) { 20 | (get_set_value)(Some(value)); 21 | } 22 | 23 | // ---------------------------------------------------------------------------- 24 | 25 | #[must_use = "You should put this widget in an ui with `ui.add(widget);`"] 26 | pub struct AudioKnob<'a> { 27 | get_set_value: GetSetValue<'a>, 28 | interactive: bool, 29 | diameter: f32, 30 | drag_length: f32, 31 | winding: Winding, 32 | orientation: Orientation, 33 | range: RangeInclusive, 34 | spread: f32, 35 | thickness: f32, 36 | shape: WidgetShape, 37 | animated: bool, 38 | snap: Option, 39 | shift_snap: Option, 40 | } 41 | 42 | impl<'a> AudioKnob<'a> { 43 | pub fn new(value: &'a mut f32) -> Self { 44 | Self::from_get_set(move |v: Option| { 45 | if let Some(v) = v { 46 | *value = v; 47 | } 48 | *value 49 | }) 50 | } 51 | 52 | pub fn from_get_set(get_set_value: impl 'a + FnMut(Option) -> f32) -> Self { 53 | Self { 54 | get_set_value: Box::new(get_set_value), 55 | interactive: true, 56 | diameter: 32.0, 57 | drag_length: 1.0, 58 | orientation: Orientation::Top, 59 | winding: Winding::Clockwise, 60 | range: 0.0..=1.0, 61 | spread: 1.0, 62 | thickness: 0.66, 63 | shape: WidgetShape::Squircle(4.0), 64 | animated: true, 65 | snap: None, 66 | shift_snap: None, 67 | } 68 | } 69 | 70 | pub fn interactive(mut self, interactive: bool) -> Self { 71 | self.interactive = interactive; 72 | self 73 | } 74 | 75 | pub fn diameter(mut self, diameter: impl Into) -> Self { 76 | self.diameter = diameter.into(); 77 | self 78 | } 79 | 80 | pub fn drag_length(mut self, drag_length: impl Into) -> Self { 81 | self.drag_length = drag_length.into(); 82 | self 83 | } 84 | 85 | pub fn winding(mut self, winding: Winding) -> Self { 86 | self.winding = winding; 87 | self 88 | } 89 | 90 | pub fn orientation(mut self, orientation: Orientation) -> Self { 91 | self.orientation = orientation; 92 | self 93 | } 94 | 95 | pub fn range(mut self, range: RangeInclusive) -> Self { 96 | self.range = range; 97 | self 98 | } 99 | 100 | pub fn spread(mut self, spread: impl Into) -> Self { 101 | self.spread = spread.into(); 102 | self 103 | } 104 | 105 | pub fn thickness(mut self, thickness: impl Into) -> Self { 106 | self.thickness = thickness.into(); 107 | self 108 | } 109 | 110 | pub fn shape(mut self, shape: WidgetShape) -> Self { 111 | self.shape = shape; 112 | self 113 | } 114 | 115 | pub fn animated(mut self, animated: bool) -> Self { 116 | self.animated = animated; 117 | self 118 | } 119 | 120 | pub fn snap(mut self, snap: Option) -> Self { 121 | self.snap = snap; 122 | self 123 | } 124 | 125 | pub fn shift_snap(mut self, shift_snap: Option) -> Self { 126 | self.shift_snap = shift_snap; 127 | self 128 | } 129 | } 130 | 131 | impl<'a> Widget for AudioKnob<'a> { 132 | fn ui(mut self, ui: &mut Ui) -> Response { 133 | let desired_size = Vec2::splat(self.diameter); 134 | 135 | let (rect, mut response) = ui.allocate_exact_size( 136 | desired_size, 137 | if self.interactive { 138 | Sense::click_and_drag() 139 | } else { 140 | Sense::hover() 141 | }, 142 | ); 143 | 144 | let constrain_value = |value: f32| value.clamp(*self.range.start(), *self.range.end()); 145 | 146 | if response.dragged() { 147 | let drag_delta = self.orientation.rot2().inverse() * response.drag_delta(); 148 | 149 | let mut new_value = get(&mut self.get_set_value); 150 | 151 | let delta = drag_delta.x + drag_delta.y * self.winding.to_float(); 152 | new_value += delta * (self.range.end() - self.range.start()) 153 | / (self.diameter * self.drag_length); 154 | 155 | set(&mut self.get_set_value, constrain_value(new_value)); 156 | response.mark_changed(); 157 | } 158 | 159 | if response.drag_stopped() { 160 | if self.animated { 161 | ui.ctx().clear_animations(); 162 | ui.ctx().animate_value_with_time( 163 | response.id, 164 | get(&mut self.get_set_value), 165 | ui.style().animation_time, 166 | ); 167 | } 168 | 169 | if let Some(snap_angle) = if ui.input(|input| input.modifiers.shift_only()) { 170 | self.shift_snap 171 | } else { 172 | self.snap 173 | } { 174 | assert!( 175 | snap_angle > 0.0, 176 | "non-positive snap angles are not supported" 177 | ); 178 | let new_value = (get(&mut self.get_set_value) / snap_angle).round() * snap_angle; 179 | set(&mut self.get_set_value, constrain_value(new_value)); 180 | response.mark_changed(); 181 | } 182 | } 183 | 184 | if ui.is_rect_visible(rect) { 185 | let visuals = *ui.style().interact(&response); 186 | 187 | let value = if self.animated && !response.dragged() { 188 | ui.ctx() 189 | .animate_value_with_time(response.id, get(&mut self.get_set_value), 0.1) 190 | } else { 191 | get(&mut self.get_set_value) 192 | }; 193 | 194 | let center_angle = (self.orientation.rot2() * Vec2::RIGHT).angle(); 195 | let spread_angle = (TAU / 2.0) * self.spread.clamp(0.0, 1.0); 196 | 197 | let (min_angle, max_angle) = ( 198 | center_angle - spread_angle * self.winding.to_float(), 199 | center_angle + spread_angle * self.winding.to_float(), 200 | ); 201 | 202 | let outer_radius = self.diameter / 2.0; 203 | let inner_radius = outer_radius * (1.0 - self.thickness.clamp(0.0, 1.0)); 204 | 205 | self.shape.paint_arc( 206 | ui, 207 | rect.center(), 208 | inner_radius, 209 | outer_radius, 210 | min_angle, 211 | max_angle, 212 | ui.style().visuals.faint_bg_color, 213 | ui.style().visuals.window_stroke(), 214 | self.orientation.rot2(), 215 | ); 216 | 217 | self.shape.paint_arc( 218 | ui, 219 | rect.center(), 220 | (inner_radius - visuals.expansion).max(0.0), 221 | outer_radius + visuals.expansion, 222 | remap_clamp(0.0, self.range.clone(), min_angle..=max_angle), 223 | remap_clamp(value, self.range, min_angle..=max_angle), 224 | visuals.bg_fill, 225 | visuals.fg_stroke, 226 | self.orientation.rot2(), 227 | ); 228 | } 229 | 230 | response 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /egui_extras_xt/src/displays/indicator_button.rs: -------------------------------------------------------------------------------- 1 | use egui::{ 2 | vec2, Align2, FontFamily, FontId, Key, Rect, Response, Sense, Stroke, StrokeKind, Ui, Widget, 3 | }; 4 | use strum::{Display, EnumIter}; 5 | 6 | use crate::displays::{DisplayStyle, DisplayStylePreset}; 7 | 8 | // ---------------------------------------------------------------------------- 9 | 10 | /// Combined into one function (rather than two) to make it easier 11 | /// for the borrow checker. 12 | type GetSetValue<'a> = Box) -> bool>; 13 | 14 | fn get(get_set_value: &mut GetSetValue<'_>) -> bool { 15 | (get_set_value)(None) 16 | } 17 | 18 | fn set(get_set_value: &mut GetSetValue<'_>, value: bool) { 19 | (get_set_value)(Some(value)); 20 | } 21 | 22 | // ---------------------------------------------------------------------------- 23 | 24 | #[non_exhaustive] 25 | #[derive(Clone, Copy, Display, EnumIter, Eq, PartialEq)] 26 | pub enum IndicatorButtonBehavior { 27 | #[strum(to_string = "Toggle")] 28 | Toggle, 29 | 30 | #[strum(to_string = "Hold")] 31 | Hold, 32 | } 33 | 34 | // ---------------------------------------------------------------------------- 35 | 36 | #[must_use = "You should put this widget in an ui with `ui.add(widget);`"] 37 | pub struct IndicatorButton<'a> { 38 | get_set_value: GetSetValue<'a>, 39 | width: f32, 40 | height: f32, 41 | label: Option, 42 | style: DisplayStyle, 43 | animated: bool, 44 | interactive: bool, 45 | margin: f32, 46 | behavior: IndicatorButtonBehavior, 47 | } 48 | 49 | impl<'a> IndicatorButton<'a> { 50 | pub fn new(value: &'a mut bool) -> Self { 51 | Self::from_get_set(move |v: Option| { 52 | if let Some(v) = v { 53 | *value = v; 54 | } 55 | *value 56 | }) 57 | } 58 | 59 | pub fn toggle(value: &'a mut bool) -> Self { 60 | Self::new(value).behavior(IndicatorButtonBehavior::Toggle) 61 | } 62 | 63 | pub fn hold(value: &'a mut bool) -> Self { 64 | Self::new(value).behavior(IndicatorButtonBehavior::Hold) 65 | } 66 | 67 | pub fn from_get_set(get_set_value: impl 'a + FnMut(Option) -> bool) -> Self { 68 | Self { 69 | get_set_value: Box::new(get_set_value), 70 | width: 64.0, 71 | height: 40.0, 72 | label: None, 73 | style: DisplayStylePreset::Default.style(), 74 | animated: true, 75 | interactive: true, 76 | margin: 0.2, 77 | behavior: IndicatorButtonBehavior::Toggle, 78 | } 79 | } 80 | 81 | pub fn width(mut self, width: impl Into) -> Self { 82 | self.width = width.into(); 83 | self 84 | } 85 | 86 | pub fn height(mut self, height: impl Into) -> Self { 87 | self.height = height.into(); 88 | self 89 | } 90 | 91 | pub fn label(mut self, label: impl ToString) -> Self { 92 | self.label = Some(label.to_string()); 93 | self 94 | } 95 | 96 | pub fn style(mut self, style: DisplayStyle) -> Self { 97 | self.style = style; 98 | self 99 | } 100 | 101 | pub fn style_preset(mut self, preset: DisplayStylePreset) -> Self { 102 | self.style = preset.style(); 103 | self 104 | } 105 | 106 | pub fn animated(mut self, animated: bool) -> Self { 107 | self.animated = animated; 108 | self 109 | } 110 | 111 | pub fn interactive(mut self, interactive: bool) -> Self { 112 | self.interactive = interactive; 113 | self 114 | } 115 | 116 | pub fn margin(mut self, margin: impl Into) -> Self { 117 | self.margin = margin.into(); 118 | self 119 | } 120 | 121 | pub fn behavior(mut self, behavior: IndicatorButtonBehavior) -> Self { 122 | self.behavior = behavior; 123 | self 124 | } 125 | } 126 | 127 | impl<'a> Widget for IndicatorButton<'a> { 128 | fn ui(mut self, ui: &mut Ui) -> Response { 129 | let desired_size = vec2(self.width, self.height); 130 | 131 | let (rect, mut response) = ui.allocate_exact_size( 132 | desired_size, 133 | if self.interactive { 134 | Sense::click_and_drag() 135 | } else { 136 | Sense::hover() 137 | }, 138 | ); 139 | 140 | match self.behavior { 141 | IndicatorButtonBehavior::Toggle => { 142 | if response.clicked() { 143 | let value = get(&mut self.get_set_value); 144 | set(&mut self.get_set_value, !value); 145 | 146 | response.mark_changed(); 147 | } 148 | } 149 | IndicatorButtonBehavior::Hold => { 150 | if response.drag_started() || response.drag_stopped() { 151 | set(&mut self.get_set_value, response.dragged()); 152 | response.mark_changed(); 153 | } 154 | 155 | if response.has_focus() { 156 | if ui.ctx().input(|input| input.key_pressed(Key::Enter)) 157 | || ui.ctx().input(|input| input.key_pressed(Key::Space)) 158 | { 159 | set(&mut self.get_set_value, true); 160 | response.mark_changed(); 161 | } 162 | 163 | if ui.ctx().input(|input| input.key_released(Key::Enter)) 164 | || ui.ctx().input(|input| input.key_released(Key::Space)) 165 | { 166 | set(&mut self.get_set_value, false); 167 | response.mark_changed(); 168 | } 169 | } 170 | } 171 | } 172 | 173 | if ui.is_rect_visible(rect) { 174 | let visuals = *ui.style().interact(&response); 175 | 176 | let value = if self.animated { 177 | ui.ctx() 178 | .animate_bool(response.id, get(&mut self.get_set_value)) 179 | } else { 180 | #[allow(clippy::collapsible_else_if)] 181 | if get(&mut self.get_set_value) { 182 | 1.0 183 | } else { 184 | 0.0 185 | } 186 | }; 187 | 188 | ui.painter().rect( 189 | rect, 190 | visuals.corner_radius, 191 | visuals.bg_fill, 192 | visuals.bg_stroke, 193 | StrokeKind::Middle, 194 | ); 195 | 196 | let top_rect = Rect::from_min_max(rect.left_top(), rect.right_center()); 197 | let bottom_rect = Rect::from_min_max(rect.left_center(), rect.right_bottom()); 198 | 199 | let margin = (self.height / 2.0) * self.margin; 200 | 201 | { 202 | let indicator_rect = if self.label.is_some() { top_rect } else { rect }; 203 | 204 | ui.painter().rect( 205 | indicator_rect.shrink(margin), 206 | 4.0, 207 | self.style.background_color, 208 | Stroke::NONE, 209 | StrokeKind::Middle, 210 | ); 211 | 212 | ui.painter().rect( 213 | indicator_rect.shrink(margin + 2.0), 214 | 4.0, 215 | self.style.foreground_color_blend(value), 216 | Stroke::NONE, 217 | StrokeKind::Middle, 218 | ); 219 | } 220 | 221 | if let Some(label) = self.label { 222 | ui.painter().text( 223 | bottom_rect.center() - vec2(0.0, margin / 2.0), 224 | Align2::CENTER_CENTER, 225 | label, 226 | FontId::new(bottom_rect.height() - margin, FontFamily::Proportional), 227 | visuals.text_color(), 228 | ); 229 | } 230 | } 231 | 232 | response 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /egui_extras_xt/src/filesystem/breadcrumb_bar.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::OsStr; 2 | use std::path::{Path, PathBuf}; 3 | 4 | use egui::{Label, Response, Sense, Ui, Widget}; 5 | use itertools::Itertools; 6 | 7 | use crate::filesystem::path_symbol::PathSymbol; 8 | use crate::filesystem::{DirectoryContextMenu, DirectoryFilter, DirectoryHoverUi}; 9 | 10 | // ---------------------------------------------------------------------------- 11 | 12 | #[must_use = "You should put this widget in an ui with `ui.add(widget);`"] 13 | pub struct BreadcrumbBar<'a> { 14 | selected_path: &'a mut PathBuf, 15 | root_directory: &'a Path, 16 | hide_file_extensions: bool, 17 | allow_navigation: bool, 18 | 19 | file_filter: Option>, 20 | file_context_menu: Option>, 21 | file_hover_ui: Option>, 22 | 23 | directory_filter: Option>, 24 | directory_context_menu: Option>, 25 | directory_hover_ui: Option>, 26 | } 27 | 28 | impl<'a> BreadcrumbBar<'a> { 29 | pub fn new(selected_path: &'a mut PathBuf, root_directory: &'a Path) -> Self { 30 | Self { 31 | selected_path, 32 | root_directory, 33 | hide_file_extensions: false, 34 | allow_navigation: true, 35 | 36 | file_filter: None, 37 | file_context_menu: None, 38 | file_hover_ui: None, 39 | 40 | directory_filter: None, 41 | directory_context_menu: None, 42 | directory_hover_ui: None, 43 | } 44 | } 45 | 46 | pub fn hide_file_extensions(mut self, hide_file_extensions: bool) -> Self { 47 | self.hide_file_extensions = hide_file_extensions; 48 | self 49 | } 50 | 51 | pub fn allow_navigation(mut self, allow_navigation: bool) -> Self { 52 | self.allow_navigation = allow_navigation; 53 | self 54 | } 55 | 56 | pub fn file_extensions(self, file_extensions: &'a [&'a str]) -> Self { 57 | self.file_filter(|path| { 58 | if let Some(file_extension) = path 59 | .extension() 60 | .and_then(OsStr::to_str) 61 | .map(str::to_lowercase) 62 | { 63 | file_extensions.contains(&file_extension.as_str()) 64 | } else { 65 | false 66 | } 67 | }) 68 | } 69 | 70 | pub fn file_filter(mut self, filter: impl Fn(&Path) -> bool + 'a) -> Self { 71 | self.file_filter = Some(Box::new(filter)); 72 | self 73 | } 74 | 75 | pub fn file_context_menu( 76 | mut self, 77 | add_contents: impl Fn(&mut Ui, &Path) + 'a, 78 | enabled: impl Fn(&Path) -> bool + 'a, 79 | ) -> Self { 80 | self.file_context_menu = Some((Box::new(add_contents), Box::new(enabled))); 81 | self 82 | } 83 | 84 | pub fn file_hover_ui( 85 | mut self, 86 | add_contents: impl Fn(&mut Ui, &Path) + 'a, 87 | enabled: impl Fn(&Path) -> bool + 'a, 88 | ) -> Self { 89 | self.file_hover_ui = Some((Box::new(add_contents), Box::new(enabled))); 90 | self 91 | } 92 | 93 | pub fn directory_filter(mut self, filter: impl Fn(&Path) -> bool + 'a) -> Self { 94 | self.directory_filter = Some(Box::new(filter)); 95 | self 96 | } 97 | 98 | pub fn directory_context_menu( 99 | mut self, 100 | add_contents: impl Fn(&mut Ui, &Path) + 'a, 101 | enabled: impl Fn(&Path) -> bool + 'a, 102 | ) -> Self { 103 | self.directory_context_menu = Some((Box::new(add_contents), Box::new(enabled))); 104 | self 105 | } 106 | 107 | pub fn directory_hover_ui( 108 | mut self, 109 | add_contents: impl Fn(&mut Ui, &Path) + 'a, 110 | enabled: impl Fn(&Path) -> bool + 'a, 111 | ) -> Self { 112 | self.directory_hover_ui = Some((Box::new(add_contents), Box::new(enabled))); 113 | self 114 | } 115 | } 116 | 117 | // ---------------------------------------------------------------------------- 118 | 119 | impl<'a> Widget for BreadcrumbBar<'a> { 120 | fn ui(self, ui: &mut Ui) -> Response { 121 | ui.horizontal(|ui| { 122 | let path_cloned = self.selected_path.clone(); 123 | let components = path_cloned.components().collect_vec(); 124 | 125 | for (path_prefix_index, path_prefix) in (0..components.len()) 126 | .map(|n| components[..=n].iter()) 127 | .map(PathBuf::from_iter) 128 | .enumerate() 129 | .filter(|(_, path_prefix)| path_prefix.starts_with(self.root_directory)) 130 | { 131 | let component_label = { 132 | let component_symbol = path_prefix.symbol(); 133 | let component_name = path_prefix 134 | .file_name() 135 | .map(OsStr::to_string_lossy) 136 | .unwrap_or_default(); 137 | format!("{component_symbol} {component_name}") 138 | }; 139 | 140 | let mut response = ui.add( 141 | Label::new(component_label) 142 | .selectable(false) 143 | .sense(Sense::click()), 144 | ); 145 | 146 | if path_prefix.is_dir() { 147 | if let Some((hover_ui_contents, hover_ui_enabled)) = &self.directory_hover_ui { 148 | if hover_ui_enabled(&path_prefix) { 149 | response = 150 | response.on_hover_ui(|ui| hover_ui_contents(ui, &path_prefix)); 151 | } 152 | } 153 | 154 | if let Some((context_menu_contents, context_menu_enabled)) = 155 | &self.directory_context_menu 156 | { 157 | if let Some(resp) = response.context_menu(|ui| { 158 | let context_menu_enabled = context_menu_enabled(&path_prefix); 159 | 160 | if context_menu_enabled { 161 | context_menu_contents(ui, &path_prefix); 162 | } 163 | 164 | if self.allow_navigation { 165 | if context_menu_enabled { 166 | ui.separator(); 167 | } 168 | 169 | if ui 170 | .button(format!("Contents of {:?}", path_prefix)) 171 | .clicked() 172 | { 173 | ui.close_menu(); 174 | } 175 | } 176 | }) { 177 | response = resp.response; 178 | } 179 | } 180 | } else { 181 | if let Some((hover_ui_contents, hover_ui_enabled)) = &self.file_hover_ui { 182 | if hover_ui_enabled(&path_prefix) { 183 | response = 184 | response.on_hover_ui(|ui| hover_ui_contents(ui, &path_prefix)); 185 | } 186 | } 187 | 188 | if let Some((context_menu_contents, context_menu_enabled)) = 189 | &self.file_context_menu 190 | { 191 | if context_menu_enabled(&path_prefix) { 192 | if let Some(resp) = 193 | response.context_menu(|ui| context_menu_contents(ui, &path_prefix)) 194 | { 195 | response = resp.response; 196 | } 197 | } 198 | } 199 | } 200 | 201 | if response.clicked() && self.allow_navigation { 202 | *self.selected_path = path_prefix.clone(); 203 | response.mark_changed(); 204 | } 205 | 206 | if path_prefix_index < components.len() - 1 { 207 | ui.add(Label::new("\u{23F5}")); 208 | } 209 | } 210 | }); 211 | 212 | // TODO: response 213 | ui.scope(|_| {}).response 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /egui_extras_xt_example/src/bin/ui_extensions.rs: -------------------------------------------------------------------------------- 1 | use eframe::egui::{self, DragValue, Response}; 2 | 3 | use strum::{Display, EnumIter, IntoEnumIterator}; 4 | 5 | use egui_extras_xt::ui::optional_value_widget::OptionalValueWidget; 6 | use egui_extras_xt::ui::widgets_from_iter::{ 7 | ComboBoxFromIter, RadioValueFromIter, SelectableValueFromIter, 8 | }; 9 | use egui_extras_xt::ui::widgets_from_slice::{ 10 | ComboBoxFromSlice, RadioValueFromSlice, SelectableValueFromSlice, 11 | }; 12 | 13 | #[derive(Clone, Copy, Display, EnumIter, PartialEq)] 14 | enum SevenSecretWeapons { 15 | #[strum(to_string = "Missile")] 16 | Missile, 17 | 18 | #[strum(to_string = "Metal detector")] 19 | MetalDetector, 20 | 21 | #[strum(to_string = "Fishing pole")] 22 | FishingPole, 23 | 24 | #[strum(to_string = "Mr. Analysis")] 25 | MrAnalysis, 26 | 27 | #[strum(to_string = "Magnet")] 28 | Magnet, 29 | 30 | #[strum(to_string = "Bug sweeper")] 31 | BugSweeper, 32 | } 33 | 34 | struct UiExtensionsExample { 35 | optional_usize: Option, 36 | optional_string: Option, 37 | secret_weapon: SevenSecretWeapons, 38 | coffee_count: usize, 39 | } 40 | 41 | impl Default for UiExtensionsExample { 42 | fn default() -> Self { 43 | Self { 44 | optional_usize: Some(1234), 45 | optional_string: Some("Test".to_owned()), 46 | secret_weapon: SevenSecretWeapons::MetalDetector, 47 | coffee_count: 1, 48 | } 49 | } 50 | } 51 | 52 | impl eframe::App for UiExtensionsExample { 53 | fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { 54 | fn debug_print_response(widget_name: &'static str, response: Response) { 55 | if response.changed() { 56 | println!("{widget_name:} changed"); 57 | } 58 | } 59 | 60 | egui::CentralPanel::default().show(ctx, |ui| { 61 | ui.push_id("optional_value", |ui| { 62 | ui.group(|ui| { 63 | debug_print_response( 64 | "optional_value_widget", 65 | ui.optional_value_widget(&mut self.optional_usize, |ui, value| { 66 | ui.add(DragValue::new(value)) 67 | }), 68 | ); 69 | 70 | ui.separator(); 71 | 72 | debug_print_response( 73 | "optional_value_widget", 74 | ui.optional_value_widget(&mut self.optional_string, |ui, value| { 75 | ui.text_edit_singleline(value) 76 | }), 77 | ); 78 | }); 79 | }); 80 | ui.add_space(16.0); 81 | 82 | ui.push_id("from_iter", |ui| { 83 | ui.group(|ui| { 84 | ui.horizontal_wrapped(|ui| { 85 | debug_print_response( 86 | "selectable_value_from_iter", 87 | ui.selectable_value_from_iter( 88 | &mut self.secret_weapon, 89 | SevenSecretWeapons::iter(), 90 | ), 91 | ); 92 | }); 93 | 94 | ui.separator(); 95 | 96 | ui.horizontal_wrapped(|ui| { 97 | debug_print_response( 98 | "combobox_from_iter", 99 | ui.combobox_from_iter( 100 | "Secret weapon", 101 | &mut self.secret_weapon, 102 | SevenSecretWeapons::iter(), 103 | ), 104 | ); 105 | }); 106 | 107 | ui.separator(); 108 | 109 | ui.horizontal_wrapped(|ui| { 110 | debug_print_response( 111 | "radio_value_from_iter", 112 | ui.radio_value_from_iter( 113 | &mut self.secret_weapon, 114 | SevenSecretWeapons::iter(), 115 | ), 116 | ); 117 | }); 118 | }); 119 | }); 120 | ui.add_space(16.0); 121 | 122 | ui.push_id("from_slice", |ui| { 123 | ui.group(|ui| { 124 | ui.horizontal_wrapped(|ui| { 125 | debug_print_response( 126 | "selectable_value_from_slice", 127 | ui.selectable_value_from_slice( 128 | &mut self.secret_weapon, 129 | &[ 130 | SevenSecretWeapons::Missile, 131 | SevenSecretWeapons::MetalDetector, 132 | SevenSecretWeapons::FishingPole, 133 | SevenSecretWeapons::MrAnalysis, 134 | SevenSecretWeapons::Magnet, 135 | SevenSecretWeapons::BugSweeper, 136 | ], 137 | ), 138 | ); 139 | }); 140 | 141 | ui.separator(); 142 | 143 | ui.horizontal_wrapped(|ui| { 144 | debug_print_response( 145 | "combobox_from_slice", 146 | ui.combobox_from_slice( 147 | "Secret weapon", 148 | &mut self.secret_weapon, 149 | &[ 150 | SevenSecretWeapons::Missile, 151 | SevenSecretWeapons::MetalDetector, 152 | SevenSecretWeapons::FishingPole, 153 | SevenSecretWeapons::MrAnalysis, 154 | SevenSecretWeapons::Magnet, 155 | SevenSecretWeapons::BugSweeper, 156 | ], 157 | ), 158 | ); 159 | }); 160 | 161 | ui.separator(); 162 | 163 | ui.horizontal_wrapped(|ui| { 164 | debug_print_response( 165 | "radio_value_from_slice", 166 | ui.radio_value_from_slice( 167 | &mut self.secret_weapon, 168 | &[ 169 | SevenSecretWeapons::Missile, 170 | SevenSecretWeapons::MetalDetector, 171 | SevenSecretWeapons::FishingPole, 172 | SevenSecretWeapons::MrAnalysis, 173 | SevenSecretWeapons::Magnet, 174 | SevenSecretWeapons::BugSweeper, 175 | ], 176 | ), 177 | ); 178 | }); 179 | }); 180 | }); 181 | ui.add_space(16.0); 182 | 183 | ui.push_id("from_iter_range", |ui| { 184 | ui.group(|ui| { 185 | ui.horizontal_wrapped(|ui| { 186 | debug_print_response( 187 | "selectable_value_from_iter", 188 | ui.selectable_value_from_iter(&mut self.coffee_count, 1..=17), 189 | ); 190 | }); 191 | 192 | ui.separator(); 193 | 194 | ui.horizontal_wrapped(|ui| { 195 | debug_print_response( 196 | "combobox_from_iter", 197 | ui.combobox_from_iter("Coffee count", &mut self.coffee_count, 1..=17), 198 | ); 199 | }); 200 | 201 | ui.separator(); 202 | 203 | ui.horizontal_wrapped(|ui| { 204 | debug_print_response( 205 | "radio_value_from_iter", 206 | ui.radio_value_from_iter(&mut self.coffee_count, 1..=17), 207 | ); 208 | }); 209 | }); 210 | }); 211 | ui.add_space(16.0); 212 | }); 213 | } 214 | } 215 | 216 | fn main() -> Result<(), eframe::Error> { 217 | let options = eframe::NativeOptions { 218 | viewport: egui::ViewportBuilder::default().with_inner_size([640.0, 480.0]), 219 | ..Default::default() 220 | }; 221 | 222 | eframe::run_native( 223 | "Ui extensions", 224 | options, 225 | Box::new(|_| Ok(Box::::default())), 226 | ) 227 | } 228 | -------------------------------------------------------------------------------- /egui_extras_xt/src/displays/display_style.rs: -------------------------------------------------------------------------------- 1 | use egui::{lerp, Color32, Rgba, Stroke, Ui}; 2 | use strum::{Display, EnumIter}; 3 | 4 | // ---------------------------------------------------------------------------- 5 | 6 | #[derive(Clone, Copy, Debug)] 7 | pub struct DisplayStyle { 8 | pub background_color: Color32, 9 | pub active_foreground_color: Color32, 10 | pub active_foreground_stroke: Stroke, 11 | pub inactive_foreground_color: Color32, 12 | pub inactive_foreground_stroke: Stroke, 13 | } 14 | 15 | impl DisplayStyle { 16 | #[must_use] 17 | pub fn foreground_color(&self, active: bool) -> Color32 { 18 | if active { 19 | self.active_foreground_color 20 | } else { 21 | self.inactive_foreground_color 22 | } 23 | } 24 | 25 | #[must_use] 26 | pub fn foreground_stroke(&self, active: bool) -> Stroke { 27 | if active { 28 | self.active_foreground_stroke 29 | } else { 30 | self.inactive_foreground_stroke 31 | } 32 | } 33 | 34 | #[must_use] 35 | pub fn foreground_color_blend(&self, value: f32) -> Color32 { 36 | Color32::from(lerp( 37 | Rgba::from(self.inactive_foreground_color)..=Rgba::from(self.active_foreground_color), 38 | value, 39 | )) 40 | } 41 | 42 | #[must_use] 43 | pub fn foreground_stroke_blend(&self, value: f32) -> Stroke { 44 | Stroke::new( 45 | lerp( 46 | self.inactive_foreground_stroke.width..=self.active_foreground_stroke.width, 47 | value, 48 | ), 49 | Color32::from(lerp( 50 | Rgba::from(self.inactive_foreground_stroke.color) 51 | ..=Rgba::from(self.active_foreground_stroke.color), 52 | value, 53 | )), 54 | ) 55 | } 56 | 57 | #[must_use] 58 | pub fn system_style(ui: &Ui) -> Self { 59 | DisplayStyle { 60 | background_color: Color32::TRANSPARENT, 61 | active_foreground_color: ui.style().visuals.text_color(), 62 | active_foreground_stroke: Stroke::NONE, 63 | inactive_foreground_color: ui.style().visuals.faint_bg_color, 64 | inactive_foreground_stroke: Stroke::NONE, 65 | } 66 | } 67 | } 68 | 69 | impl Default for DisplayStyle { 70 | fn default() -> Self { 71 | DisplayStylePreset::Default.style() 72 | } 73 | } 74 | 75 | // ---------------------------------------------------------------------------- 76 | 77 | #[non_exhaustive] 78 | #[derive(Clone, Copy, Debug, Display, EnumIter, Eq, PartialEq)] 79 | pub enum DisplayStylePreset { 80 | #[strum(to_string = "Default")] 81 | Default, 82 | 83 | #[strum(to_string = "Calculator")] 84 | Calculator, 85 | 86 | #[strum(to_string = "Nintendo Game Boy")] 87 | NintendoGameBoy, 88 | 89 | #[strum(to_string = "Knight Rider")] 90 | KnightRider, 91 | 92 | #[strum(to_string = "Blue Negative")] 93 | BlueNegative, 94 | 95 | #[strum(to_string = "Amber")] 96 | Amber, 97 | 98 | #[strum(to_string = "Light Blue")] 99 | LightBlue, 100 | 101 | #[strum(to_string = "DeLorean Red")] 102 | DeLoreanRed, 103 | 104 | #[strum(to_string = "DeLorean Green")] 105 | DeLoreanGreen, 106 | 107 | #[strum(to_string = "DeLorean Amber")] 108 | DeLoreanAmber, 109 | 110 | #[strum(to_string = "Yamaha MU2000")] 111 | YamahaMU2000, 112 | 113 | #[strum(to_string = "Dracula")] 114 | Dracula, 115 | } 116 | 117 | impl DisplayStylePreset { 118 | #[must_use] 119 | pub fn style(&self) -> DisplayStyle { 120 | match *self { 121 | DisplayStylePreset::Default => DisplayStyle { 122 | background_color: Color32::from_rgb(0x00, 0x20, 0x00), 123 | active_foreground_color: Color32::from_rgb(0x00, 0xF0, 0x00), 124 | active_foreground_stroke: Stroke::NONE, 125 | inactive_foreground_color: Color32::from_rgb(0x00, 0x30, 0x00), 126 | inactive_foreground_stroke: Stroke::NONE, 127 | }, 128 | DisplayStylePreset::Calculator => DisplayStyle { 129 | background_color: Color32::from_rgb(0xC5, 0xCB, 0xB6), 130 | active_foreground_color: Color32::from_rgb(0x00, 0x00, 0x00), 131 | active_foreground_stroke: Stroke::NONE, 132 | inactive_foreground_color: Color32::from_rgb(0xB9, 0xBE, 0xAB), 133 | inactive_foreground_stroke: Stroke::NONE, 134 | }, 135 | DisplayStylePreset::NintendoGameBoy => DisplayStyle { 136 | background_color: Color32::from_rgb(0x9B, 0xBC, 0x0F), 137 | active_foreground_color: Color32::from_rgb(0x0F, 0x38, 0x0F), 138 | active_foreground_stroke: Stroke::NONE, 139 | inactive_foreground_color: Color32::from_rgb(0x8B, 0xAC, 0x0F), 140 | inactive_foreground_stroke: Stroke::NONE, 141 | }, 142 | DisplayStylePreset::KnightRider => DisplayStyle { 143 | background_color: Color32::from_rgb(0x10, 0x00, 0x00), 144 | active_foreground_color: Color32::from_rgb(0xC8, 0x00, 0x00), 145 | active_foreground_stroke: Stroke::NONE, 146 | inactive_foreground_color: Color32::from_rgb(0x20, 0x00, 0x00), 147 | inactive_foreground_stroke: Stroke::NONE, 148 | }, 149 | DisplayStylePreset::BlueNegative => DisplayStyle { 150 | background_color: Color32::from_rgb(0x00, 0x00, 0xFF), 151 | active_foreground_color: Color32::from_rgb(0xE0, 0xFF, 0xFF), 152 | active_foreground_stroke: Stroke::NONE, 153 | inactive_foreground_color: Color32::from_rgb(0x28, 0x28, 0xFF), 154 | inactive_foreground_stroke: Stroke::NONE, 155 | }, 156 | DisplayStylePreset::Amber => DisplayStyle { 157 | background_color: Color32::from_rgb(0x1D, 0x12, 0x07), 158 | active_foreground_color: Color32::from_rgb(0xFF, 0x9A, 0x21), 159 | active_foreground_stroke: Stroke::NONE, 160 | inactive_foreground_color: Color32::from_rgb(0x33, 0x20, 0x00), 161 | inactive_foreground_stroke: Stroke::NONE, 162 | }, 163 | DisplayStylePreset::LightBlue => DisplayStyle { 164 | background_color: Color32::from_rgb(0x0F, 0xB0, 0xBC), 165 | active_foreground_color: Color32::from_black_alpha(223), 166 | active_foreground_stroke: Stroke::NONE, 167 | inactive_foreground_color: Color32::from_black_alpha(60), 168 | inactive_foreground_stroke: Stroke::NONE, 169 | }, 170 | DisplayStylePreset::DeLoreanRed => DisplayStyle { 171 | background_color: Color32::from_rgb(0x12, 0x07, 0x0A), 172 | active_foreground_color: Color32::from_rgb(0xFF, 0x59, 0x13), 173 | active_foreground_stroke: Stroke::NONE, 174 | inactive_foreground_color: Color32::from_rgb(0x48, 0x0A, 0x0B), 175 | inactive_foreground_stroke: Stroke::NONE, 176 | }, 177 | DisplayStylePreset::DeLoreanGreen => DisplayStyle { 178 | background_color: Color32::from_rgb(0x05, 0x0A, 0x0A), 179 | active_foreground_color: Color32::from_rgb(0x4A, 0xF5, 0x0F), 180 | active_foreground_stroke: Stroke::NONE, 181 | inactive_foreground_color: Color32::from_rgb(0x07, 0x29, 0x0F), 182 | inactive_foreground_stroke: Stroke::NONE, 183 | }, 184 | DisplayStylePreset::DeLoreanAmber => DisplayStyle { 185 | background_color: Color32::from_rgb(0x08, 0x08, 0x0B), 186 | active_foreground_color: Color32::from_rgb(0xF2, 0xC4, 0x21), 187 | active_foreground_stroke: Stroke::NONE, 188 | inactive_foreground_color: Color32::from_rgb(0x51, 0x2C, 0x0F), 189 | inactive_foreground_stroke: Stroke::NONE, 190 | }, 191 | DisplayStylePreset::YamahaMU2000 => DisplayStyle { 192 | background_color: Color32::from_rgb(0x8C, 0xD7, 0x01), 193 | active_foreground_color: Color32::from_rgb(0x04, 0x4A, 0x00), 194 | active_foreground_stroke: Stroke::NONE, 195 | inactive_foreground_color: Color32::from_rgb(0x7B, 0xCE, 0x02), 196 | inactive_foreground_stroke: Stroke::NONE, 197 | }, 198 | DisplayStylePreset::Dracula => DisplayStyle { 199 | background_color: Color32::from_rgb(0x26, 0x12, 0x1E), // Dracula pink, HSV(.., .., 15%) 200 | active_foreground_color: Color32::from_rgb(0xFF, 0x79, 0xC6), // Dracula pink, HSV(.., .., 100%) 201 | active_foreground_stroke: Stroke::NONE, 202 | inactive_foreground_color: Color32::from_rgb(0x41, 0x1F, 0x33), // Dracula pink, HSV(.., .., 25%) 203 | inactive_foreground_stroke: Stroke::NONE, 204 | }, 205 | } 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /egui_extras_xt/src/barcodes/barcode_widget.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Borrow; 2 | use std::sync::Arc; 3 | 4 | use barcoders::error::Error; 5 | use egui::util::cache::{ComputerMut, FrameCache}; 6 | use egui::{ 7 | vec2, Align2, Color32, FontFamily, FontId, Rect, Response, Sense, Stroke, StrokeKind, Ui, 8 | Widget, 9 | }; 10 | use emath::GuiRounding; 11 | 12 | use barcoders::sym::codabar::Codabar; 13 | use barcoders::sym::code11::Code11; 14 | use barcoders::sym::code128::Code128; 15 | use barcoders::sym::code39::Code39; 16 | use barcoders::sym::code93::Code93; 17 | use barcoders::sym::ean13::EAN13; 18 | use barcoders::sym::ean8::EAN8; 19 | use barcoders::sym::ean_supp::EANSUPP; 20 | use barcoders::sym::tf::TF; 21 | 22 | use strum::{Display, EnumIter}; 23 | 24 | // ---------------------------------------------------------------------------- 25 | 26 | #[non_exhaustive] 27 | #[derive(Clone, Copy, Debug, Display, EnumIter, Eq, Hash, PartialEq)] 28 | pub enum BarcodeKind { 29 | #[strum(to_string = "Codabar")] 30 | Codabar, 31 | 32 | #[strum(to_string = "Code 11")] 33 | Code11, 34 | 35 | #[strum(to_string = "Code 39")] 36 | Code39, 37 | 38 | #[strum(to_string = "Code 39 (+checksum)")] 39 | Code39Checksum, 40 | 41 | #[strum(to_string = "Code 93")] 42 | Code93, 43 | 44 | #[strum(to_string = "Code 128")] 45 | Code128, 46 | 47 | #[strum(to_string = "EAN-8")] 48 | EAN8, 49 | 50 | #[strum(to_string = "EAN-13")] 51 | EAN13, 52 | 53 | #[strum(to_string = "Supplemental EAN")] 54 | EANSUPP, 55 | 56 | #[strum(to_string = "Interleaved 2 of 5")] 57 | ITF, 58 | 59 | #[strum(to_string = "Standard 2 of 5")] 60 | STF, 61 | } 62 | 63 | impl BarcodeKind { 64 | fn encode>(self, data: T) -> Result, Error> { 65 | match self { 66 | BarcodeKind::Codabar => Codabar::new(data).map(|b| b.encode()), 67 | BarcodeKind::Code11 => Code11::new(data).map(|b| b.encode()), 68 | BarcodeKind::Code39 => Code39::new(data).map(|b| b.encode()), 69 | BarcodeKind::Code39Checksum => Code39::with_checksum(data).map(|b| b.encode()), 70 | BarcodeKind::Code93 => Code93::new(data).map(|b| b.encode()), 71 | BarcodeKind::Code128 => Code128::new(data).map(|b| b.encode()), 72 | BarcodeKind::EAN8 => EAN8::new(data).map(|b| b.encode()), 73 | BarcodeKind::EAN13 => EAN13::new(data).map(|b| b.encode()), 74 | BarcodeKind::EANSUPP => EANSUPP::new(data).map(|b| b.encode()), 75 | BarcodeKind::ITF => TF::interleaved(data).map(|b| b.encode()), 76 | BarcodeKind::STF => TF::standard(data).map(|b| b.encode()), 77 | } 78 | } 79 | } 80 | 81 | // ---------------------------------------------------------------------------- 82 | 83 | type BarcodeCacheKey<'a> = (BarcodeKind, &'a str); 84 | type BarcodeCacheValue = Arc, Error>>; 85 | 86 | #[derive(Default)] 87 | struct BarcodeComputer; 88 | 89 | impl<'a> ComputerMut, BarcodeCacheValue> for BarcodeComputer { 90 | fn compute(&mut self, key: BarcodeCacheKey) -> BarcodeCacheValue { 91 | let (barcode_kind, value) = key; 92 | Arc::new(barcode_kind.encode(value)) 93 | } 94 | } 95 | 96 | type BarcodeCache<'a> = FrameCache; 97 | 98 | // ---------------------------------------------------------------------------- 99 | 100 | #[must_use = "You should put this widget in an ui with `ui.add(widget);`"] 101 | pub struct BarcodeWidget<'a> { 102 | value: &'a str, 103 | barcode_kind: BarcodeKind, 104 | bar_width: usize, 105 | bar_height: f32, 106 | horizontal_padding: f32, 107 | vertical_padding: f32, 108 | label: Option<&'a str>, 109 | label_height: f32, 110 | label_top_margin: f32, 111 | foreground_color: Color32, 112 | background_color: Color32, 113 | } 114 | 115 | impl<'a> BarcodeWidget<'a> { 116 | pub fn new(value: &'a str) -> Self { 117 | Self { 118 | value, 119 | barcode_kind: BarcodeKind::Code39, 120 | bar_width: 2, 121 | bar_height: 64.0, 122 | horizontal_padding: 50.0, 123 | vertical_padding: 10.0, 124 | label: None, 125 | label_height: 20.0, 126 | label_top_margin: 4.0, 127 | foreground_color: Color32::BLACK, 128 | background_color: Color32::WHITE, 129 | } 130 | } 131 | 132 | pub fn barcode_kind(mut self, barcode_kind: BarcodeKind) -> Self { 133 | self.barcode_kind = barcode_kind; 134 | self 135 | } 136 | 137 | pub fn bar_width(mut self, bar_width: impl Into) -> Self { 138 | self.bar_width = bar_width.into(); 139 | self 140 | } 141 | 142 | pub fn bar_height(mut self, bar_height: impl Into) -> Self { 143 | self.bar_height = bar_height.into(); 144 | self 145 | } 146 | 147 | pub fn horizontal_padding(mut self, horizontal_padding: impl Into) -> Self { 148 | self.horizontal_padding = horizontal_padding.into(); 149 | self 150 | } 151 | 152 | pub fn vertical_padding(mut self, vertical_padding: impl Into) -> Self { 153 | self.vertical_padding = vertical_padding.into(); 154 | self 155 | } 156 | 157 | pub fn label(mut self, label: &'a str) -> Self { 158 | self.label = Some(label); 159 | self 160 | } 161 | 162 | pub fn label_height(mut self, label_height: impl Into) -> Self { 163 | self.label_height = label_height.into(); 164 | self 165 | } 166 | 167 | pub fn label_top_margin(mut self, label_top_margin: impl Into) -> Self { 168 | self.label_top_margin = label_top_margin.into(); 169 | self 170 | } 171 | 172 | pub fn foreground_color(mut self, foreground_color: impl Into) -> Self { 173 | self.foreground_color = foreground_color.into(); 174 | self 175 | } 176 | 177 | pub fn background_color(mut self, background_color: impl Into) -> Self { 178 | self.background_color = background_color.into(); 179 | self 180 | } 181 | } 182 | 183 | impl<'a> Widget for BarcodeWidget<'a> { 184 | fn ui(self, ui: &mut Ui) -> Response { 185 | let cached_barcode = ui.memory_mut(|memory| { 186 | let cache = memory.caches.cache::>(); 187 | cache.get((self.barcode_kind, self.value)) 188 | }); 189 | 190 | if let Ok(barcode) = cached_barcode.borrow() { 191 | let bar_width = self.bar_width as f32 / ui.ctx().pixels_per_point(); 192 | 193 | let desired_size = { 194 | let mut size = vec2(bar_width * barcode.len() as f32, self.bar_height) 195 | + vec2(self.horizontal_padding, self.vertical_padding) * 2.0; 196 | 197 | if self.label.is_some() { 198 | size += vec2(0.0, self.label_height + self.label_top_margin); 199 | } 200 | 201 | size 202 | }; 203 | 204 | let (rect, response) = ui.allocate_exact_size(desired_size, Sense::hover()); 205 | 206 | if ui.is_rect_visible(rect) { 207 | ui.painter().rect( 208 | rect, 209 | ui.style().visuals.noninteractive().corner_radius, 210 | self.background_color, 211 | Stroke::NONE, 212 | StrokeKind::Middle, 213 | ); 214 | 215 | barcode 216 | .iter() 217 | .enumerate() 218 | .filter(|&(_bar_index, bar_value)| *bar_value == 1) 219 | .map(|(bar_index, _bar_value)| { 220 | Rect::from_min_size( 221 | (rect.left_top() 222 | + vec2(self.horizontal_padding, self.vertical_padding)) 223 | .round_to_pixels(ui.pixels_per_point()) 224 | + vec2(bar_width * bar_index as f32, 0.0), 225 | vec2(bar_width, self.bar_height), 226 | ) 227 | }) 228 | .for_each(|bar_rect| { 229 | ui.painter() 230 | .rect_filled(bar_rect, 0.0, self.foreground_color); 231 | }); 232 | 233 | if let Some(label) = self.label { 234 | ui.painter().text( 235 | rect.center_bottom() - vec2(0.0, self.vertical_padding), 236 | Align2::CENTER_BOTTOM, 237 | label, 238 | FontId::new(self.label_height, FontFamily::Proportional), 239 | self.foreground_color, 240 | ); 241 | } 242 | } 243 | 244 | response 245 | } else { 246 | ui.colored_label( 247 | ui.style().visuals.error_fg_color, 248 | "\u{1F525} Failed to render barcode", 249 | ) 250 | } 251 | } 252 | } 253 | -------------------------------------------------------------------------------- /egui_extras_xt/src/compasses/compass_marker.rs: -------------------------------------------------------------------------------- 1 | use std::f32::consts::TAU; 2 | 3 | use ecolor::Hsva; 4 | use egui::{vec2, Align2, Color32, FontFamily, FontId, Rect, Shape, Stroke, StrokeKind, Ui, Vec2}; 5 | use itertools::Itertools; 6 | use strum::Display; 7 | 8 | use crate::common::normalized_angle_unsigned_excl; 9 | use crate::hash::PearsonHash; 10 | 11 | // ---------------------------------------------------------------------------- 12 | 13 | #[must_use] 14 | #[non_exhaustive] 15 | #[derive(Clone, Copy, Debug, Display, PartialEq)] 16 | pub enum DefaultCompassMarkerColor { 17 | #[strum(to_string = "System")] 18 | System, 19 | 20 | #[strum(to_string = "Fixed")] 21 | Fixed(Color32), 22 | 23 | #[strum(to_string = "HSV by angle")] 24 | HsvByAngle { 25 | hue_phase: f32, 26 | saturation: f32, 27 | value: f32, 28 | }, 29 | 30 | #[strum(to_string = "HSV by label")] 31 | HsvByLabel { 32 | hue_phase: f32, 33 | saturation: f32, 34 | value: f32, 35 | }, 36 | } 37 | 38 | impl DefaultCompassMarkerColor { 39 | #[must_use] 40 | pub(crate) fn color(&self, ui: &Ui, marker: &CompassMarker) -> Color32 { 41 | match *self { 42 | DefaultCompassMarkerColor::System => ui.style().visuals.text_color(), 43 | DefaultCompassMarkerColor::Fixed(color) => color, 44 | DefaultCompassMarkerColor::HsvByAngle { 45 | hue_phase, 46 | saturation, 47 | value, 48 | } => { 49 | let hue_raw = marker.angle / TAU; 50 | let hue = (hue_raw + hue_phase).rem_euclid(1.0); 51 | Color32::from(Hsva::new(hue, saturation, value, 1.0)) 52 | } 53 | DefaultCompassMarkerColor::HsvByLabel { 54 | hue_phase, 55 | saturation, 56 | value, 57 | } => { 58 | let marker_label = marker.label.unwrap_or(""); 59 | let hue_raw = marker_label.pearson_hash() as f32 / 255.0; 60 | let hue = (hue_raw + hue_phase).rem_euclid(1.0); 61 | Color32::from(Hsva::new(hue, saturation, value, 1.0)) 62 | } 63 | } 64 | } 65 | } 66 | 67 | // ---------------------------------------------------------------------------- 68 | 69 | #[must_use = "You should put this marker into a compass with `compass.markers(&[markers]);`"] 70 | pub struct CompassMarker<'a> { 71 | pub(crate) angle: f32, 72 | pub(crate) distance: Option, 73 | pub(crate) shape: Option, 74 | pub(crate) label: Option<&'a str>, 75 | pub(crate) color: Option, 76 | } 77 | 78 | impl<'a> CompassMarker<'a> { 79 | pub fn new(angle: f32) -> Self { 80 | Self { 81 | angle: normalized_angle_unsigned_excl(angle), 82 | distance: None, 83 | shape: None, 84 | label: None, 85 | color: None, 86 | } 87 | } 88 | 89 | pub fn distance(mut self, distance: f32) -> Self { 90 | self.distance = Some(distance); 91 | self 92 | } 93 | 94 | pub fn shape(mut self, shape: CompassMarkerShape) -> Self { 95 | self.shape = Some(shape); 96 | self 97 | } 98 | 99 | pub fn label(mut self, label: &'a str) -> Self { 100 | self.label = Some(label); 101 | self 102 | } 103 | 104 | pub fn color(mut self, color: Color32) -> Self { 105 | self.color = Some(color); 106 | self 107 | } 108 | } 109 | 110 | // ---------------------------------------------------------------------------- 111 | 112 | #[non_exhaustive] 113 | #[derive(Clone, Copy, Debug, Display, PartialEq)] 114 | pub enum CompassMarkerShape { 115 | #[strum(to_string = "Square")] 116 | Square, 117 | 118 | #[strum(to_string = "Circle")] 119 | Circle, 120 | 121 | #[strum(to_string = "Right arrow")] 122 | RightArrow, 123 | 124 | #[strum(to_string = "Up arrow")] 125 | UpArrow, 126 | 127 | #[strum(to_string = "Left arrow")] 128 | LeftArrow, 129 | 130 | #[strum(to_string = "Down arrow")] 131 | DownArrow, 132 | 133 | #[strum(to_string = "Diamond")] 134 | Diamond, 135 | 136 | #[strum(to_string = "Star")] 137 | Star(usize, f32), 138 | 139 | #[strum(to_string = "Emoji")] 140 | Emoji(char), 141 | } 142 | 143 | impl CompassMarkerShape { 144 | pub(crate) fn paint( 145 | &self, 146 | ui: &mut Ui, 147 | rect: Rect, 148 | fill: Color32, 149 | stroke: Stroke, 150 | stroke_kind: StrokeKind, 151 | ) { 152 | match *self { 153 | CompassMarkerShape::Square => { 154 | ui.painter().rect(rect, 0.0, fill, stroke, stroke_kind); 155 | } 156 | CompassMarkerShape::Circle => { 157 | ui.painter() 158 | .rect(rect, rect.width() / 2.0, fill, stroke, stroke_kind); 159 | } 160 | CompassMarkerShape::RightArrow => { 161 | let rect = Rect::from_center_size( 162 | rect.center(), 163 | rect.size() * vec2(3.0f32.sqrt() / 2.0, 1.0), 164 | ); 165 | 166 | ui.painter().add(Shape::convex_polygon( 167 | vec![rect.right_center(), rect.left_bottom(), rect.left_top()], 168 | fill, 169 | stroke, 170 | )); 171 | } 172 | CompassMarkerShape::UpArrow => { 173 | let rect = Rect::from_center_size( 174 | rect.center(), 175 | rect.size() * vec2(1.0, 3.0f32.sqrt() / 2.0), 176 | ); 177 | 178 | ui.painter().add(Shape::convex_polygon( 179 | vec![rect.center_top(), rect.right_bottom(), rect.left_bottom()], 180 | fill, 181 | stroke, 182 | )); 183 | } 184 | CompassMarkerShape::LeftArrow => { 185 | let rect = Rect::from_center_size( 186 | rect.center(), 187 | rect.size() * vec2(3.0f32.sqrt() / 2.0, 1.0), 188 | ); 189 | 190 | ui.painter().add(Shape::convex_polygon( 191 | vec![rect.left_center(), rect.right_top(), rect.right_bottom()], 192 | fill, 193 | stroke, 194 | )); 195 | } 196 | CompassMarkerShape::DownArrow => { 197 | let rect = Rect::from_center_size( 198 | rect.center(), 199 | rect.size() * vec2(1.0, 3.0f32.sqrt() / 2.0), 200 | ); 201 | 202 | ui.painter().add(Shape::convex_polygon( 203 | vec![rect.left_top(), rect.right_top(), rect.center_bottom()], 204 | fill, 205 | stroke, 206 | )); 207 | } 208 | CompassMarkerShape::Diamond => { 209 | ui.painter().add(Shape::convex_polygon( 210 | vec![ 211 | rect.center_top(), 212 | rect.right_center(), 213 | rect.center_bottom(), 214 | rect.left_center(), 215 | ], 216 | fill, 217 | stroke, 218 | )); 219 | } 220 | CompassMarkerShape::Star(rays, ratio) => { 221 | assert!(rays >= 2, "star-shaped markers must have at least 2 rays"); 222 | assert!( 223 | (0.0..=1.0).contains(&ratio), 224 | "ray ratio of star-shaped markers must be normalized" 225 | ); 226 | 227 | let outer_radius = rect.width() * 0.5; 228 | let inner_radius = outer_radius * ratio; 229 | let star_rotation = -TAU * 0.25; 230 | 231 | let outer_points = (0..rays).map(|point_index| { 232 | rect.center() 233 | + Vec2::angled( 234 | star_rotation + TAU * ((point_index as f32 + 0.0) / rays as f32), 235 | ) * outer_radius 236 | }); 237 | 238 | let inner_points = (0..rays).map(|point_index| { 239 | rect.center() 240 | + Vec2::angled( 241 | star_rotation + TAU * ((point_index as f32 + 0.5) / rays as f32), 242 | ) * inner_radius 243 | }); 244 | 245 | // TODO: Broken polygon renderer 246 | // https://github.com/emilk/egui/issues/513 247 | ui.painter().add(Shape::convex_polygon( 248 | outer_points.interleave(inner_points).collect_vec(), 249 | fill, 250 | stroke, 251 | )); 252 | } 253 | CompassMarkerShape::Emoji(emoji) => { 254 | ui.painter().text( 255 | rect.center(), 256 | Align2::CENTER_CENTER, 257 | emoji, 258 | FontId::new(rect.height(), FontFamily::Proportional), 259 | fill, 260 | ); 261 | } 262 | } 263 | } 264 | } 265 | -------------------------------------------------------------------------------- /egui_extras_xt_example/src/bin/widget_gallery/pages/linear_compass_page.rs: -------------------------------------------------------------------------------- 1 | use eframe::egui::{DragValue, Grid, Ui}; 2 | use eframe::epaint::Color32; 3 | use egui_extras_xt::common::{Winding, WrapMode}; 4 | use egui_extras_xt::compasses::{ 5 | CompassAxisLabels, CompassMarker, CompassMarkerShape, DefaultCompassMarkerColor, LinearCompass, 6 | }; 7 | use egui_extras_xt::ui::optional_value_widget::OptionalValueWidget; 8 | use egui_extras_xt::ui::widgets_from_iter::SelectableValueFromIter; 9 | use strum::IntoEnumIterator; 10 | 11 | use crate::pages::ui::{ 12 | compass_axis_labels_ui, default_compass_marker_color_ui, default_compass_marker_shape_ui, 13 | }; 14 | use crate::pages::PageImpl; 15 | 16 | pub struct LinearCompassPage { 17 | value: f32, 18 | interactive: bool, 19 | wrap: WrapMode, 20 | winding: Winding, 21 | width: f32, 22 | height: f32, 23 | spread: f32, 24 | axis_labels: Vec, 25 | snap: Option, 26 | shift_snap: Option, 27 | min: Option, 28 | max: Option, 29 | animated: bool, 30 | show_cursor: bool, 31 | show_ticks: bool, 32 | show_axes: bool, 33 | default_marker_color: DefaultCompassMarkerColor, 34 | default_marker_shape: CompassMarkerShape, 35 | } 36 | 37 | impl Default for LinearCompassPage { 38 | fn default() -> LinearCompassPage { 39 | LinearCompassPage { 40 | value: 0.0, 41 | interactive: true, 42 | wrap: WrapMode::Unsigned, 43 | winding: Winding::Clockwise, 44 | width: 512.0, 45 | height: 48.0, 46 | spread: 180.0f32.to_radians(), 47 | axis_labels: vec![ 48 | "N".to_owned(), 49 | "E".to_owned(), 50 | "S".to_owned(), 51 | "W".to_owned(), 52 | ], 53 | snap: None, 54 | shift_snap: Some(10.0f32.to_radians()), 55 | min: None, 56 | max: None, 57 | animated: false, 58 | show_cursor: true, 59 | show_ticks: true, 60 | show_axes: true, 61 | default_marker_color: DefaultCompassMarkerColor::HsvByAngle { 62 | hue_phase: 0.0, 63 | saturation: 1.0, 64 | value: 1.0, 65 | }, 66 | default_marker_shape: CompassMarkerShape::Square, 67 | } 68 | } 69 | } 70 | 71 | impl PageImpl for LinearCompassPage { 72 | fn ui(&mut self, ui: &mut Ui) { 73 | ui.add( 74 | LinearCompass::new(&mut self.value) 75 | .interactive(self.interactive) 76 | .wrap(self.wrap) 77 | .winding(self.winding) 78 | .width(self.width) 79 | .height(self.height) 80 | .spread(self.spread) 81 | .snap(self.snap) 82 | .axis_labels(CompassAxisLabels::from_slice(&self.axis_labels)) 83 | .shift_snap(self.shift_snap) 84 | .min(self.min) 85 | .max(self.max) 86 | .animated(self.animated) 87 | .show_cursor(self.show_cursor) 88 | .show_ticks(self.show_ticks) 89 | .show_axes(self.show_axes) 90 | .default_marker_color(self.default_marker_color) 91 | .default_marker_shape(self.default_marker_shape) 92 | .markers(&[ 93 | CompassMarker::new(0.0f32.to_radians()).label("Default"), 94 | // Grand Theft Auto style markers 95 | CompassMarker::new(70.0f32.to_radians()) 96 | .shape(CompassMarkerShape::Square) 97 | .label("Sweet") 98 | .color(Color32::from_rgb(0x00, 0x00, 0xFF)), 99 | CompassMarker::new(85.0f32.to_radians()) 100 | .shape(CompassMarkerShape::DownArrow) 101 | .label("Reece's") 102 | .color(Color32::from_rgb(0xFF, 0xFF, 0x00)), 103 | CompassMarker::new(100.0f32.to_radians()) 104 | .shape(CompassMarkerShape::UpArrow) 105 | .label("Big Smoke") 106 | .color(Color32::from_rgb(0xFF, 0x00, 0x00)), 107 | // Emoji markers 108 | CompassMarker::new(553.0f32.to_radians()) 109 | .shape(CompassMarkerShape::Emoji('🐱')) 110 | .label("Cat") 111 | .color(Color32::from_rgb(0xF8, 0xE9, 0xFF)), 112 | CompassMarker::new(563.0f32.to_radians()) 113 | .shape(CompassMarkerShape::Emoji('🐶')) 114 | .label("Dog") 115 | .color(Color32::from_rgb(0xC0, 0x8C, 0x85)), 116 | // All marker shapes 117 | CompassMarker::new(240.0f32.to_radians()) 118 | .shape(CompassMarkerShape::Square) 119 | .label("A"), 120 | CompassMarker::new(250.0f32.to_radians()) 121 | .shape(CompassMarkerShape::Circle) 122 | .label("B"), 123 | CompassMarker::new(260.0f32.to_radians()) 124 | .shape(CompassMarkerShape::RightArrow) 125 | .label("C"), 126 | CompassMarker::new(270.0f32.to_radians()) 127 | .shape(CompassMarkerShape::UpArrow) 128 | .label("D"), 129 | CompassMarker::new(280.0f32.to_radians()) 130 | .shape(CompassMarkerShape::LeftArrow) 131 | .label("E"), 132 | CompassMarker::new(290.0f32.to_radians()) 133 | .shape(CompassMarkerShape::DownArrow) 134 | .label("F"), 135 | CompassMarker::new(300.0f32.to_radians()) 136 | .shape(CompassMarkerShape::Diamond) 137 | .label("G"), 138 | CompassMarker::new(310.0f32.to_radians()) 139 | .shape(CompassMarkerShape::Star(5, 0.5)) 140 | .label("H"), 141 | CompassMarker::new(320.0f32.to_radians()) 142 | .shape(CompassMarkerShape::Emoji('🗿')) 143 | .label("I"), 144 | // Transparent colors 145 | CompassMarker::new(30.0f32.to_radians()) 146 | .shape(CompassMarkerShape::Square) 147 | .label("Near") 148 | .color(Color32::from_rgb(0x40, 0x80, 0x80).linear_multiply(1.0)), 149 | CompassMarker::new(40.0f32.to_radians()) 150 | .shape(CompassMarkerShape::Square) 151 | .label("Far") 152 | .color(Color32::from_rgb(0x40, 0x80, 0x80).linear_multiply(0.5)), 153 | CompassMarker::new(50.0f32.to_radians()) 154 | .shape(CompassMarkerShape::Square) 155 | .label("Very far") 156 | .color(Color32::from_rgb(0x40, 0x80, 0x80).linear_multiply(0.25)), 157 | ]), 158 | ); 159 | ui.separator(); 160 | 161 | Grid::new("linear_compass_properties") 162 | .num_columns(2) 163 | .spacing([20.0, 10.0]) 164 | .striped(true) 165 | .show(ui, |ui| { 166 | ui.label("Value"); 167 | ui.drag_angle(&mut self.value); 168 | ui.end_row(); 169 | 170 | ui.label("Interactive"); 171 | ui.checkbox(&mut self.interactive, ""); 172 | ui.end_row(); 173 | 174 | ui.label("Wrap"); 175 | ui.horizontal(|ui| { 176 | ui.selectable_value_from_iter(&mut self.wrap, WrapMode::iter()); 177 | }); 178 | ui.end_row(); 179 | 180 | ui.label("Winding"); 181 | ui.horizontal(|ui| { 182 | ui.selectable_value_from_iter(&mut self.winding, Winding::iter()); 183 | }); 184 | ui.end_row(); 185 | 186 | ui.label("Width"); 187 | ui.add(DragValue::new(&mut self.width)); 188 | ui.end_row(); 189 | 190 | ui.label("Height"); 191 | ui.add(DragValue::new(&mut self.height)); 192 | ui.end_row(); 193 | 194 | ui.label("Spread"); 195 | ui.drag_angle(&mut self.spread); 196 | ui.end_row(); 197 | 198 | ui.label("Axis labels"); 199 | compass_axis_labels_ui(ui, &mut self.axis_labels); 200 | ui.end_row(); 201 | 202 | ui.label("Snap"); 203 | ui.optional_value_widget(&mut self.snap, Ui::drag_angle); 204 | ui.end_row(); 205 | 206 | ui.label("Shift snap"); 207 | ui.optional_value_widget(&mut self.shift_snap, Ui::drag_angle); 208 | ui.end_row(); 209 | 210 | ui.label("Minimum"); 211 | ui.optional_value_widget(&mut self.min, Ui::drag_angle); 212 | ui.end_row(); 213 | 214 | ui.label("Maximum"); 215 | ui.optional_value_widget(&mut self.max, Ui::drag_angle); 216 | ui.end_row(); 217 | 218 | ui.label("Animated"); 219 | ui.checkbox(&mut self.animated, ""); 220 | ui.end_row(); 221 | 222 | ui.label("Show cursor"); 223 | ui.checkbox(&mut self.show_cursor, ""); 224 | ui.end_row(); 225 | 226 | ui.label("Show ticks"); 227 | ui.checkbox(&mut self.show_ticks, ""); 228 | ui.end_row(); 229 | 230 | ui.label("Show axes"); 231 | ui.checkbox(&mut self.show_axes, ""); 232 | ui.end_row(); 233 | 234 | ui.label("Default marker color"); 235 | default_compass_marker_color_ui(ui, &mut self.default_marker_color); 236 | ui.end_row(); 237 | 238 | ui.label("Default marker shape"); 239 | default_compass_marker_shape_ui(ui, &mut self.default_marker_shape); 240 | ui.end_row(); 241 | }); 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /egui_extras_xt/src/displays/segmented_display/widget.rs: -------------------------------------------------------------------------------- 1 | use egui::{pos2, vec2, Pos2, Response, Sense, Shape, Stroke, StrokeKind, Ui, UiBuilder, Widget}; 2 | use itertools::Itertools; 3 | 4 | use crate::displays::segmented_display::{ 5 | DisplayDigit, DisplayKind, DisplayMetrics, DisplayMetricsPreset, 6 | }; 7 | use crate::displays::{DisplayStyle, DisplayStylePreset}; 8 | 9 | #[must_use = "You should put this widget in an ui with `ui.add(widget);`"] 10 | pub struct SegmentedDisplayWidget { 11 | display_kind: DisplayKind, 12 | digits: Vec, 13 | digit_height: f32, 14 | metrics: DisplayMetrics, 15 | style: DisplayStyle, 16 | show_dots: bool, 17 | show_colons: bool, 18 | show_apostrophes: bool, 19 | } 20 | 21 | impl SegmentedDisplayWidget { 22 | pub fn new(display_kind: DisplayKind) -> Self { 23 | Self { 24 | display_kind, 25 | digits: Vec::new(), 26 | digit_height: 80.0, 27 | metrics: DisplayMetrics::default(), 28 | style: DisplayStylePreset::Default.style(), 29 | show_dots: true, 30 | show_colons: true, 31 | show_apostrophes: true, 32 | } 33 | } 34 | 35 | pub fn seven_segment>(value: T) -> Self { 36 | Self::new(DisplayKind::SevenSegment).push_string(value.as_ref()) 37 | } 38 | 39 | pub fn nine_segment>(value: T) -> Self { 40 | Self::new(DisplayKind::NineSegment).push_string(value.as_ref()) 41 | } 42 | 43 | pub fn sixteen_segment>(value: T) -> Self { 44 | Self::new(DisplayKind::SixteenSegment).push_string(value.as_ref()) 45 | } 46 | 47 | pub fn push_string>(mut self, value: T) -> Self { 48 | let display_impl = self.display_kind.display_impl(); 49 | 50 | self.digits.extend( 51 | [None] 52 | .into_iter() 53 | .chain(value.as_ref().chars().map(Some)) 54 | .chain([None]) 55 | .tuple_windows() 56 | .filter_map(|(prev, curr, next)| match curr { 57 | Some('.') if self.show_dots => None, 58 | Some(':') if self.show_colons => None, 59 | Some('\'') if self.show_apostrophes => None, 60 | Some(c) if display_impl.glyph(c).is_some() => Some(DisplayDigit { 61 | glyph: display_impl.glyph(c).unwrap(), 62 | dot: (next == Some('.')) && self.show_dots, 63 | colon: (prev == Some(':')) && self.show_colons, 64 | apostrophe: (prev == Some('\'')) && self.show_apostrophes, 65 | }), 66 | _ => None, 67 | }), 68 | ); 69 | self 70 | } 71 | 72 | pub fn push_digit(mut self, digit: DisplayDigit) -> Self { 73 | self.digits.push(digit); 74 | self 75 | } 76 | 77 | pub fn digit_height(mut self, digit_height: impl Into) -> Self { 78 | self.digit_height = digit_height.into(); 79 | self 80 | } 81 | 82 | pub fn style(mut self, style: DisplayStyle) -> Self { 83 | self.style = style; 84 | self 85 | } 86 | 87 | pub fn style_preset(mut self, preset: DisplayStylePreset) -> Self { 88 | self.style = preset.style(); 89 | self 90 | } 91 | 92 | pub fn metrics(mut self, metrics: DisplayMetrics) -> Self { 93 | self.metrics = metrics; 94 | self 95 | } 96 | 97 | pub fn metrics_preset(mut self, preset: DisplayMetricsPreset) -> Self { 98 | self.metrics = preset.metrics(); 99 | self 100 | } 101 | 102 | pub fn show_dots(mut self, show_dots: bool) -> Self { 103 | self.show_dots = show_dots; 104 | self 105 | } 106 | 107 | pub fn show_colons(mut self, show_colons: bool) -> Self { 108 | self.show_colons = show_colons; 109 | self 110 | } 111 | 112 | pub fn show_apostrophes(mut self, show_apostrophes: bool) -> Self { 113 | self.show_apostrophes = show_apostrophes; 114 | self 115 | } 116 | } 117 | 118 | impl Widget for SegmentedDisplayWidget { 119 | fn ui(self, ui: &mut Ui) -> Response { 120 | let display_impl = self.display_kind.display_impl(); 121 | 122 | let digit_height = self.digit_height; 123 | let digit_width = digit_height * self.metrics.digit_ratio; 124 | 125 | // Turn relative metrics to absolute metrics 126 | let segment_thickness = self.metrics.segment_thickness * digit_height; 127 | let segment_spacing = self.metrics.segment_spacing * digit_height; 128 | let digit_shearing = self.metrics.digit_shearing * digit_width; 129 | let digit_spacing = self.metrics.digit_spacing * digit_width; 130 | let margin_horizontal = self.metrics.margin_horizontal * digit_width; 131 | let margin_vertical = self.metrics.margin_vertical * digit_height; 132 | let digit_median = self.metrics.digit_median * (digit_height / 2.0); 133 | let colon_separation = self.metrics.colon_separation * (digit_height / 2.0); 134 | 135 | let desired_size = vec2( 136 | (digit_width * self.digits.len() as f32) 137 | + (digit_spacing * (self.digits.len().saturating_sub(1)) as f32) 138 | + (2.0 * margin_horizontal) 139 | + (2.0 * digit_shearing.abs()), 140 | digit_height + (2.0 * margin_vertical), 141 | ); 142 | 143 | let (rect, response) = ui.allocate_exact_size(desired_size, Sense::click()); 144 | 145 | let mut child_ui = ui.new_child(UiBuilder::new().max_rect(rect).layout(*ui.layout())); 146 | child_ui.set_clip_rect(child_ui.clip_rect().intersect(rect)); 147 | 148 | if child_ui.is_rect_visible(rect) { 149 | // Draw the widget background without clipping 150 | ui.painter().rect( 151 | rect, 152 | ui.style().visuals.noninteractive().corner_radius, 153 | self.style.background_color, 154 | Stroke::NONE, 155 | StrokeKind::Middle, 156 | ); 157 | 158 | let segment_geometry = display_impl.geometry( 159 | digit_width, 160 | digit_height, 161 | segment_thickness, 162 | segment_spacing, 163 | digit_median, 164 | ); 165 | assert_eq!(segment_geometry.len(), display_impl.segment_count()); 166 | 167 | #[rustfmt::skip] 168 | let apostrophe_points: Vec = vec![ 169 | pos2(-(digit_width / 2.0) - (digit_spacing / 2.0) - (segment_thickness / 2.0), -(digit_height / 2.0) ), 170 | pos2(-(digit_width / 2.0) - (digit_spacing / 2.0) + (segment_thickness / 2.0), -(digit_height / 2.0) ), 171 | pos2(-(digit_width / 2.0) - (digit_spacing / 2.0) - (segment_thickness / 2.0), -(digit_height / 2.0) + (segment_thickness * 2.0)), 172 | ]; 173 | 174 | #[rustfmt::skip] 175 | let (colon_top_pos, colon_bottom_pos, dot_pos) = ( 176 | pos2(-(digit_width / 2.0) - (digit_spacing / 2.0), digit_median - colon_separation), 177 | pos2(-(digit_width / 2.0) - (digit_spacing / 2.0), digit_median + colon_separation), 178 | pos2( (digit_width / 2.0) + (digit_spacing / 2.0), (digit_height / 2.0) - (segment_thickness / 2.0)) 179 | ); 180 | 181 | let paint_digit = |digit: &DisplayDigit, digit_center: Pos2| { 182 | let transform = |&Pos2 { x, y }| { 183 | digit_center + vec2(x, y) 184 | - vec2((y / (digit_height / 2.0)) * digit_shearing, 0.0) 185 | }; 186 | 187 | for (segment_index, segment_points) in segment_geometry.iter().enumerate() { 188 | let segment_active = ((digit.glyph >> segment_index) & 0x01) != 0x00; 189 | 190 | // TODO: concave_polygon 191 | // https://github.com/emilk/egui/issues/513 192 | child_ui.painter().add(Shape::convex_polygon( 193 | segment_points.iter().map(transform).collect_vec(), 194 | self.style.foreground_color(segment_active), 195 | self.style.foreground_stroke(segment_active), 196 | )); 197 | } 198 | 199 | if self.show_dots { 200 | child_ui.painter().circle( 201 | transform(&dot_pos), 202 | segment_thickness / 2.0, 203 | self.style.foreground_color(digit.dot), 204 | self.style.foreground_stroke(digit.dot), 205 | ); 206 | } 207 | 208 | if self.show_colons { 209 | child_ui.painter().circle( 210 | transform(&colon_top_pos), 211 | segment_thickness / 2.0, 212 | self.style.foreground_color(digit.colon), 213 | self.style.foreground_stroke(digit.colon), 214 | ); 215 | 216 | child_ui.painter().circle( 217 | transform(&colon_bottom_pos), 218 | segment_thickness / 2.0, 219 | self.style.foreground_color(digit.colon), 220 | self.style.foreground_stroke(digit.colon), 221 | ); 222 | } 223 | 224 | if self.show_apostrophes { 225 | child_ui.painter().add(Shape::convex_polygon( 226 | apostrophe_points.iter().map(transform).collect_vec(), 227 | self.style.foreground_color(digit.apostrophe), 228 | self.style.foreground_stroke(digit.apostrophe), 229 | )); 230 | } 231 | }; 232 | 233 | for (digit_index, digit) in self.digits.iter().enumerate() { 234 | let digit_center = rect.left_center() 235 | + vec2( 236 | margin_horizontal 237 | + digit_shearing.abs() 238 | + ((digit_width + digit_spacing) * digit_index as f32) 239 | + (digit_width / 2.0), 240 | 0.0, 241 | ); 242 | 243 | paint_digit(digit, digit_center); 244 | } 245 | } 246 | 247 | response 248 | } 249 | } 250 | --------------------------------------------------------------------------------