Add helpers for zooming an app using Ctrl+Plus and Ctrl+Minus (#2239)

* Using tracing-subscriber in hello_world example

* Add Key::Plus/Minus/Equals

* Warn if failing to guess OS from User-Agent

* Remove jitter when using Context::set_pixels_per_point

* Demo app: zoom in/out using ⌘+ and ⌘-

* Demo app: make backend panel GUI scale slider better

* Optimize debug builds a bit

* typo

* Update changelog

* Add helper module `egui::gui_zoom` for zooming an app

* Better names, and update changelog

* Combine Plus and Equals keys

* Last fix

* Fix docs
This commit is contained in:
Emil Ernerfeldt 2022-11-05 11:18:13 +01:00 committed by GitHub
parent 25718f2774
commit a0b3f1126b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 290 additions and 112 deletions

View file

@ -13,10 +13,13 @@ NOTE: [`epaint`](crates/epaint/CHANGELOG.md), [`eframe`](crates/eframe/CHANGELOG
* 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)).
* Texture loading now takes a `TexureOptions` with minification and magnification filters ([#2224](https://github.com/emilk/egui/pull/2224)).
* Added `Key::Minus` and `Key::Equals` ([#2239](https://github.com/emilk/egui/pull/2239)).
* Added `egui::gui_zoom` module with helpers for scaling the whole GUI of an app ([#2239](https://github.com/emilk/egui/pull/2239)).
### Fixed 🐛
* ⚠️ BREAKING: Fix text being too small ([#2069](https://github.com/emilk/egui/pull/2069)).
* Improved text rendering ([#2071](https://github.com/emilk/egui/pull/2071)).
* Less jitter when calling `Context::set_pixels_per_point` ([#2239](https://github.com/emilk/egui/pull/2239)).
## 0.19.0 - 2022-08-20

1
Cargo.lock generated
View file

@ -1926,6 +1926,7 @@ name = "hello_world"
version = "0.1.0"
dependencies = [
"eframe",
"tracing-subscriber",
]
[[package]]

View file

@ -15,9 +15,6 @@ members = [
"examples/*",
]
[profile.dev]
split-debuginfo = "unpacked" # faster debug builds on mac
[profile.release]
# lto = true # VERY slightly smaller wasm
# opt-level = 's' # 10-20% smaller wasm compared to `opt-level = 3`
@ -26,3 +23,12 @@ opt-level = 2 # fast and small wasm, basically same as `opt-level = 's'`
# opt-level = 3 # unecessarily large wasm for no performance gain
# debug = true # include debug symbols, useful when profiling wasm
[profile.dev]
split-debuginfo = "unpacked" # faster debug builds on mac
opt-level = 1 # Make debug builds run faster
# Optimize all dependencies even in debug builds (does not affect workspace packages):
[profile.dev.package."*"]
opt-level = 2

View file

@ -113,83 +113,88 @@ pub fn should_ignore_key(key: &str) -> bool {
/// Web sends all all keys as strings, so it is up to us to figure out if it is
/// a real text input or the name of a key.
pub fn translate_key(key: &str) -> Option<egui::Key> {
use egui::Key;
match key {
"ArrowDown" => Some(egui::Key::ArrowDown),
"ArrowLeft" => Some(egui::Key::ArrowLeft),
"ArrowRight" => Some(egui::Key::ArrowRight),
"ArrowUp" => Some(egui::Key::ArrowUp),
"ArrowDown" => Some(Key::ArrowDown),
"ArrowLeft" => Some(Key::ArrowLeft),
"ArrowRight" => Some(Key::ArrowRight),
"ArrowUp" => Some(Key::ArrowUp),
"Esc" | "Escape" => Some(egui::Key::Escape),
"Tab" => Some(egui::Key::Tab),
"Backspace" => Some(egui::Key::Backspace),
"Enter" => Some(egui::Key::Enter),
"Space" | " " => Some(egui::Key::Space),
"Esc" | "Escape" => Some(Key::Escape),
"Tab" => Some(Key::Tab),
"Backspace" => Some(Key::Backspace),
"Enter" => Some(Key::Enter),
"Space" | " " => Some(Key::Space),
"Help" | "Insert" => Some(egui::Key::Insert),
"Delete" => Some(egui::Key::Delete),
"Home" => Some(egui::Key::Home),
"End" => Some(egui::Key::End),
"PageUp" => Some(egui::Key::PageUp),
"PageDown" => Some(egui::Key::PageDown),
"Help" | "Insert" => Some(Key::Insert),
"Delete" => Some(Key::Delete),
"Home" => Some(Key::Home),
"End" => Some(Key::End),
"PageUp" => Some(Key::PageUp),
"PageDown" => Some(Key::PageDown),
"0" => Some(egui::Key::Num0),
"1" => Some(egui::Key::Num1),
"2" => Some(egui::Key::Num2),
"3" => Some(egui::Key::Num3),
"4" => Some(egui::Key::Num4),
"5" => Some(egui::Key::Num5),
"6" => Some(egui::Key::Num6),
"7" => Some(egui::Key::Num7),
"8" => Some(egui::Key::Num8),
"9" => Some(egui::Key::Num9),
"-" => Some(Key::Minus),
"+" | "=" => Some(Key::PlusEquals),
"a" | "A" => Some(egui::Key::A),
"b" | "B" => Some(egui::Key::B),
"c" | "C" => Some(egui::Key::C),
"d" | "D" => Some(egui::Key::D),
"e" | "E" => Some(egui::Key::E),
"f" | "F" => Some(egui::Key::F),
"g" | "G" => Some(egui::Key::G),
"h" | "H" => Some(egui::Key::H),
"i" | "I" => Some(egui::Key::I),
"j" | "J" => Some(egui::Key::J),
"k" | "K" => Some(egui::Key::K),
"l" | "L" => Some(egui::Key::L),
"m" | "M" => Some(egui::Key::M),
"n" | "N" => Some(egui::Key::N),
"o" | "O" => Some(egui::Key::O),
"p" | "P" => Some(egui::Key::P),
"q" | "Q" => Some(egui::Key::Q),
"r" | "R" => Some(egui::Key::R),
"s" | "S" => Some(egui::Key::S),
"t" | "T" => Some(egui::Key::T),
"u" | "U" => Some(egui::Key::U),
"v" | "V" => Some(egui::Key::V),
"w" | "W" => Some(egui::Key::W),
"x" | "X" => Some(egui::Key::X),
"y" | "Y" => Some(egui::Key::Y),
"z" | "Z" => Some(egui::Key::Z),
"0" => Some(Key::Num0),
"1" => Some(Key::Num1),
"2" => Some(Key::Num2),
"3" => Some(Key::Num3),
"4" => Some(Key::Num4),
"5" => Some(Key::Num5),
"6" => Some(Key::Num6),
"7" => Some(Key::Num7),
"8" => Some(Key::Num8),
"9" => Some(Key::Num9),
"F1" => Some(egui::Key::F1),
"F2" => Some(egui::Key::F2),
"F3" => Some(egui::Key::F3),
"F4" => Some(egui::Key::F4),
"F5" => Some(egui::Key::F5),
"F6" => Some(egui::Key::F6),
"F7" => Some(egui::Key::F7),
"F8" => Some(egui::Key::F8),
"F9" => Some(egui::Key::F9),
"F10" => Some(egui::Key::F10),
"F11" => Some(egui::Key::F11),
"F12" => Some(egui::Key::F12),
"F13" => Some(egui::Key::F13),
"F14" => Some(egui::Key::F14),
"F15" => Some(egui::Key::F15),
"F16" => Some(egui::Key::F16),
"F17" => Some(egui::Key::F17),
"F18" => Some(egui::Key::F18),
"F19" => Some(egui::Key::F19),
"F20" => Some(egui::Key::F20),
"a" | "A" => Some(Key::A),
"b" | "B" => Some(Key::B),
"c" | "C" => Some(Key::C),
"d" | "D" => Some(Key::D),
"e" | "E" => Some(Key::E),
"f" | "F" => Some(Key::F),
"g" | "G" => Some(Key::G),
"h" | "H" => Some(Key::H),
"i" | "I" => Some(Key::I),
"j" | "J" => Some(Key::J),
"k" | "K" => Some(Key::K),
"l" | "L" => Some(Key::L),
"m" | "M" => Some(Key::M),
"n" | "N" => Some(Key::N),
"o" | "O" => Some(Key::O),
"p" | "P" => Some(Key::P),
"q" | "Q" => Some(Key::Q),
"r" | "R" => Some(Key::R),
"s" | "S" => Some(Key::S),
"t" | "T" => Some(Key::T),
"u" | "U" => Some(Key::U),
"v" | "V" => Some(Key::V),
"w" | "W" => Some(Key::W),
"x" | "X" => Some(Key::X),
"y" | "Y" => Some(Key::Y),
"z" | "Z" => Some(Key::Z),
"F1" => Some(Key::F1),
"F2" => Some(Key::F2),
"F3" => Some(Key::F3),
"F4" => Some(Key::F4),
"F5" => Some(Key::F5),
"F6" => Some(Key::F6),
"F7" => Some(Key::F7),
"F8" => Some(Key::F8),
"F9" => Some(Key::F9),
"F10" => Some(Key::F10),
"F11" => Some(Key::F11),
"F12" => Some(Key::F12),
"F13" => Some(Key::F13),
"F14" => Some(Key::F14),
"F15" => Some(Key::F15),
"F16" => Some(Key::F16),
"F17" => Some(Key::F17),
"F18" => Some(Key::F18),
"F19" => Some(Key::F19),
"F20" => Some(Key::F20),
_ => None,
}

View file

@ -710,6 +710,11 @@ fn translate_virtual_key_code(key: winit::event::VirtualKeyCode) -> Option<egui:
VirtualKeyCode::PageUp => Key::PageUp,
VirtualKeyCode::PageDown => Key::PageDown,
VirtualKeyCode::Minus => Key::Minus,
// Using Mac the key with the Plus sign on it is reported as the Equals key
// (with both English and Swedish keyboard).
VirtualKeyCode::Equals => Key::PlusEquals,
VirtualKeyCode::Key0 | VirtualKeyCode::Numpad0 => Key::Num0,
VirtualKeyCode::Key1 | VirtualKeyCode::Numpad1 => Key::Num1,
VirtualKeyCode::Key2 | VirtualKeyCode::Numpad2 => Key::Num2,

View file

@ -65,18 +65,25 @@ struct ContextImpl {
}
impl ContextImpl {
fn begin_frame_mut(&mut self, new_raw_input: RawInput) {
fn begin_frame_mut(&mut self, mut new_raw_input: RawInput) {
self.has_requested_repaint_this_frame = false; // allow new calls during the frame
if let Some(new_pixels_per_point) = self.memory.new_pixels_per_point.take() {
new_raw_input.pixels_per_point = Some(new_pixels_per_point);
// This is a bit hacky, but is required to avoid jitter:
let ratio = self.input.pixels_per_point / new_pixels_per_point;
let mut rect = self.input.screen_rect;
rect.min = (ratio * rect.min.to_vec2()).to_pos2();
rect.max = (ratio * rect.max.to_vec2()).to_pos2();
new_raw_input.screen_rect = Some(rect);
}
self.memory.begin_frame(&self.input, &new_raw_input);
self.input = std::mem::take(&mut self.input)
.begin_frame(new_raw_input, self.requested_repaint_last_frame);
if let Some(new_pixels_per_point) = self.memory.new_pixels_per_point.take() {
self.input.pixels_per_point = new_pixels_per_point;
}
self.frame_state.begin_frame(&self.input);
self.update_fonts_mut();

View file

@ -13,10 +13,10 @@ use crate::emath::*;
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct RawInput {
/// Position and size of the area that egui should use.
/// Position and size of the area that egui should use, in points.
/// Usually you would set this to
///
/// `Some(Rect::from_pos_size(Default::default(), screen_size))`.
/// `Some(Rect::from_pos_size(Default::default(), screen_size_in_points))`.
///
/// but you could also constrain egui to some smaller portion of your window if you like.
///
@ -516,13 +516,13 @@ impl ModifierNames<'static> {
concat: "",
};
/// Alt, Ctrl, Shift, Command
/// Alt, Ctrl, Shift, Cmd
pub const NAMES: Self = Self {
is_short: false,
alt: "Alt",
ctrl: "Ctrl",
shift: "Shift",
mac_cmd: "Command",
mac_cmd: "Cmd",
concat: "+",
};
}
@ -585,6 +585,11 @@ pub enum Key {
PageUp,
PageDown,
/// The virtual keycode for the Minus key.
Minus,
/// The virtual keycode for the Plus/Equals key.
PlusEquals,
/// Either from the main row or from the numpad.
Num0,
/// Either from the main row or from the numpad.
@ -667,6 +672,8 @@ impl Key {
Key::ArrowLeft => "",
Key::ArrowRight => "",
Key::ArrowUp => "",
Key::Minus => "-",
Key::PlusEquals => "+",
_ => self.name(),
}
}
@ -689,6 +696,8 @@ impl Key {
Key::End => "End",
Key::PageUp => "PageUp",
Key::PageDown => "PageDown",
Key::Minus => "Minus",
Key::PlusEquals => "Plus",
Key::Num0 => "0",
Key::Num1 => "1",
Key::Num2 => "2",

117
crates/egui/src/gui_zoom.rs Normal file
View file

@ -0,0 +1,117 @@
//! Helpers for zooming the whole GUI of an app (changing [`Context::pixels_per_point`].
//!
use crate::*;
/// The suggested keyboard shortcuts for global gui zooming.
pub mod kb_shortcuts {
use super::*;
pub const ZOOM_IN: KeyboardShortcut =
KeyboardShortcut::new(Modifiers::COMMAND, Key::PlusEquals);
pub const ZOOM_OUT: KeyboardShortcut = KeyboardShortcut::new(Modifiers::COMMAND, Key::Minus);
pub const ZOOM_RESET: KeyboardShortcut = KeyboardShortcut::new(Modifiers::COMMAND, Key::Num0);
}
/// Let the user scale the GUI (change `Context::pixels_per_point`) by pressing
/// Cmd+Plus, Cmd+Minus or Cmd+0, just like in a browser.
///
/// When using [`eframe`](https://github.com/emilk/egui/tree/master/crates/eframe), you want to call this as:
/// ```ignore
/// // On web, the browser controls the gui zoom.
/// if !frame.is_web() {
/// egui::gui_zoom::zoom_with_keyboard_shortcuts(
/// ctx,
/// frame.info().native_pixels_per_point,
/// );
/// }
/// ```
pub fn zoom_with_keyboard_shortcuts(ctx: &Context, native_pixels_per_point: Option<f32>) {
if ctx.input_mut().consume_shortcut(&kb_shortcuts::ZOOM_RESET) {
if let Some(native_pixels_per_point) = native_pixels_per_point {
ctx.set_pixels_per_point(native_pixels_per_point);
}
} else {
if ctx.input_mut().consume_shortcut(&kb_shortcuts::ZOOM_IN) {
zoom_in(ctx);
}
if ctx.input_mut().consume_shortcut(&kb_shortcuts::ZOOM_OUT) {
zoom_out(ctx);
}
}
}
const MIN_PIXELS_PER_POINT: f32 = 0.2;
const MAX_PIXELS_PER_POINT: f32 = 4.0;
/// Make everything larger.
pub fn zoom_in(ctx: &Context) {
let mut pixels_per_point = ctx.pixels_per_point();
pixels_per_point += 0.1;
pixels_per_point = pixels_per_point.clamp(MIN_PIXELS_PER_POINT, MAX_PIXELS_PER_POINT);
pixels_per_point = (pixels_per_point * 10.).round() / 10.;
ctx.set_pixels_per_point(pixels_per_point);
}
/// Make everything smaller.
pub fn zoom_out(ctx: &Context) {
let mut pixels_per_point = ctx.pixels_per_point();
pixels_per_point -= 0.1;
pixels_per_point = pixels_per_point.clamp(MIN_PIXELS_PER_POINT, MAX_PIXELS_PER_POINT);
pixels_per_point = (pixels_per_point * 10.).round() / 10.;
ctx.set_pixels_per_point(pixels_per_point);
}
/// Show buttons for zooming the ui.
///
/// This is meant to be called from within a menu (See [`Ui::menu_button`]).
///
/// When using [`eframe`](https://github.com/emilk/egui/tree/master/crates/eframe), you want to call this as:
/// ```ignore
/// // On web, the browser controls the gui zoom.
/// if !frame.is_web() {
/// ui.menu_button("View", |ui| {
/// egui::gui_zoom::zoom_menu_buttons(
/// ui,
/// frame.info().native_pixels_per_point,
/// );
/// });
/// }
/// ```
pub fn zoom_menu_buttons(ui: &mut Ui, native_pixels_per_point: Option<f32>) {
if ui
.add_enabled(
ui.ctx().pixels_per_point() < MAX_PIXELS_PER_POINT,
Button::new("Zoom In").shortcut_text(ui.ctx().format_shortcut(&kb_shortcuts::ZOOM_IN)),
)
.clicked()
{
zoom_in(ui.ctx());
ui.close_menu();
}
if ui
.add_enabled(
ui.ctx().pixels_per_point() > MIN_PIXELS_PER_POINT,
Button::new("Zoom Out")
.shortcut_text(ui.ctx().format_shortcut(&kb_shortcuts::ZOOM_OUT)),
)
.clicked()
{
zoom_out(ui.ctx());
ui.close_menu();
}
if let Some(native_pixels_per_point) = native_pixels_per_point {
if ui
.add_enabled(
ui.ctx().pixels_per_point() != native_pixels_per_point,
Button::new("Reset Zoom")
.shortcut_text(ui.ctx().format_shortcut(&kb_shortcuts::ZOOM_RESET)),
)
.clicked()
{
ui.ctx().set_pixels_per_point(native_pixels_per_point);
ui.close_menu();
}
}
}

View file

@ -305,6 +305,7 @@ mod context;
mod data;
mod frame_state;
pub(crate) mod grid;
pub mod gui_zoom;
mod id;
mod input_state;
pub mod introspection;

View file

@ -65,6 +65,11 @@ impl OperatingSystem {
{
Self::Nix
} else {
#[cfg(feature = "tracing")]
tracing::warn!(
"egui: Failed to guess operating system from User-Agent {:?}. Please file an issue at https://github.com/emilk/egui/issues",
user_agent);
Self::Unknown
}
}

View file

@ -161,13 +161,10 @@ impl BackendPanel {
ui.monospace(format!("{:#?}", frame.info().web_info.location));
});
// For instance: `eframe` web sets `pixels_per_point` every frame to force
// egui to use the same scale as the web zoom factor.
let integration_controls_pixels_per_point = ui.input().raw.pixels_per_point.is_some();
// On web, the browser controls `pixels_per_point`.
let integration_controls_pixels_per_point = frame.is_web();
if !integration_controls_pixels_per_point {
if let Some(new_pixels_per_point) = self.pixels_per_point_ui(ui, &frame.info()) {
ui.ctx().set_pixels_per_point(new_pixels_per_point);
}
self.pixels_per_point_ui(ui, &frame.info());
}
#[cfg(not(target_arch = "wasm32"))]
@ -202,27 +199,36 @@ impl BackendPanel {
}
}
fn pixels_per_point_ui(
&mut self,
ui: &mut egui::Ui,
info: &eframe::IntegrationInfo,
) -> Option<f32> {
let pixels_per_point = self.pixels_per_point.get_or_insert_with(|| {
info.native_pixels_per_point
.unwrap_or_else(|| ui.ctx().pixels_per_point())
});
fn pixels_per_point_ui(&mut self, ui: &mut egui::Ui, info: &eframe::IntegrationInfo) {
let pixels_per_point = self
.pixels_per_point
.get_or_insert_with(|| ui.ctx().pixels_per_point());
let mut reset = false;
ui.horizontal(|ui| {
ui.spacing_mut().slider_width = 90.0;
ui.add(
let response = ui
.add(
egui::Slider::new(pixels_per_point, 0.5..=5.0)
.logarithmic(true)
.clamp_to_range(true)
.text("Scale"),
)
.on_hover_text("Physical pixels per point.");
if response.drag_released() {
// We wait until mouse release to activate:
ui.ctx().set_pixels_per_point(*pixels_per_point);
reset = true;
} else if !response.is_pointer_button_down_on() {
// When not dragging, show the current pixels_per_point so others can change it.
reset = true;
}
if let Some(native_pixels_per_point) = info.native_pixels_per_point {
let enabled = *pixels_per_point != native_pixels_per_point;
let enabled = ui.ctx().pixels_per_point() != native_pixels_per_point;
if ui
.add_enabled(enabled, egui::Button::new("Reset"))
.on_hover_text(format!(
@ -231,16 +237,13 @@ impl BackendPanel {
))
.clicked()
{
*pixels_per_point = native_pixels_per_point;
ui.ctx().set_pixels_per_point(native_pixels_per_point);
}
}
});
// We wait until mouse release to activate:
if ui.ctx().is_using_pointer() {
None
} else {
Some(*pixels_per_point)
if reset {
self.pixels_per_point = None;
}
}

View file

@ -209,6 +209,11 @@ impl eframe::App for WrapApp {
self.state.backend_panel.end_of_frame(ctx);
self.ui_file_drag_and_drop(ctx);
// On web, the browser controls `pixels_per_point`.
if !frame.is_web() {
egui::gui_zoom::zoom_with_keyboard_shortcuts(ctx, frame.info().native_pixels_per_point);
}
}
#[cfg(feature = "glow")]

View file

@ -321,6 +321,13 @@ fn file_menu_button(ui: &mut Ui) {
ui.set_min_width(220.0);
ui.style_mut().wrap = Some(false);
// On the web the browser controls the zoom
#[cfg(not(target_arch = "wasm32"))]
{
egui::gui_zoom::zoom_menu_buttons(ui, None);
ui.separator();
}
if ui
.add(
egui::Button::new("Organize Windows")

View file

@ -10,3 +10,4 @@ publish = false
[dependencies]
eframe = { path = "../../crates/eframe" }
tracing-subscriber = "0.3"

View file

@ -3,6 +3,9 @@
use eframe::egui;
fn main() {
// Log to stdout (if you run with `RUST_LOG=debug`).
tracing_subscriber::fmt::init();
let options = eframe::NativeOptions::default();
eframe::run_native(
"My egui App",