Browse Source

Keyboard shortcut helpers (#2202)

* eframe web: Add WebInfo::user_agent

* Deprecate `Modifier::ALT_SHIFT`

* Add code for formatting Modifiers and Key

* Add type KeyboardShortcut

* Code cleanup

* Add Context::os/set_os to query/set what OS egui believes it is on

* Add Fonts::has_glyph(s)

* Add helper function for formatting keyboard shortcuts

* Faster code

* Add way to set a shortcut text on menu buttons

* Cleanup

* format_keyboard_shortcut -> format_shortcut

* Add TODO about supporting more keyboard sumbols

* Modifiers::plus

* Use the new keyboard shortcuts in emark editor demo

* Explain why ALT+SHIFT is a bad modifier combo

* Fix doctest
pull/2208/head
Emil Ernerfeldt 2 years ago
committed by GitHub
parent
commit
02b9d2d082
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      CHANGELOG.md
  2. 2
      crates/eframe/CHANGELOG.md
  3. 3
      crates/eframe/src/epi.rs
  4. 8
      crates/eframe/src/web/backend.rs
  5. 58
      crates/egui/src/context.rs
  6. 247
      crates/egui/src/data/input.rs
  7. 8
      crates/egui/src/input_state.rs
  8. 1
      crates/egui/src/lib.rs
  9. 71
      crates/egui/src/os.rs
  10. 5
      crates/egui/src/style.rs
  11. 105
      crates/egui/src/widgets/button.rs
  12. 37
      crates/egui_demo_lib/src/demo/demo_app_windows.rs
  13. 63
      crates/egui_demo_lib/src/easy_mark/easy_mark_editor.rs
  14. 1
      crates/epaint/CHANGELOG.md
  15. 20
      crates/epaint/src/text/font.rs
  16. 21
      crates/epaint/src/text/fonts.rs

3
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)).

2
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

3
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,
}

8
crates/eframe/src/web/backend.rs

@ -87,6 +87,10 @@ impl IsDestroyed {
// ----------------------------------------------------------------------------
fn user_agent() -> Option<String> {
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);

58
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 {

247
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 {

8
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

1
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;

71
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
}
}
}

5
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`].

105
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<bool>,
/// None means default for interact
fill: Option<Color32>,
@ -36,6 +37,7 @@ impl Button {
pub fn new(text: impl Into<WidgetText>) -> 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<Vec2>,
image_size: impl Into<Vec2>,
text: impl Into<WidgetText>,
) -> 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<WidgetText>) -> 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

37
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()
{

63
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
}

1
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

20
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<char> {
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) {

21
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()

Loading…
Cancel
Save