diff --git a/CHANGELOG.md b/CHANGELOG.md index 78be94a284d..0c3e8acb7c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,9 @@ NOTE: [`epaint`](crates/epaint/CHANGELOG.md), [`eframe`](crates/eframe/CHANGELOG ### Added ⭐ * Added helper functions for animating panels that collapse/expand ([#2190](https://github.com/emilk/egui/pull/2190)). +* Added `Context::os/Context::set_os` to query/set what operating system egui believes it is running on ([#2202](https://github.com/emilk/egui/pull/2202)). +* Added `Button::shortcut_text` for showing keyboard shortcuts in menu buttons ([#2202](https://github.com/emilk/egui/pull/2202)). +* Added `egui::KeyboardShortcut` for showing keyboard shortcuts in menu buttons ([#2202](https://github.com/emilk/egui/pull/2202)). ### Fixed 🐛 * ⚠️ BREAKING: Fix text being too small ([#2069](https://github.com/emilk/egui/pull/2069)). diff --git a/crates/eframe/CHANGELOG.md b/crates/eframe/CHANGELOG.md index 1934e3d94a0..695f0df8f76 100644 --- a/crates/eframe/CHANGELOG.md +++ b/crates/eframe/CHANGELOG.md @@ -14,7 +14,7 @@ NOTE: [`egui-winit`](../egui-winit/CHANGELOG.md), [`egui_glium`](../egui_glium/C * Added `center` to `NativeOptions` and `monitor_size` to `WindowInfo` on desktop ([#2035](https://github.com/emilk/egui/pull/2035)). * Web: you can access your application from JS using `AppRunner::app_mut`. See `crates/egui_demo_app/src/lib.rs`. * Web: You can now use WebGL on top of `wgpu` by enabling the `wgpu` feature (and disabling `glow` via disabling default features) ([#2107](https://github.com/emilk/egui/pull/2107)). - +* Web: Add `WebInfo::user_agent` ([#2202](https://github.com/emilk/egui/pull/2202)). ## 0.19.0 - 2022-08-20 diff --git a/crates/eframe/src/epi.rs b/crates/eframe/src/epi.rs index d6a523fd8cf..5239364bc23 100644 --- a/crates/eframe/src/epi.rs +++ b/crates/eframe/src/epi.rs @@ -767,6 +767,9 @@ impl Frame { #[derive(Clone, Debug)] #[cfg(target_arch = "wasm32")] pub struct WebInfo { + /// The browser user agent. + pub user_agent: String, + /// Information about the URL. pub location: Location, } diff --git a/crates/eframe/src/web/backend.rs b/crates/eframe/src/web/backend.rs index 7d637063279..b153e5c3987 100644 --- a/crates/eframe/src/web/backend.rs +++ b/crates/eframe/src/web/backend.rs @@ -87,6 +87,10 @@ impl IsDestroyed { // ---------------------------------------------------------------------------- +fn user_agent() -> Option { + web_sys::window()?.navigator().user_agent().ok() +} + fn web_location() -> epi::Location { let location = web_sys::window().unwrap().location(); @@ -198,6 +202,7 @@ impl AppRunner { let info = epi::IntegrationInfo { web_info: epi::WebInfo { + user_agent: user_agent().unwrap_or_default(), location: web_location(), }, system_theme, @@ -207,6 +212,9 @@ impl AppRunner { let storage = LocalStorage::default(); let egui_ctx = egui::Context::default(); + egui_ctx.set_os(egui::os::OperatingSystem::from_user_agent( + &user_agent().unwrap_or_default(), + )); load_memory(&egui_ctx); let theme = system_theme.unwrap_or(web_options.default_theme); diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 3ec4052cd3f..9a8974571a2 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -3,7 +3,8 @@ use std::sync::Arc; use crate::{ animation_manager::AnimationManager, data::output::PlatformOutput, frame_state::FrameState, - input_state::*, layers::GraphicLayers, memory::Options, output::FullOutput, TextureHandle, *, + input_state::*, layers::GraphicLayers, memory::Options, os::OperatingSystem, + output::FullOutput, TextureHandle, *, }; use epaint::{mutex::*, stats::*, text::Fonts, textures::TextureFilter, TessellationOptions, *}; @@ -36,6 +37,8 @@ struct ContextImpl { animation_manager: AnimationManager, tex_manager: WrappedTextureManager, + os: OperatingSystem, + input: InputState, /// State that is collected during a frame and then cleared @@ -563,6 +566,59 @@ impl Context { pub fn tessellation_options(&self) -> RwLockWriteGuard<'_, TessellationOptions> { RwLockWriteGuard::map(self.write(), |c| &mut c.memory.options.tessellation_options) } + + /// What operating system are we running on? + /// + /// When compiling natively, this is + /// figured out from the `target_os`. + /// + /// For web, this can be figured out from the user-agent, + /// and is done so by [`eframe`](https://github.com/emilk/egui/tree/master/crates/eframe). + pub fn os(&self) -> OperatingSystem { + self.read().os + } + + /// Set the operating system we are running on. + /// + /// If you are writing wasm-based integration for egui you + /// may want to set this based on e.g. the user-agent. + pub fn set_os(&self, os: OperatingSystem) { + self.write().os = os; + } + + /// Format the given shortcut in a human-readable way (e.g. `Ctrl+Shift+X`). + /// + /// Can be used to get the text for [`Button::shortcut_text`]. + pub fn format_shortcut(&self, shortcut: &KeyboardShortcut) -> String { + let os = self.os(); + + let is_mac = matches!(os, OperatingSystem::Mac | OperatingSystem::IOS); + + let can_show_symbols = || { + let ModifierNames { + alt, + ctrl, + shift, + mac_cmd, + .. + } = ModifierNames::SYMBOLS; + + let font_id = TextStyle::Body.resolve(&self.style()); + let fonts = self.fonts(); + let mut fonts = fonts.lock(); + let font = fonts.fonts.font(&font_id); + font.has_glyphs(alt) + && font.has_glyphs(ctrl) + && font.has_glyphs(shift) + && font.has_glyphs(mac_cmd) + }; + + if is_mac && can_show_symbols() { + shortcut.format(&ModifierNames::SYMBOLS, is_mac) + } else { + shortcut.format(&ModifierNames::NAMES, is_mac) + } + } } impl Context { diff --git a/crates/egui/src/data/input.rs b/crates/egui/src/data/input.rs index 49c8c394c54..6c3dfcc8cfb 100644 --- a/crates/egui/src/data/input.rs +++ b/crates/egui/src/data/input.rs @@ -297,6 +297,10 @@ pub const NUM_POINTER_BUTTONS: usize = 5; /// State of the modifier keys. These must be fed to egui. /// /// The best way to compare [`Modifiers`] is by using [`Modifiers::matches`]. +/// +/// NOTE: For cross-platform uses, ALT+SHIFT is a bad combination of modifiers +/// as on mac that is how you type special characters, +/// so those key presses are usually not reported to egui. #[derive(Clone, Copy, Debug, Default, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub struct Modifiers { @@ -321,10 +325,6 @@ pub struct Modifiers { } impl Modifiers { - pub fn new() -> Self { - Default::default() - } - pub const NONE: Self = Self { alt: false, ctrl: false, @@ -354,6 +354,8 @@ impl Modifiers { mac_cmd: false, command: false, }; + + #[deprecated = "Use `Modifiers::ALT | Modifiers::SHIFT` instead"] pub const ALT_SHIFT: Self = Self { alt: true, ctrl: false, @@ -380,24 +382,50 @@ impl Modifiers { command: true, }; - #[inline(always)] + /// ``` + /// # use egui::Modifiers; + /// assert_eq!( + /// Modifiers::CTRL | Modifiers::ALT, + /// Modifiers { ctrl: true, alt: true, ..Default::default() } + /// ); + /// assert_eq!( + /// Modifiers::ALT.plus(Modifiers::CTRL), + /// Modifiers::CTRL.plus(Modifiers::ALT), + /// ); + /// assert_eq!( + /// Modifiers::CTRL | Modifiers::ALT, + /// Modifiers::CTRL.plus(Modifiers::ALT), + /// ); + /// ``` + #[inline] + pub const fn plus(self, rhs: Self) -> Self { + Self { + alt: self.alt | rhs.alt, + ctrl: self.ctrl | rhs.ctrl, + shift: self.shift | rhs.shift, + mac_cmd: self.mac_cmd | rhs.mac_cmd, + command: self.command | rhs.command, + } + } + + #[inline] pub fn is_none(&self) -> bool { self == &Self::default() } - #[inline(always)] + #[inline] pub fn any(&self) -> bool { !self.is_none() } /// Is shift the only pressed button? - #[inline(always)] + #[inline] pub fn shift_only(&self) -> bool { self.shift && !(self.alt || self.command) } /// true if only [`Self::ctrl`] or only [`Self::mac_cmd`] is pressed. - #[inline(always)] + #[inline] pub fn command_only(&self) -> bool { !self.alt && !self.shift && self.command } @@ -453,17 +481,82 @@ impl Modifiers { impl std::ops::BitOr for Modifiers { type Output = Self; + #[inline] fn bitor(self, rhs: Self) -> Self { - Self { - alt: self.alt | rhs.alt, - ctrl: self.ctrl | rhs.ctrl, - shift: self.shift | rhs.shift, - mac_cmd: self.mac_cmd | rhs.mac_cmd, - command: self.command | rhs.command, + self.plus(rhs) + } +} + +// ---------------------------------------------------------------------------- + +/// Names of different modifier keys. +/// +/// Used to name modifiers. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct ModifierNames<'a> { + pub is_short: bool, + + pub alt: &'a str, + pub ctrl: &'a str, + pub shift: &'a str, + pub mac_cmd: &'a str, + + /// What goes between the names + pub concat: &'a str, +} + +impl ModifierNames<'static> { + /// ⌥ ^ ⇧ ⌘ - NOTE: not supported by the default egui font. + pub const SYMBOLS: Self = Self { + is_short: true, + alt: "⌥", + ctrl: "^", + shift: "⇧", + mac_cmd: "⌘", + concat: "", + }; + + /// Alt, Ctrl, Shift, Command + pub const NAMES: Self = Self { + is_short: false, + alt: "Alt", + ctrl: "Ctrl", + shift: "Shift", + mac_cmd: "Command", + concat: "+", + }; +} + +impl<'a> ModifierNames<'a> { + pub fn format(&self, modifiers: &Modifiers, is_mac: bool) -> String { + let mut s = String::new(); + + let mut append_if = |modifier_is_active, modifier_name| { + if modifier_is_active { + if !s.is_empty() { + s += self.concat; + } + s += modifier_name; + } + }; + + if is_mac { + append_if(modifiers.ctrl, self.ctrl); + append_if(modifiers.shift, self.shift); + append_if(modifiers.alt, self.alt); + append_if(modifiers.mac_cmd || modifiers.command, self.mac_cmd); + } else { + append_if(modifiers.ctrl, self.ctrl); + append_if(modifiers.alt, self.alt); + append_if(modifiers.shift, self.shift); } + + s } } +// ---------------------------------------------------------------------------- + /// Keyboard keys. /// /// Includes all keys egui is interested in (such as `Home` and `End`) @@ -563,6 +656,132 @@ pub enum Key { F20, } +impl Key { + /// Emoji or name representing the key + pub fn symbol_or_name(self) -> &'static str { + // TODO(emilk): add support for more unicode symbols (see for instance https://wincent.com/wiki/Unicode_representations_of_modifier_keys). + // Before we do we must first make sure they are supported in `Fonts` though, + // so perhaps this functions needs to take a `supports_character: impl Fn(char) -> bool` or something. + match self { + Key::ArrowDown => "⏷", + Key::ArrowLeft => "⏴", + Key::ArrowRight => "⏵", + Key::ArrowUp => "⏶", + _ => self.name(), + } + } + + /// Human-readable English name. + pub fn name(self) -> &'static str { + match self { + Key::ArrowDown => "Down", + Key::ArrowLeft => "Left", + Key::ArrowRight => "Right", + Key::ArrowUp => "Up", + Key::Escape => "Escape", + Key::Tab => "Tab", + Key::Backspace => "Backspace", + Key::Enter => "Enter", + Key::Space => "Space", + Key::Insert => "Insert", + Key::Delete => "Delete", + Key::Home => "Home", + Key::End => "End", + Key::PageUp => "PageUp", + Key::PageDown => "PageDown", + Key::Num0 => "0", + Key::Num1 => "1", + Key::Num2 => "2", + Key::Num3 => "3", + Key::Num4 => "4", + Key::Num5 => "5", + Key::Num6 => "6", + Key::Num7 => "7", + Key::Num8 => "8", + Key::Num9 => "9", + Key::A => "A", + Key::B => "B", + Key::C => "C", + Key::D => "D", + Key::E => "E", + Key::F => "F", + Key::G => "G", + Key::H => "H", + Key::I => "I", + Key::J => "J", + Key::K => "K", + Key::L => "L", + Key::M => "M", + Key::N => "N", + Key::O => "O", + Key::P => "P", + Key::Q => "Q", + Key::R => "R", + Key::S => "S", + Key::T => "T", + Key::U => "U", + Key::V => "V", + Key::W => "W", + Key::X => "X", + Key::Y => "Y", + Key::Z => "Z", + Key::F1 => "F1", + Key::F2 => "F2", + Key::F3 => "F3", + Key::F4 => "F4", + Key::F5 => "F5", + Key::F6 => "F6", + Key::F7 => "F7", + Key::F8 => "F8", + Key::F9 => "F9", + Key::F10 => "F10", + Key::F11 => "F11", + Key::F12 => "F12", + Key::F13 => "F13", + Key::F14 => "F14", + Key::F15 => "F15", + Key::F16 => "F16", + Key::F17 => "F17", + Key::F18 => "F18", + Key::F19 => "F19", + Key::F20 => "F20", + } + } +} + +// ---------------------------------------------------------------------------- + +/// A keyboard shortcut, e.g. `Ctrl+Alt+W`. +/// +/// Can be used with [`crate::InputState::consume_shortcut`] +/// and [`crate::Context::format_shortcut`]. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct KeyboardShortcut { + pub modifiers: Modifiers, + pub key: Key, +} + +impl KeyboardShortcut { + pub const fn new(modifiers: Modifiers, key: Key) -> Self { + Self { modifiers, key } + } + + pub fn format(&self, names: &ModifierNames<'_>, is_mac: bool) -> String { + let mut s = names.format(&self.modifiers, is_mac); + if !s.is_empty() { + s += names.concat; + } + if names.is_short { + s += self.key.symbol_or_name(); + } else { + s += self.key.name(); + } + s + } +} + +// ---------------------------------------------------------------------------- + impl RawInput { pub fn ui(&self, ui: &mut crate::Ui) { let Self { diff --git a/crates/egui/src/input_state.rs b/crates/egui/src/input_state.rs index 143fd6c31a4..1140bb16830 100644 --- a/crates/egui/src/input_state.rs +++ b/crates/egui/src/input_state.rs @@ -267,6 +267,14 @@ impl InputState { match_found } + /// Check if the given shortcut has been pressed. + /// + /// If so, `true` is returned and the key pressed is consumed, so that this will only return `true` once. + pub fn consume_shortcut(&mut self, shortcut: &KeyboardShortcut) -> bool { + let KeyboardShortcut { modifiers, key } = *shortcut; + self.consume_key(modifiers, key) + } + /// Was the given key pressed this frame? pub fn key_pressed(&self, desired_key: Key) -> bool { self.num_presses(desired_key) > 0 diff --git a/crates/egui/src/lib.rs b/crates/egui/src/lib.rs index 812cfec1e68..87a9090d23f 100644 --- a/crates/egui/src/lib.rs +++ b/crates/egui/src/lib.rs @@ -312,6 +312,7 @@ pub mod layers; mod layout; mod memory; pub mod menu; +pub mod os; mod painter; pub(crate) mod placer; mod response; diff --git a/crates/egui/src/os.rs b/crates/egui/src/os.rs new file mode 100644 index 00000000000..dea4dee1c7b --- /dev/null +++ b/crates/egui/src/os.rs @@ -0,0 +1,71 @@ +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum OperatingSystem { + /// Unknown OS - could be wasm + Unknown, + + /// Android OS. + Android, + + /// Apple iPhone OS. + IOS, + + /// Linux or Unix other than Android. + Nix, + + /// MacOS. + Mac, + + /// Windows. + Windows, +} + +impl Default for OperatingSystem { + fn default() -> Self { + Self::from_target_os() + } +} + +impl OperatingSystem { + pub const fn from_target_os() -> Self { + if cfg!(target_arch = "wasm32") { + Self::Unknown + } else if cfg!(target_os = "android") { + Self::Android + } else if cfg!(target_os = "ios") { + Self::IOS + } else if cfg!(target_os = "macos") { + Self::Mac + } else if cfg!(target_os = "windows") { + Self::Android + } else if cfg!(target_os = "linux") + || cfg!(target_os = "dragonfly") + || cfg!(target_os = "freebsd") + || cfg!(target_os = "netbsd") + || cfg!(target_os = "openbsd") + { + Self::Nix + } else { + Self::Unknown + } + } + + /// Helper: try to guess from the user-agent of a browser. + pub fn from_user_agent(user_agent: &str) -> Self { + if user_agent.contains("Android") { + Self::Android + } else if user_agent.contains("like Mac") { + Self::IOS + } else if user_agent.contains("Win") { + Self::Windows + } else if user_agent.contains("Mac") { + Self::Mac + } else if user_agent.contains("Linux") + || user_agent.contains("X11") + || user_agent.contains("Unix") + { + Self::Nix + } else { + Self::Unknown + } + } +} diff --git a/crates/egui/src/style.rs b/crates/egui/src/style.rs index 84de740322d..c28d0948623 100644 --- a/crates/egui/src/style.rs +++ b/crates/egui/src/style.rs @@ -21,11 +21,12 @@ pub enum TextStyle { /// Normal labels. Easily readable, doesn't take up too much space. Body, - /// Same size as [`Self::Body`], but used when monospace is important (for aligning number, code snippets, etc). + /// Same size as [`Self::Body`], but used when monospace is important (for code snippets, aligning numbers, etc). Monospace, /// Buttons. Maybe slightly bigger than [`Self::Body`]. - /// Signifies that he item is interactive. + /// + /// Signifies that he item can be interacted with. Button, /// Heading. Probably larger than [`Self::Body`]. diff --git a/crates/egui/src/widgets/button.rs b/crates/egui/src/widgets/button.rs index 0ed1fd5e076..19385470067 100644 --- a/crates/egui/src/widgets/button.rs +++ b/crates/egui/src/widgets/button.rs @@ -21,6 +21,7 @@ use crate::*; #[must_use = "You should put this widget in an ui with `ui.add(widget);`"] pub struct Button { text: WidgetText, + shortcut_text: WidgetText, wrap: Option, /// None means default for interact fill: Option, @@ -36,6 +37,7 @@ impl Button { pub fn new(text: impl Into) -> Self { Self { text: text.into(), + shortcut_text: Default::default(), wrap: None, fill: None, stroke: None, @@ -47,23 +49,16 @@ impl Button { } } - /// Creates a button with an image to the left of the text. The size of the image as displayed is defined by the size Vec2 provided. + /// Creates a button with an image to the left of the text. The size of the image as displayed is defined by the provided size. #[allow(clippy::needless_pass_by_value)] pub fn image_and_text( texture_id: TextureId, - size: impl Into, + image_size: impl Into, text: impl Into, ) -> Self { Self { - text: text.into(), - fill: None, - stroke: None, - sense: Sense::click(), - small: false, - frame: None, - wrap: None, - min_size: Vec2::ZERO, - image: Some(widgets::Image::new(texture_id, size)), + image: Some(widgets::Image::new(texture_id, image_size)), + ..Self::new(text) } } @@ -116,16 +111,28 @@ impl Button { self } + /// Set the minimum size of the button. pub fn min_size(mut self, min_size: Vec2) -> Self { self.min_size = min_size; self } + + /// Show some text on the right side of the button, in weak color. + /// + /// Designed for menu buttons, for setting a keyboard shortcut text (e.g. `Ctrl+S`). + /// + /// The text can be created with [`Context::format_shortcut`]. + pub fn shortcut_text(mut self, shortcut_text: impl Into) -> Self { + self.shortcut_text = shortcut_text.into(); + self + } } impl Widget for Button { fn ui(self, ui: &mut Ui) -> Response { let Button { text, + shortcut_text, wrap, fill, stroke, @@ -142,27 +149,51 @@ impl Widget for Button { if small { button_padding.y = 0.0; } - let total_extra = button_padding + button_padding; - - let wrap_width = ui.available_width() - total_extra.x; - let text = text.into_galley(ui, wrap, wrap_width, TextStyle::Button); - let mut desired_size = text.size() + 2.0 * button_padding; - if !small { - desired_size.y = desired_size.y.at_least(ui.spacing().interact_size.y); + let mut text_wrap_width = ui.available_width() - 2.0 * button_padding.x; + if let Some(image) = image { + text_wrap_width -= image.size().x + ui.spacing().icon_spacing; } - desired_size = desired_size.at_least(min_size); + if !shortcut_text.is_empty() { + text_wrap_width -= 60.0; // Some space for the shortcut text (which we never wrap). + } + + let text = text.into_galley(ui, wrap, text_wrap_width, TextStyle::Button); + let shortcut_text = (!shortcut_text.is_empty()) + .then(|| shortcut_text.into_galley(ui, Some(false), f32::INFINITY, TextStyle::Button)); + let mut desired_size = text.size(); if let Some(image) = image { desired_size.x += image.size().x + ui.spacing().icon_spacing; - desired_size.y = desired_size.y.max(image.size().y + 2.0 * button_padding.y); + desired_size.y = desired_size.y.max(image.size().y); + } + if let Some(shortcut_text) = &shortcut_text { + desired_size.x += ui.spacing().item_spacing.x + shortcut_text.size().x; + desired_size.y = desired_size.y.max(shortcut_text.size().y); } + if !small { + desired_size.y = desired_size.y.at_least(ui.spacing().interact_size.y); + } + desired_size += 2.0 * button_padding; + desired_size = desired_size.at_least(min_size); let (rect, response) = ui.allocate_at_least(desired_size, sense); response.widget_info(|| WidgetInfo::labeled(WidgetType::Button, text.text())); if ui.is_rect_visible(rect) { let visuals = ui.style().interact(&response); + + if frame { + let fill = fill.unwrap_or(visuals.bg_fill); + let stroke = stroke.unwrap_or(visuals.bg_stroke); + ui.painter().rect( + rect.expand(visuals.expansion), + visuals.rounding, + fill, + stroke, + ); + } + let text_pos = if let Some(image) = image { let icon_spacing = ui.spacing().icon_spacing; pos2( @@ -174,27 +205,27 @@ impl Widget for Button { .align_size_within_rect(text.size(), rect.shrink2(button_padding)) .min }; + text.paint_with_visuals(ui.painter(), text_pos, visuals); - if frame { - let fill = fill.unwrap_or(visuals.bg_fill); - let stroke = stroke.unwrap_or(visuals.bg_stroke); - ui.painter().rect( - rect.expand(visuals.expansion), - visuals.rounding, - fill, - stroke, + if let Some(shortcut_text) = shortcut_text { + let shortcut_text_pos = pos2( + rect.max.x - button_padding.x - shortcut_text.size().x, + rect.center().y - 0.5 * shortcut_text.size().y, + ); + shortcut_text.paint_with_fallback_color( + ui.painter(), + shortcut_text_pos, + ui.visuals().weak_text_color(), ); } - text.paint_with_visuals(ui.painter(), text_pos, visuals); - } - - if let Some(image) = image { - let image_rect = Rect::from_min_size( - pos2(rect.min.x, rect.center().y - 0.5 - (image.size().y / 2.0)), - image.size(), - ); - image.paint_at(ui, image_rect); + if let Some(image) = image { + let image_rect = Rect::from_min_size( + pos2(rect.min.x, rect.center().y - 0.5 - (image.size().y / 2.0)), + image.size(), + ); + image.paint_at(ui, image_rect); + } } response diff --git a/crates/egui_demo_lib/src/demo/demo_app_windows.rs b/crates/egui_demo_lib/src/demo/demo_app_windows.rs index c5fe3bb63e0..be5b2b1ff89 100644 --- a/crates/egui_demo_lib/src/demo/demo_app_windows.rs +++ b/crates/egui_demo_lib/src/demo/demo_app_windows.rs @@ -1,4 +1,4 @@ -use egui::{Context, ScrollArea, Ui}; +use egui::{Context, Modifiers, ScrollArea, Ui}; use std::collections::BTreeSet; use super::About; @@ -239,7 +239,7 @@ impl DemoWindows { fn desktop_ui(&mut self, ctx: &Context) { egui::SidePanel::right("egui_demo_panel") .resizable(false) - .default_width(145.0) + .default_width(150.0) .show(ctx, |ui| { egui::trace!(ui); ui.vertical_centered(|ui| { @@ -301,13 +301,42 @@ impl DemoWindows { // ---------------------------------------------------------------------------- fn file_menu_button(ui: &mut Ui) { + let organize_shortcut = + egui::KeyboardShortcut::new(Modifiers::ALT | Modifiers::SHIFT, egui::Key::O); + let reset_shortcut = + egui::KeyboardShortcut::new(Modifiers::ALT | Modifiers::SHIFT, egui::Key::R); + + // NOTE: we must check the shortcuts OUTSIDE of the actual "File" menu, + // or else they would only be checked if the "File" menu was actually open! + + if ui.input_mut().consume_shortcut(&organize_shortcut) { + ui.ctx().memory().reset_areas(); + } + + if ui.input_mut().consume_shortcut(&reset_shortcut) { + *ui.ctx().memory() = Default::default(); + } + ui.menu_button("File", |ui| { - if ui.button("Organize windows").clicked() { + ui.set_min_width(220.0); + ui.style_mut().wrap = Some(false); + + if ui + .add( + egui::Button::new("Organize Windows") + .shortcut_text(ui.ctx().format_shortcut(&organize_shortcut)), + ) + .clicked() + { ui.ctx().memory().reset_areas(); ui.close_menu(); } + if ui - .button("Reset egui memory") + .add( + egui::Button::new("Reset egui memory") + .shortcut_text(ui.ctx().format_shortcut(&reset_shortcut)), + ) .on_hover_text("Forget scroll, positions, sizes etc") .clicked() { diff --git a/crates/egui_demo_lib/src/easy_mark/easy_mark_editor.rs b/crates/egui_demo_lib/src/easy_mark/easy_mark_editor.rs index ae491f3f2f9..c5c64a8c01f 100644 --- a/crates/egui_demo_lib/src/easy_mark/easy_mark_editor.rs +++ b/crates/egui_demo_lib/src/easy_mark/easy_mark_editor.rs @@ -106,23 +106,42 @@ impl EasyMarkEditor { } } +pub const SHORTCUT_BOLD: KeyboardShortcut = KeyboardShortcut::new(Modifiers::COMMAND, Key::B); +pub const SHORTCUT_CODE: KeyboardShortcut = KeyboardShortcut::new(Modifiers::COMMAND, Key::N); +pub const SHORTCUT_ITALICS: KeyboardShortcut = KeyboardShortcut::new(Modifiers::COMMAND, Key::I); +pub const SHORTCUT_SUBSCRIPT: KeyboardShortcut = KeyboardShortcut::new(Modifiers::COMMAND, Key::L); +pub const SHORTCUT_SUPERSCRIPT: KeyboardShortcut = + KeyboardShortcut::new(Modifiers::COMMAND, Key::Y); +pub const SHORTCUT_STRIKETHROUGH: KeyboardShortcut = + KeyboardShortcut::new(Modifiers::CTRL.plus(Modifiers::SHIFT), Key::Q); +pub const SHORTCUT_UNDERLINE: KeyboardShortcut = + KeyboardShortcut::new(Modifiers::CTRL.plus(Modifiers::SHIFT), Key::W); +pub const SHORTCUT_INDENT: KeyboardShortcut = + KeyboardShortcut::new(Modifiers::CTRL.plus(Modifiers::SHIFT), Key::E); + fn nested_hotkeys_ui(ui: &mut egui::Ui) { - let _ = ui.label("CTRL+B *bold*"); - let _ = ui.label("CTRL+N `code`"); - let _ = ui.label("CTRL+I /italics/"); - let _ = ui.label("CTRL+L $subscript$"); - let _ = ui.label("CTRL+Y ^superscript^"); - let _ = ui.label("ALT+SHIFT+Q ~strikethrough~"); - let _ = ui.label("ALT+SHIFT+W _underline_"); - let _ = ui.label("ALT+SHIFT+E two spaces"); // Placeholder for tab indent + egui::Grid::new("shortcuts").striped(true).show(ui, |ui| { + let mut label = |shortcut, what| { + ui.label(what); + ui.weak(ui.ctx().format_shortcut(&shortcut)); + ui.end_row(); + }; + + label(SHORTCUT_BOLD, "*bold*"); + label(SHORTCUT_CODE, "`code`"); + label(SHORTCUT_ITALICS, "/italics/"); + label(SHORTCUT_SUBSCRIPT, "$subscript$"); + label(SHORTCUT_SUPERSCRIPT, "^superscript^"); + label(SHORTCUT_STRIKETHROUGH, "~strikethrough~"); + label(SHORTCUT_UNDERLINE, "_underline_"); + label(SHORTCUT_INDENT, "two spaces"); // Placeholder for tab indent + }); } fn shortcuts(ui: &Ui, code: &mut dyn TextBuffer, ccursor_range: &mut CCursorRange) -> bool { let mut any_change = false; - if ui - .input_mut() - .consume_key(egui::Modifiers::ALT_SHIFT, Key::E) - { + + if ui.input_mut().consume_shortcut(&SHORTCUT_INDENT) { // This is a placeholder till we can indent the active line any_change = true; let [primary, _secondary] = ccursor_range.sorted(); @@ -131,20 +150,22 @@ fn shortcuts(ui: &Ui, code: &mut dyn TextBuffer, ccursor_range: &mut CCursorRang ccursor_range.primary.index += advance; ccursor_range.secondary.index += advance; } - for (modifier, key, surrounding) in [ - (egui::Modifiers::COMMAND, Key::B, "*"), // *bold* - (egui::Modifiers::COMMAND, Key::N, "`"), // `code` - (egui::Modifiers::COMMAND, Key::I, "/"), // /italics/ - (egui::Modifiers::COMMAND, Key::L, "$"), // $subscript$ - (egui::Modifiers::COMMAND, Key::Y, "^"), // ^superscript^ - (egui::Modifiers::ALT_SHIFT, Key::Q, "~"), // ~strikethrough~ - (egui::Modifiers::ALT_SHIFT, Key::W, "_"), // _underline_ + + for (shortcut, surrounding) in [ + (SHORTCUT_BOLD, "*"), + (SHORTCUT_CODE, "`"), + (SHORTCUT_ITALICS, "/"), + (SHORTCUT_SUBSCRIPT, "$"), + (SHORTCUT_SUPERSCRIPT, "^"), + (SHORTCUT_STRIKETHROUGH, "~"), + (SHORTCUT_UNDERLINE, "_"), ] { - if ui.input_mut().consume_key(modifier, key) { + if ui.input_mut().consume_shortcut(&shortcut) { any_change = true; toggle_surrounding(code, ccursor_range, surrounding); }; } + any_change } diff --git a/crates/epaint/CHANGELOG.md b/crates/epaint/CHANGELOG.md index ea107312df0..cb060a20e33 100644 --- a/crates/epaint/CHANGELOG.md +++ b/crates/epaint/CHANGELOG.md @@ -5,6 +5,7 @@ All notable changes to the epaint crate will be documented in this file. ## Unreleased * ⚠️ BREAKING: Fix text being too small ([#2069](https://github.com/emilk/egui/pull/2069)). * ⚠️ BREAKING: epaint now expects integrations to do all color blending in gamma space ([#2071](https://github.com/emilk/egui/pull/2071)). +* Add `Fonts::has_glyph(s)` for querying if a glyph is supported ([#2202](https://github.com/emilk/egui/pull/2202)). ## 0.19.0 - 2022-08-20 diff --git a/crates/epaint/src/text/font.rs b/crates/epaint/src/text/font.rs index 698956b02db..ee0574ae6e2 100644 --- a/crates/epaint/src/text/font.rs +++ b/crates/epaint/src/text/font.rs @@ -31,7 +31,7 @@ impl UvRect { } } -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, PartialEq)] pub struct GlyphInfo { pub(crate) id: ab_glyph::GlyphId, @@ -265,6 +265,12 @@ impl Font { slf } + pub fn preload_characters(&mut self, s: &str) { + for c in s.chars() { + self.glyph_info(c); + } + } + pub fn preload_common_characters(&mut self) { // Preload the printable ASCII characters [32, 126] (which excludes control codes): const FIRST_ASCII: usize = 32; // 32 == space @@ -276,7 +282,7 @@ impl Font { self.glyph_info(crate::text::PASSWORD_REPLACEMENT_CHAR); } - /// All supported characters + /// All supported characters. pub fn characters(&mut self) -> &BTreeSet { self.characters.get_or_insert_with(|| { let mut characters = BTreeSet::new(); @@ -310,6 +316,16 @@ impl Font { self.glyph_info(c).1.advance_width } + /// Can we display this glyph? + pub fn has_glyph(&mut self, c: char) -> bool { + self.glyph_info(c) != self.replacement_glyph // TODO(emilk): this is a false negative if the user asks about the replacement character itself 🤦‍♂️ + } + + /// Can we display all the glyphs in this text? + pub fn has_glyphs(&mut self, s: &str) -> bool { + s.chars().all(|c| self.has_glyph(c)) + } + /// `\n` will (intentionally) show up as the replacement character. fn glyph_info(&mut self, c: char) -> (FontIndex, GlyphInfo) { if let Some(font_index_glyph_info) = self.glyph_info_cache.get(&c) { diff --git a/crates/epaint/src/text/fonts.rs b/crates/epaint/src/text/fonts.rs index 5fdc82fef80..4857213c21f 100644 --- a/crates/epaint/src/text/fonts.rs +++ b/crates/epaint/src/text/fonts.rs @@ -430,6 +430,17 @@ impl Fonts { self.lock().fonts.glyph_width(font_id, c) } + /// Can we display this glyph? + #[inline] + pub fn has_glyph(&self, font_id: &FontId, c: char) -> bool { + self.lock().fonts.has_glyph(font_id, c) + } + + /// Can we display all the glyphs in this text? + pub fn has_glyphs(&self, font_id: &FontId, s: &str) -> bool { + self.lock().fonts.has_glyphs(font_id, s) + } + /// Height of one row of text in points #[inline] pub fn row_height(&self, font_id: &FontId) -> f32 { @@ -627,6 +638,16 @@ impl FontsImpl { self.font(font_id).glyph_width(c) } + /// Can we display this glyph? + pub fn has_glyph(&mut self, font_id: &FontId, c: char) -> bool { + self.font(font_id).has_glyph(c) + } + + /// Can we display all the glyphs in this text? + pub fn has_glyphs(&mut self, font_id: &FontId, s: &str) -> bool { + self.font(font_id).has_glyphs(s) + } + /// Height of one row of text. In points fn row_height(&mut self, font_id: &FontId) -> f32 { self.font(font_id).row_height()