[app] Add demo app slider to change scale of all of Egui

This commit is contained in:
Emil Ernerfeldt 2020-10-17 23:54:46 +02:00
parent 251cde60f0
commit a4e19d7207
9 changed files with 149 additions and 85 deletions

View file

@ -5,6 +5,7 @@
* `ui.horizontal(...)` etc returns `Response` * `ui.horizontal(...)` etc returns `Response`
* Add ability to override text color with `visuals.override_text_color` * Add ability to override text color with `visuals.override_text_color`
* Refactored the interface for `egui::app::App` * Refactored the interface for `egui::app::App`
* Demo App: Add slider to scale all of Egui
## 0.2.0 - 2020-10-10 ## 0.2.0 - 2020-10-10

View file

@ -24,12 +24,14 @@ pub trait App {
fn on_exit(&mut self, _storage: &mut dyn Storage) {} fn on_exit(&mut self, _storage: &mut dyn Storage) {}
} }
#[derive(Clone, Debug)]
pub struct WebInfo { pub struct WebInfo {
/// e.g. "#fragment" part of "www.example.com/index.html#fragment" /// e.g. "#fragment" part of "www.example.com/index.html#fragment"
pub web_location_hash: String, pub web_location_hash: String,
} }
/// Information about the backend passed to the use app each frame. /// Information about the backend passed to the use app each frame.
#[derive(Clone, Debug)]
pub struct BackendInfo { pub struct BackendInfo {
/// If the app is running in a Web context, this returns information about the environment. /// If the app is running in a Web context, this returns information about the environment.
pub web_info: Option<WebInfo>, pub web_info: Option<WebInfo>,
@ -41,14 +43,20 @@ pub struct BackendInfo {
/// Local time. Used for the clock in the demo app. /// Local time. Used for the clock in the demo app.
/// Set to `None` if you don't know. /// Set to `None` if you don't know.
pub seconds_since_midnight: Option<f64>, pub seconds_since_midnight: Option<f64>,
/// The OS native pixels-per-point
pub native_pixels_per_point: Option<f32>,
} }
/// Action that can be taken by the user app. /// Action that can be taken by the user app.
#[derive(Clone, Copy, Debug, Default)] #[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct AppOutput { pub struct AppOutput {
/// Set to `true` to stop the app. /// Set to `true` to stop the app.
/// This does nothing for web apps. /// This does nothing for web apps.
pub quit: bool, pub quit: bool,
/// If the app sets this, change the `pixels_per_point` of Egui to this next frame.
pub pixels_per_point: Option<f32>,
} }
pub trait TextureAllocator { pub trait TextureAllocator {

View file

@ -188,6 +188,9 @@ pub struct DemoApp {
demo_window: DemoWindow, demo_window: DemoWindow,
fractal_clock: FractalClock, fractal_clock: FractalClock,
/// current slider value for current gui scale (backend demo only)
pixels_per_point: Option<f32>,
#[cfg_attr(feature = "serde", serde(skip))] #[cfg_attr(feature = "serde", serde(skip))]
frame_history: FrameHistory, frame_history: FrameHistory,
@ -320,8 +323,6 @@ impl DemoApp {
let is_web = info.web_info.is_some(); let is_web = info.web_info.is_some();
let mut options = app::AppOutput::default();
if is_web { if is_web {
ui.label("Egui is an immediate mode GUI written in Rust, compiled to WebAssembly, rendered with WebGL."); ui.label("Egui is an immediate mode GUI written in Rust, compiled to WebAssembly, rendered with WebGL.");
ui.label( ui.label(
@ -332,23 +333,17 @@ impl DemoApp {
ui.label("Project home page:"); ui.label("Project home page:");
ui.hyperlink("https://github.com/emilk/egui"); ui.hyperlink("https://github.com/emilk/egui");
}); });
} else { ui.separator();
ui.heading("Egui");
options.quit |= ui.button("Quit").clicked;
} }
ui.separator();
self.run_mode_ui(ui); self.run_mode_ui(ui);
if self.run_mode == RunMode::Continuous { ui.separator();
ui.label(format!(
"Repainting the UI each frame. FPS: {:.1}", let mut output = app::AppOutput::default();
self.frame_history.fps() output.pixels_per_point = self.pixels_per_point_ui(ui, info);
));
} else { ui.separator();
ui.label("Only running UI code when there are animations or input");
}
ui.separator(); ui.separator();
self.frame_history.ui(ui); self.frame_history.ui(ui);
@ -359,7 +354,41 @@ impl DemoApp {
"Show color blend test (debug backend painter)", "Show color blend test (debug backend painter)",
); );
options if !is_web {
output.quit |= ui.button("Quit").clicked;
}
output
}
fn pixels_per_point_ui(&mut self, ui: &mut Ui, info: &app::BackendInfo) -> Option<f32> {
self.pixels_per_point = self
.pixels_per_point
.or(info.native_pixels_per_point)
.or_else(|| Some(ui.ctx().pixels_per_point()));
if let Some(pixels_per_point) = &mut self.pixels_per_point {
ui.add(
Slider::f32(pixels_per_point, 0.5..=5.0)
.logarithmic(true)
.text("Scale (physical pixels per point)"),
);
if let Some(native_pixels_per_point) = info.native_pixels_per_point {
if ui
.button(format!(
"Reset scale to native value ({:.1})",
native_pixels_per_point
))
.clicked
{
*pixels_per_point = native_pixels_per_point;
}
}
if !ui.ctx().is_using_mouse() {
// We wait until mouse release to activate:
return Some(*pixels_per_point);
}
}
None
} }
fn run_mode_ui(&mut self, ui: &mut Ui) { fn run_mode_ui(&mut self, ui: &mut Ui) {
@ -371,6 +400,15 @@ impl DemoApp {
ui.radio_value(run_mode, RunMode::Reactive, "Reactive") ui.radio_value(run_mode, RunMode::Reactive, "Reactive")
.on_hover_text("Repaint when there are animations or input (e.g. mouse movement)"); .on_hover_text("Repaint when there are animations or input (e.g. mouse movement)");
}); });
if self.run_mode == RunMode::Continuous {
ui.label(format!(
"Repainting the UI each frame. FPS: {:.1}",
self.frame_history.fps()
));
} else {
ui.label("Only running UI code when there are animations or input");
}
} }
} }

View file

@ -388,7 +388,7 @@ impl InputState {
ui.label(format!("scroll_delta: {:?} points", scroll_delta)); ui.label(format!("scroll_delta: {:?} points", scroll_delta));
ui.label(format!("screen_size: {:?} points", screen_size)); ui.label(format!("screen_size: {:?} points", screen_size));
ui.label(format!( ui.label(format!(
"{:?} points for each physical pixel (HDPI factor)", "{:?} physical pixels for each logical point",
pixels_per_point pixels_per_point
)); ));
ui.label(format!("time: {:.3} s", time)); ui.label(format!("time: {:.3} s", time));

View file

@ -51,7 +51,10 @@ pub fn run(title: &str, mut storage: FileStorage, mut app: impl App + 'static) -
let mut ctx = egui::Context::new(); let mut ctx = egui::Context::new();
*ctx.memory() = egui::app::get_value(&storage, EGUI_MEMORY_KEY).unwrap_or_default(); *ctx.memory() = egui::app::get_value(&storage, EGUI_MEMORY_KEY).unwrap_or_default();
let mut raw_input = make_raw_input(&display); let mut raw_input = egui::RawInput {
pixels_per_point: Some(native_pixels_per_point(&display)),
..Default::default()
};
let start_time = Instant::now(); let start_time = Instant::now();
let mut previous_frame_time = None; let mut previous_frame_time = None;
@ -62,11 +65,14 @@ pub fn run(title: &str, mut storage: FileStorage, mut app: impl App + 'static) -
let mut redraw = || { let mut redraw = || {
let egui_start = Instant::now(); let egui_start = Instant::now();
raw_input.time = start_time.elapsed().as_nanos() as f64 * 1e-9; raw_input.time = start_time.elapsed().as_nanos() as f64 * 1e-9;
raw_input.screen_size =
screen_size_in_pixels(&display) / raw_input.pixels_per_point.unwrap();
let backend_info = egui::app::BackendInfo { let backend_info = egui::app::BackendInfo {
web_info: None, web_info: None,
cpu_usage: previous_frame_time, cpu_usage: previous_frame_time,
seconds_since_midnight: Some(seconds_since_midnight()), seconds_since_midnight: Some(seconds_since_midnight()),
native_pixels_per_point: Some(native_pixels_per_point(&display)),
}; };
let mut ui = ctx.begin_frame(raw_input.take()); let mut ui = ctx.begin_frame(raw_input.take());
@ -75,10 +81,17 @@ pub fn run(title: &str, mut storage: FileStorage, mut app: impl App + 'static) -
let frame_time = (Instant::now() - egui_start).as_secs_f64() as f32; let frame_time = (Instant::now() - egui_start).as_secs_f64() as f32;
previous_frame_time = Some(frame_time); previous_frame_time = Some(frame_time);
painter.paint_jobs(&display, paint_jobs, &ctx.texture()); painter.paint_jobs(&display, ctx.pixels_per_point(), paint_jobs, &ctx.texture());
{ {
let egui::app::AppOutput { quit } = app_output; let egui::app::AppOutput {
quit,
pixels_per_point,
} = app_output;
if let Some(pixels_per_point) = pixels_per_point {
// User changed GUI scale
raw_input.pixels_per_point = Some(pixels_per_point);
}
*control_flow = if quit { *control_flow = if quit {
glutin::event_loop::ControlFlow::Exit glutin::event_loop::ControlFlow::Exit

View file

@ -27,30 +27,16 @@ pub fn input_to_egui(
use glutin::event::WindowEvent::*; use glutin::event::WindowEvent::*;
match event { match event {
CloseRequested | Destroyed => *control_flow = ControlFlow::Exit, CloseRequested | Destroyed => *control_flow = ControlFlow::Exit,
Resized(physical_size) => {
raw_input.screen_size =
egui::vec2(physical_size.width as f32, physical_size.height as f32)
/ raw_input.pixels_per_point.unwrap();
}
ScaleFactorChanged {
scale_factor,
new_inner_size,
} => {
raw_input.pixels_per_point = Some(scale_factor as f32);
raw_input.screen_size =
egui::vec2(new_inner_size.width as f32, new_inner_size.height as f32)
/ (scale_factor as f32);
}
MouseInput { state, .. } => { MouseInput { state, .. } => {
raw_input.mouse_down = state == glutin::event::ElementState::Pressed; raw_input.mouse_down = state == glutin::event::ElementState::Pressed;
} }
CursorMoved { position, .. } => { CursorMoved {
position: pos_in_pixels,
..
} => {
raw_input.mouse_pos = Some(pos2( raw_input.mouse_pos = Some(pos2(
position.x as f32 / raw_input.pixels_per_point.unwrap(), pos_in_pixels.x as f32 / raw_input.pixels_per_point.unwrap(),
position.y as f32 / raw_input.pixels_per_point.unwrap(), pos_in_pixels.y as f32 / raw_input.pixels_per_point.unwrap(),
)); ));
} }
CursorLeft { .. } => { CursorLeft { .. } => {
@ -213,14 +199,11 @@ pub fn seconds_since_midnight() -> f64 {
time.num_seconds_from_midnight() as f64 + 1e-9 * (time.nanosecond() as f64) time.num_seconds_from_midnight() as f64 + 1e-9 * (time.nanosecond() as f64)
} }
pub fn make_raw_input(display: &glium::Display) -> egui::RawInput { pub fn screen_size_in_pixels(display: &glium::Display) -> Vec2 {
let pixels_per_point = display.gl_window().window().scale_factor() as f32; let (width_in_pixels, height_in_pixels) = display.get_framebuffer_dimensions();
egui::RawInput { vec2(width_in_pixels as f32, height_in_pixels as f32)
screen_size: { }
let (width, height) = display.get_framebuffer_dimensions();
vec2(width as f32, height as f32) / pixels_per_point pub fn native_pixels_per_point(display: &glium::Display) -> f32 {
}, display.gl_window().window().scale_factor() as f32
pixels_per_point: Some(pixels_per_point),
..Default::default()
}
} }

View file

@ -152,16 +152,23 @@ impl Painter {
pub fn paint_jobs( pub fn paint_jobs(
&mut self, &mut self,
display: &glium::Display, display: &glium::Display,
pixels_per_point: f32,
jobs: PaintJobs, jobs: PaintJobs,
texture: &egui::Texture, egui_texture: &egui::Texture,
) { ) {
self.upload_egui_texture(display, texture); self.upload_egui_texture(display, egui_texture);
self.upload_pending_user_textures(display); self.upload_pending_user_textures(display);
let mut target = display.draw(); let mut target = display.draw();
target.clear_color(0.0, 0.0, 0.0, 0.0); target.clear_color(0.0, 0.0, 0.0, 0.0);
for (clip_rect, triangles) in jobs { for (clip_rect, triangles) in jobs {
self.paint_job(&mut target, display, clip_rect, &triangles) self.paint_job(
&mut target,
display,
pixels_per_point,
clip_rect,
&triangles,
)
} }
target.finish().unwrap(); target.finish().unwrap();
} }
@ -183,6 +190,7 @@ impl Painter {
&mut self, &mut self,
target: &mut Frame, target: &mut Frame,
display: &glium::Display, display: &glium::Display,
pixels_per_point: f32,
clip_rect: Rect, clip_rect: Rect,
triangles: &Triangles, triangles: &Triangles,
) { ) {
@ -217,15 +225,14 @@ impl Painter {
let index_buffer = let index_buffer =
glium::IndexBuffer::new(display, PrimitiveType::TrianglesList, &indices).unwrap(); glium::IndexBuffer::new(display, PrimitiveType::TrianglesList, &indices).unwrap();
let pixels_per_point = display.gl_window().window().scale_factor() as f32; let (width_in_pixels, height_in_pixels) = display.get_framebuffer_dimensions();
let (width_pixels, height_pixels) = display.get_framebuffer_dimensions(); let width_in_points = width_in_pixels as f32 / pixels_per_point;
let width_points = width_pixels as f32 / pixels_per_point; let height_in_points = height_in_pixels as f32 / pixels_per_point;
let height_points = height_pixels as f32 / pixels_per_point;
let texture = self.get_texture(triangles.texture_id); let texture = self.get_texture(triangles.texture_id);
let uniforms = uniform! { let uniforms = uniform! {
u_screen_size: [width_points, height_points], u_screen_size: [width_in_points, height_in_points],
u_sampler: texture.sampled().wrap_function(SamplerWrapFunction::Clamp), u_sampler: texture.sampled().wrap_function(SamplerWrapFunction::Clamp),
}; };
@ -255,10 +262,10 @@ impl Painter {
let clip_max_y = pixels_per_point * clip_rect.max.y; let clip_max_y = pixels_per_point * clip_rect.max.y;
// Make sure clip rect can fit withing an `u32`: // Make sure clip rect can fit withing an `u32`:
let clip_min_x = clamp(clip_min_x, 0.0..=width_pixels as f32); let clip_min_x = clamp(clip_min_x, 0.0..=width_in_pixels as f32);
let clip_min_y = clamp(clip_min_y, 0.0..=height_pixels as f32); let clip_min_y = clamp(clip_min_y, 0.0..=height_in_pixels as f32);
let clip_max_x = clamp(clip_max_x, clip_min_x..=width_pixels as f32); let clip_max_x = clamp(clip_max_x, clip_min_x..=width_in_pixels as f32);
let clip_max_y = clamp(clip_max_y, clip_min_y..=height_pixels as f32); let clip_max_y = clamp(clip_max_y, clip_min_y..=height_in_pixels as f32);
let clip_min_x = clip_min_x.round() as u32; let clip_min_x = clip_min_x.round() as u32;
let clip_min_y = clip_min_y.round() as u32; let clip_min_y = clip_min_y.round() as u32;
@ -269,7 +276,7 @@ impl Painter {
blend, blend,
scissor: Some(glium::Rect { scissor: Some(glium::Rect {
left: clip_min_x, left: clip_min_x,
bottom: height_pixels - clip_max_y, bottom: height_in_pixels - clip_max_y,
width: clip_max_x - clip_min_x, width: clip_max_x - clip_min_x,
height: clip_max_y - clip_min_y, height: clip_max_y - clip_min_y,
}), }),

View file

@ -2,7 +2,7 @@ use crate::*;
pub use egui::{ pub use egui::{
app::{App, WebInfo}, app::{App, WebInfo},
Srgba, pos2, Srgba,
}; };
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
@ -105,13 +105,17 @@ pub struct WebInput {
} }
impl WebInput { impl WebInput {
pub fn new_frame(&mut self) -> egui::RawInput { pub fn new_frame(&mut self, pixels_per_point: f32) -> egui::RawInput {
// Compensate for potential different scale of Egui compared to native.
let scale = native_pixels_per_point() / pixels_per_point;
let scroll_delta = std::mem::take(&mut self.scroll_delta) * scale;
let mouse_pos = self.mouse_pos.map(|mp| pos2(mp.x * scale, mp.y * scale));
egui::RawInput { egui::RawInput {
mouse_down: self.mouse_down, mouse_down: self.mouse_down,
mouse_pos: self.mouse_pos, mouse_pos,
scroll_delta: std::mem::take(&mut self.scroll_delta), scroll_delta,
screen_size: screen_size().unwrap(), screen_size: screen_size_in_native_points().unwrap() * scale,
pixels_per_point: Some(pixels_per_point()), pixels_per_point: Some(pixels_per_point),
time: now_sec(), time: now_sec(),
events: std::mem::take(&mut self.events), events: std::mem::take(&mut self.events),
} }
@ -121,6 +125,7 @@ impl WebInput {
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
pub struct AppRunner { pub struct AppRunner {
pixels_per_point: f32,
pub web_backend: WebBackend, pub web_backend: WebBackend,
pub web_input: WebInput, pub web_input: WebInput,
pub app: Box<dyn App>, pub app: Box<dyn App>,
@ -130,6 +135,7 @@ pub struct AppRunner {
impl AppRunner { impl AppRunner {
pub fn new(web_backend: WebBackend, app: Box<dyn App>) -> Result<Self, JsValue> { pub fn new(web_backend: WebBackend, app: Box<dyn App>) -> Result<Self, JsValue> {
Ok(Self { Ok(Self {
pixels_per_point: native_pixels_per_point(),
web_backend, web_backend,
web_input: Default::default(), web_input: Default::default(),
app, app,
@ -142,9 +148,9 @@ impl AppRunner {
} }
pub fn logic(&mut self) -> Result<(egui::Output, egui::PaintJobs), JsValue> { pub fn logic(&mut self) -> Result<(egui::Output, egui::PaintJobs), JsValue> {
resize_to_screen_size(self.web_backend.canvas_id()); resize_canvas_to_screen_size(self.web_backend.canvas_id());
let raw_input = self.web_input.new_frame(); let raw_input = self.web_input.new_frame(self.pixels_per_point);
let backend_info = egui::app::BackendInfo { let backend_info = egui::app::BackendInfo {
web_info: Some(WebInfo { web_info: Some(WebInfo {
@ -152,6 +158,7 @@ impl AppRunner {
}), }),
cpu_usage: self.web_backend.previous_frame_time, cpu_usage: self.web_backend.previous_frame_time,
seconds_since_midnight: Some(seconds_since_midnight()), seconds_since_midnight: Some(seconds_since_midnight()),
native_pixels_per_point: Some(native_pixels_per_point()),
}; };
let mut ui = self.web_backend.begin_frame(raw_input); let mut ui = self.web_backend.begin_frame(raw_input);
@ -162,7 +169,14 @@ impl AppRunner {
handle_output(&egui_output); handle_output(&egui_output);
{ {
let egui::app::AppOutput { quit: _ } = app_output; let egui::app::AppOutput {
quit: _,
pixels_per_point,
} = app_output;
if let Some(pixels_per_point) = pixels_per_point {
self.pixels_per_point = pixels_per_point;
}
} }
Ok((egui_output, paint_jobs)) Ok((egui_output, paint_jobs))

View file

@ -17,14 +17,6 @@ pub fn console_log(s: String) {
web_sys::console::log_1(&s.into()); web_sys::console::log_1(&s.into());
} }
pub fn screen_size() -> Option<egui::Vec2> {
let window = web_sys::window()?;
Some(egui::Vec2::new(
window.inner_width().ok()?.as_f64()? as f32,
window.inner_height().ok()?.as_f64()? as f32,
))
}
pub fn now_sec() -> f64 { pub fn now_sec() -> f64 {
web_sys::window() web_sys::window()
.expect("should have a Window") .expect("should have a Window")
@ -40,7 +32,15 @@ pub fn seconds_since_midnight() -> f64 {
seconds as f64 + 1e-3 * (d.get_milliseconds() as f64) seconds as f64 + 1e-3 * (d.get_milliseconds() as f64)
} }
pub fn pixels_per_point() -> f32 { pub fn screen_size_in_native_points() -> Option<egui::Vec2> {
let window = web_sys::window()?;
Some(egui::Vec2::new(
window.inner_width().ok()?.as_f64()? as f32,
window.inner_height().ok()?.as_f64()? as f32,
))
}
pub fn native_pixels_per_point() -> f32 {
let pixels_per_point = web_sys::window().unwrap().device_pixel_ratio() as f32; let pixels_per_point = web_sys::window().unwrap().device_pixel_ratio() as f32;
if pixels_per_point > 0.0 && pixels_per_point.is_finite() { if pixels_per_point > 0.0 && pixels_per_point.is_finite() {
pixels_per_point pixels_per_point
@ -78,11 +78,11 @@ pub fn pos_from_touch_event(event: &web_sys::TouchEvent) -> egui::Pos2 {
} }
} }
pub fn resize_to_screen_size(canvas_id: &str) -> Option<()> { pub fn resize_canvas_to_screen_size(canvas_id: &str) -> Option<()> {
let canvas = canvas_element(canvas_id)?; let canvas = canvas_element(canvas_id)?;
let screen_size = screen_size()?; let screen_size = screen_size_in_native_points()?;
let pixels_per_point = pixels_per_point(); let pixels_per_point = native_pixels_per_point();
canvas canvas
.style() .style()
.set_property("width", &format!("{}px", screen_size.x)) .set_property("width", &format!("{}px", screen_size.x))