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
This commit is contained in:
parent
d97282cd92
commit
02b9d2d082
16 changed files with 573 additions and 84 deletions
|
@ -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)).
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
Normal file
71
crates/egui/src/os.rs
Normal file
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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`].
|
||||
|
|
|
@ -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,38 +149,39 @@ 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 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;
|
||||
}
|
||||
if !shortcut_text.is_empty() {
|
||||
text_wrap_width -= 60.0; // Some space for the shortcut text (which we never wrap).
|
||||
}
|
||||
|
||||
let mut desired_size = text.size() + 2.0 * button_padding;
|
||||
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);
|
||||
}
|
||||
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);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
let text_pos = if let Some(image) = image {
|
||||
let icon_spacing = ui.spacing().icon_spacing;
|
||||
pos2(
|
||||
rect.min.x + button_padding.x + image.size().x + icon_spacing,
|
||||
rect.center().y - 0.5 * text.size().y,
|
||||
)
|
||||
} else {
|
||||
ui.layout()
|
||||
.align_size_within_rect(text.size(), rect.shrink2(button_padding))
|
||||
.min
|
||||
};
|
||||
|
||||
if frame {
|
||||
let fill = fill.unwrap_or(visuals.bg_fill);
|
||||
|
@ -186,7 +194,29 @@ impl Widget for Button {
|
|||
);
|
||||
}
|
||||
|
||||
let text_pos = if let Some(image) = image {
|
||||
let icon_spacing = ui.spacing().icon_spacing;
|
||||
pos2(
|
||||
rect.min.x + button_padding.x + image.size().x + icon_spacing,
|
||||
rect.center().y - 0.5 * text.size().y,
|
||||
)
|
||||
} else {
|
||||
ui.layout()
|
||||
.align_size_within_rect(text.size(), rect.shrink2(button_padding))
|
||||
.min
|
||||
};
|
||||
text.paint_with_visuals(ui.painter(), text_pos, visuals);
|
||||
|
||||
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(),
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(image) = image {
|
||||
|
@ -196,6 +226,7 @@ impl Widget for Button {
|
|||
);
|
||||
image.paint_at(ui, image_rect);
|
||||
}
|
||||
}
|
||||
|
||||
response
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
{
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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…
Reference in a new issue