Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Keyboard shortcut helpers #2202

Merged
merged 17 commits into from
Oct 31, 2022
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)).
Expand Down
2 changes: 1 addition & 1 deletion crates/eframe/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions crates/eframe/src/epi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
Expand Down
8 changes: 8 additions & 0 deletions crates/eframe/src/web/backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down Expand Up @@ -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,
Expand All @@ -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);
Expand Down
58 changes: 57 additions & 1 deletion crates/egui/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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, *};

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
195 changes: 195 additions & 0 deletions crates/egui/src/data/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -464,6 +466,76 @@ impl std::ops::BitOr for Modifiers {
}
}

// ----------------------------------------------------------------------------

/// 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`)
Expand Down Expand Up @@ -563,6 +635,129 @@ pub enum Key {
F20,
}

impl Key {
/// Emoji or name representing the key
pub fn symbol_or_name(self) -> &'static str {
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 {
Expand Down
8 changes: 8 additions & 0 deletions crates/egui/src/input_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions crates/egui/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading