This commit is contained in:
nora 2023-04-18 15:38:14 +02:00
parent 12163d1338
commit 550b1644cb
363 changed files with 84081 additions and 16 deletions

View file

@ -0,0 +1,67 @@
[package]
name = "egui_demo_app"
version = "0.21.0"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
license = "MIT OR Apache-2.0"
edition = "2021"
rust-version = "1.65"
publish = false
default-run = "egui_demo_app"
[package.metadata.docs.rs]
all-features = true
[lib]
crate-type = ["cdylib", "rlib"]
[features]
default = ["glow", "persistence"]
http = ["ehttp", "image", "poll-promise", "egui_extras/image"]
persistence = ["eframe/persistence", "egui/persistence", "serde"]
web_screen_reader = ["eframe/web_screen_reader"] # experimental
serde = ["dep:serde", "egui_demo_lib/serde", "egui/serde"]
syntax_highlighting = ["egui_demo_lib/syntax_highlighting"]
glow = ["eframe/glow"]
wgpu = ["eframe/wgpu", "bytemuck"]
[dependencies]
chrono = { version = "0.4", features = ["js-sys", "wasmbind"] }
eframe = { version = "0.21.0", path = "../eframe", default-features = false }
egui = { version = "0.21.0", path = "../egui", features = [
"extra_debug_asserts",
] }
egui_demo_lib = { version = "0.21.0", path = "../egui_demo_lib", features = [
"chrono",
] }
tracing = "0.1"
# Optional dependencies:
bytemuck = { version = "1.7.1", optional = true }
egui_extras = { version = "0.21.0", optional = true, path = "../egui_extras" }
# feature "http":
ehttp = { version = "0.2.0", optional = true }
image = { version = "0.24", optional = true, default-features = false, features = [
"jpeg",
"png",
] }
poll-promise = { version = "0.2", optional = true, default-features = false }
# feature "persistence":
serde = { version = "1", optional = true, features = ["derive"] }
# native:
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
tracing-subscriber = "0.3"
# web:
[target.'cfg(target_arch = "wasm32")'.dependencies]
console_error_panic_hook = "0.1.6"
tracing-wasm = "0.2"
wasm-bindgen-futures = "0.4"

View file

@ -0,0 +1,19 @@
# egui demo app
This app demonstrates [`egui`](https://github.com/emilk/egui/) and [`eframe`](https://github.com/emilk/egui/tree/master/crates/eframe).
View the demo app online at <https://egui.rs>.
Run it locally with `cargo run --release -p egui_demo_app`.
`egui_demo_app` can be compiled to WASM and viewed in a browser locally with:
```sh
./scripts/start_server.sh &
./scripts/build_demo_web.sh --open
```
`egui_demo_app` uses [`egui_demo_lib`](https://github.com/emilk/egui/tree/master/crates/egui_demo_lib).
## Running with `wgpu` backend
`(cd egui_demo_app && cargo r --features wgpu)`

View file

@ -0,0 +1,201 @@
use std::sync::Arc;
use eframe::egui_glow;
use egui::mutex::Mutex;
use egui_glow::glow;
pub struct Custom3d {
/// Behind an `Arc<Mutex<…>>` so we can pass it to [`egui::PaintCallback`] and paint later.
rotating_triangle: Arc<Mutex<RotatingTriangle>>,
angle: f32,
}
impl Custom3d {
pub fn new<'a>(cc: &'a eframe::CreationContext<'a>) -> Option<Self> {
let gl = cc.gl.as_ref()?;
Some(Self {
rotating_triangle: Arc::new(Mutex::new(RotatingTriangle::new(gl)?)),
angle: 0.0,
})
}
}
impl eframe::App for Custom3d {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
egui::CentralPanel::default().show(ctx, |ui| {
egui::ScrollArea::both()
.auto_shrink([false; 2])
.show(ui, |ui| {
ui.horizontal(|ui| {
ui.spacing_mut().item_spacing.x = 0.0;
ui.label("The triangle is being painted using ");
ui.hyperlink_to("glow", "https://github.com/grovesNL/glow");
ui.label(" (OpenGL).");
});
ui.label("It's not a very impressive demo, but it shows you can embed 3D inside of egui.");
egui::Frame::canvas(ui.style()).show(ui, |ui| {
self.custom_painting(ui);
});
ui.label("Drag to rotate!");
ui.add(egui_demo_lib::egui_github_link_file!());
});
});
}
fn on_exit(&mut self, gl: Option<&glow::Context>) {
if let Some(gl) = gl {
self.rotating_triangle.lock().destroy(gl);
}
}
}
impl Custom3d {
fn custom_painting(&mut self, ui: &mut egui::Ui) {
let (rect, response) =
ui.allocate_exact_size(egui::Vec2::splat(300.0), egui::Sense::drag());
self.angle += response.drag_delta().x * 0.01;
// Clone locals so we can move them into the paint callback:
let angle = self.angle;
let rotating_triangle = self.rotating_triangle.clone();
let cb = egui_glow::CallbackFn::new(move |_info, painter| {
rotating_triangle.lock().paint(painter.gl(), angle);
});
let callback = egui::PaintCallback {
rect,
callback: Arc::new(cb),
};
ui.painter().add(callback);
}
}
struct RotatingTriangle {
program: glow::Program,
vertex_array: glow::VertexArray,
}
#[allow(unsafe_code)] // we need unsafe code to use glow
impl RotatingTriangle {
fn new(gl: &glow::Context) -> Option<Self> {
use glow::HasContext as _;
let shader_version = egui_glow::ShaderVersion::get(gl);
unsafe {
let program = gl.create_program().expect("Cannot create program");
if !shader_version.is_new_shader_interface() {
tracing::warn!(
"Custom 3D painting hasn't been ported to {:?}",
shader_version
);
return None;
}
let (vertex_shader_source, fragment_shader_source) = (
r#"
const vec2 verts[3] = vec2[3](
vec2(0.0, 1.0),
vec2(-1.0, -1.0),
vec2(1.0, -1.0)
);
const vec4 colors[3] = vec4[3](
vec4(1.0, 0.0, 0.0, 1.0),
vec4(0.0, 1.0, 0.0, 1.0),
vec4(0.0, 0.0, 1.0, 1.0)
);
out vec4 v_color;
uniform float u_angle;
void main() {
v_color = colors[gl_VertexID];
gl_Position = vec4(verts[gl_VertexID], 0.0, 1.0);
gl_Position.x *= cos(u_angle);
}
"#,
r#"
precision mediump float;
in vec4 v_color;
out vec4 out_color;
void main() {
out_color = v_color;
}
"#,
);
let shader_sources = [
(glow::VERTEX_SHADER, vertex_shader_source),
(glow::FRAGMENT_SHADER, fragment_shader_source),
];
let shaders: Vec<_> = shader_sources
.iter()
.map(|(shader_type, shader_source)| {
let shader = gl
.create_shader(*shader_type)
.expect("Cannot create shader");
gl.shader_source(
shader,
&format!(
"{}\n{}",
shader_version.version_declaration(),
shader_source
),
);
gl.compile_shader(shader);
assert!(
gl.get_shader_compile_status(shader),
"Failed to compile custom_3d_glow {shader_type}: {}",
gl.get_shader_info_log(shader)
);
gl.attach_shader(program, shader);
shader
})
.collect();
gl.link_program(program);
if !gl.get_program_link_status(program) {
panic!("{}", gl.get_program_info_log(program));
}
for shader in shaders {
gl.detach_shader(program, shader);
gl.delete_shader(shader);
}
let vertex_array = gl
.create_vertex_array()
.expect("Cannot create vertex array");
Some(Self {
program,
vertex_array,
})
}
}
fn destroy(&self, gl: &glow::Context) {
use glow::HasContext as _;
unsafe {
gl.delete_program(self.program);
gl.delete_vertex_array(self.vertex_array);
}
}
fn paint(&self, gl: &glow::Context, angle: f32) {
use glow::HasContext as _;
unsafe {
gl.use_program(Some(self.program));
gl.uniform_1_f32(
gl.get_uniform_location(self.program, "u_angle").as_ref(),
angle,
);
gl.bind_vertex_array(Some(self.vertex_array));
gl.draw_arrays(glow::TRIANGLES, 0, 3);
}
}
}

View file

@ -0,0 +1,187 @@
use std::{num::NonZeroU64, sync::Arc};
use eframe::{
egui_wgpu::wgpu::util::DeviceExt,
egui_wgpu::{self, wgpu},
};
pub struct Custom3d {
angle: f32,
}
impl Custom3d {
pub fn new<'a>(cc: &'a eframe::CreationContext<'a>) -> Option<Self> {
// Get the WGPU render state from the eframe creation context. This can also be retrieved
// from `eframe::Frame` when you don't have a `CreationContext` available.
let wgpu_render_state = cc.wgpu_render_state.as_ref()?;
let device = &wgpu_render_state.device;
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("custom3d"),
source: wgpu::ShaderSource::Wgsl(include_str!("./custom3d_wgpu_shader.wgsl").into()),
});
let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("custom3d"),
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: NonZeroU64::new(16),
},
count: None,
}],
});
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("custom3d"),
bind_group_layouts: &[&bind_group_layout],
push_constant_ranges: &[],
});
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("custom3d"),
layout: Some(&pipeline_layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: "vs_main",
buffers: &[],
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: "fs_main",
targets: &[Some(wgpu_render_state.target_format.into())],
}),
primitive: wgpu::PrimitiveState::default(),
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
multiview: None,
});
let uniform_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("custom3d"),
contents: bytemuck::cast_slice(&[0.0_f32; 4]), // 16 bytes aligned!
// Mapping at creation (as done by the create_buffer_init utility) doesn't require us to to add the MAP_WRITE usage
// (this *happens* to workaround this bug )
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::UNIFORM,
});
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("custom3d"),
layout: &bind_group_layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: uniform_buffer.as_entire_binding(),
}],
});
// Because the graphics pipeline must have the same lifetime as the egui render pass,
// instead of storing the pipeline in our `Custom3D` struct, we insert it into the
// `paint_callback_resources` type map, which is stored alongside the render pass.
wgpu_render_state
.renderer
.write()
.paint_callback_resources
.insert(TriangleRenderResources {
pipeline,
bind_group,
uniform_buffer,
});
Some(Self { angle: 0.0 })
}
}
impl eframe::App for Custom3d {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
egui::CentralPanel::default().show(ctx, |ui| {
egui::ScrollArea::both()
.auto_shrink([false; 2])
.show(ui, |ui| {
ui.horizontal(|ui| {
ui.spacing_mut().item_spacing.x = 0.0;
ui.label("The triangle is being painted using ");
ui.hyperlink_to("WGPU", "https://wgpu.rs");
ui.label(" (Portable Rust graphics API awesomeness)");
});
ui.label("It's not a very impressive demo, but it shows you can embed 3D inside of egui.");
egui::Frame::canvas(ui.style()).show(ui, |ui| {
self.custom_painting(ui);
});
ui.label("Drag to rotate!");
ui.add(egui_demo_lib::egui_github_link_file!());
});
});
}
}
impl Custom3d {
fn custom_painting(&mut self, ui: &mut egui::Ui) {
let (rect, response) =
ui.allocate_exact_size(egui::Vec2::splat(300.0), egui::Sense::drag());
self.angle += response.drag_delta().x * 0.01;
// Clone locals so we can move them into the paint callback:
let angle = self.angle;
// The callback function for WGPU is in two stages: prepare, and paint.
//
// The prepare callback is called every frame before paint and is given access to the wgpu
// Device and Queue, which can be used, for instance, to update buffers and uniforms before
// rendering.
//
// You can use the main `CommandEncoder` that is passed-in, return an arbitrary number
// of user-defined `CommandBuffer`s, or both.
// The main command buffer, as well as all user-defined ones, will be submitted together
// to the GPU in a single call.
//
// The paint callback is called after prepare and is given access to the render pass, which
// can be used to issue draw commands.
let cb = egui_wgpu::CallbackFn::new()
.prepare(move |device, queue, _encoder, paint_callback_resources| {
let resources: &TriangleRenderResources = paint_callback_resources.get().unwrap();
resources.prepare(device, queue, angle);
Vec::new()
})
.paint(move |_info, render_pass, paint_callback_resources| {
let resources: &TriangleRenderResources = paint_callback_resources.get().unwrap();
resources.paint(render_pass);
});
let callback = egui::PaintCallback {
rect,
callback: Arc::new(cb),
};
ui.painter().add(callback);
}
}
struct TriangleRenderResources {
pipeline: wgpu::RenderPipeline,
bind_group: wgpu::BindGroup,
uniform_buffer: wgpu::Buffer,
}
impl TriangleRenderResources {
fn prepare(&self, _device: &wgpu::Device, queue: &wgpu::Queue, angle: f32) {
// Update our uniform buffer with the angle from the UI
queue.write_buffer(
&self.uniform_buffer,
0,
bytemuck::cast_slice(&[angle, 0.0, 0.0, 0.0]),
);
}
fn paint<'rp>(&'rp self, render_pass: &mut wgpu::RenderPass<'rp>) {
// Draw our triangle!
render_pass.set_pipeline(&self.pipeline);
render_pass.set_bind_group(0, &self.bind_group, &[]);
render_pass.draw(0..3, 0..1);
}
}

View file

@ -0,0 +1,39 @@
struct VertexOut {
@location(0) color: vec4<f32>,
@builtin(position) position: vec4<f32>,
};
struct Uniforms {
@size(16) angle: f32, // pad to 16 bytes
};
@group(0) @binding(0)
var<uniform> uniforms: Uniforms;
var<private> v_positions: array<vec2<f32>, 3> = array<vec2<f32>, 3>(
vec2<f32>(0.0, 1.0),
vec2<f32>(1.0, -1.0),
vec2<f32>(-1.0, -1.0),
);
var<private> v_colors: array<vec4<f32>, 3> = array<vec4<f32>, 3>(
vec4<f32>(1.0, 0.0, 0.0, 1.0),
vec4<f32>(0.0, 1.0, 0.0, 1.0),
vec4<f32>(0.0, 0.0, 1.0, 1.0),
);
@vertex
fn vs_main(@builtin(vertex_index) v_idx: u32) -> VertexOut {
var out: VertexOut;
out.position = vec4<f32>(v_positions[v_idx], 0.0, 1.0);
out.position.x = out.position.x * cos(uniforms.angle);
out.color = v_colors[v_idx];
return out;
}
@fragment
fn fs_main(in: VertexOut) -> @location(0) vec4<f32> {
return in.color;
}

View file

@ -0,0 +1,205 @@
use egui::{containers::*, widgets::*, *};
use std::f32::consts::TAU;
#[derive(PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "serde", serde(default))]
pub struct FractalClock {
paused: bool,
time: f64,
zoom: f32,
start_line_width: f32,
depth: usize,
length_factor: f32,
luminance_factor: f32,
width_factor: f32,
line_count: usize,
}
impl Default for FractalClock {
fn default() -> Self {
Self {
paused: false,
time: 0.0,
zoom: 0.25,
start_line_width: 2.5,
depth: 9,
length_factor: 0.8,
luminance_factor: 0.8,
width_factor: 0.9,
line_count: 0,
}
}
}
impl FractalClock {
pub fn ui(&mut self, ui: &mut Ui, seconds_since_midnight: Option<f64>) {
if !self.paused {
self.time = seconds_since_midnight.unwrap_or_else(|| ui.input(|i| i.time));
ui.ctx().request_repaint();
}
let painter = Painter::new(
ui.ctx().clone(),
ui.layer_id(),
ui.available_rect_before_wrap(),
);
self.paint(&painter);
// Make sure we allocate what we used (everything)
ui.expand_to_include_rect(painter.clip_rect());
Frame::popup(ui.style())
.stroke(Stroke::NONE)
.show(ui, |ui| {
ui.set_max_width(270.0);
CollapsingHeader::new("Settings")
.show(ui, |ui| self.options_ui(ui, seconds_since_midnight));
});
}
fn options_ui(&mut self, ui: &mut Ui, seconds_since_midnight: Option<f64>) {
if seconds_since_midnight.is_some() {
ui.label(format!(
"Local time: {:02}:{:02}:{:02}.{:03}",
(self.time % (24.0 * 60.0 * 60.0) / 3600.0).floor(),
(self.time % (60.0 * 60.0) / 60.0).floor(),
(self.time % 60.0).floor(),
(self.time % 1.0 * 100.0).floor()
));
} else {
ui.label("The fractal_clock clock is not showing the correct time");
};
ui.label(format!("Painted line count: {}", self.line_count));
ui.checkbox(&mut self.paused, "Paused");
ui.add(Slider::new(&mut self.zoom, 0.0..=1.0).text("zoom"));
ui.add(Slider::new(&mut self.start_line_width, 0.0..=5.0).text("Start line width"));
ui.add(Slider::new(&mut self.depth, 0..=14).text("depth"));
ui.add(Slider::new(&mut self.length_factor, 0.0..=1.0).text("length factor"));
ui.add(Slider::new(&mut self.luminance_factor, 0.0..=1.0).text("luminance factor"));
ui.add(Slider::new(&mut self.width_factor, 0.0..=1.0).text("width factor"));
egui::reset_button(ui, self);
ui.hyperlink_to(
"Inspired by a screensaver by Rob Mayoff",
"http://www.dqd.com/~mayoff/programs/FractalClock/",
);
ui.add(egui_demo_lib::egui_github_link_file!());
}
fn paint(&mut self, painter: &Painter) {
struct Hand {
length: f32,
angle: f32,
vec: Vec2,
}
impl Hand {
fn from_length_angle(length: f32, angle: f32) -> Self {
Self {
length,
angle,
vec: length * Vec2::angled(angle),
}
}
}
let angle_from_period =
|period| TAU * (self.time.rem_euclid(period) / period) as f32 - TAU / 4.0;
let hands = [
// Second hand:
Hand::from_length_angle(self.length_factor, angle_from_period(60.0)),
// Minute hand:
Hand::from_length_angle(self.length_factor, angle_from_period(60.0 * 60.0)),
// Hour hand:
Hand::from_length_angle(0.5, angle_from_period(12.0 * 60.0 * 60.0)),
];
let mut shapes: Vec<Shape> = Vec::new();
let rect = painter.clip_rect();
let to_screen = emath::RectTransform::from_to(
Rect::from_center_size(Pos2::ZERO, rect.square_proportions() / self.zoom),
rect,
);
let mut paint_line = |points: [Pos2; 2], color: Color32, width: f32| {
let line = [to_screen * points[0], to_screen * points[1]];
// culling
if rect.intersects(Rect::from_two_pos(line[0], line[1])) {
shapes.push(Shape::line_segment(line, (width, color)));
}
};
let hand_rotations = [
hands[0].angle - hands[2].angle + TAU / 2.0,
hands[1].angle - hands[2].angle + TAU / 2.0,
];
let hand_rotors = [
hands[0].length * emath::Rot2::from_angle(hand_rotations[0]),
hands[1].length * emath::Rot2::from_angle(hand_rotations[1]),
];
#[derive(Clone, Copy)]
struct Node {
pos: Pos2,
dir: Vec2,
}
let mut nodes = Vec::new();
let mut width = self.start_line_width;
for (i, hand) in hands.iter().enumerate() {
let center = pos2(0.0, 0.0);
let end = center + hand.vec;
paint_line([center, end], Color32::from_additive_luminance(255), width);
if i < 2 {
nodes.push(Node {
pos: end,
dir: hand.vec,
});
}
}
let mut luminance = 0.7; // Start dimmer than main hands
let mut new_nodes = Vec::new();
for _ in 0..self.depth {
new_nodes.clear();
new_nodes.reserve(nodes.len() * 2);
luminance *= self.luminance_factor;
width *= self.width_factor;
let luminance_u8 = (255.0 * luminance).round() as u8;
if luminance_u8 == 0 {
break;
}
for &rotor in &hand_rotors {
for a in &nodes {
let new_dir = rotor * a.dir;
let b = Node {
pos: a.pos + new_dir,
dir: new_dir,
};
paint_line(
[a.pos, b.pos],
Color32::from_additive_luminance(luminance_u8),
width,
);
new_nodes.push(b);
}
}
std::mem::swap(&mut nodes, &mut new_nodes);
}
self.line_count = shapes.len();
painter.extend(shapes);
}
}

View file

@ -0,0 +1,266 @@
use egui_extras::RetainedImage;
use poll_promise::Promise;
struct Resource {
/// HTTP response
response: ehttp::Response,
text: Option<String>,
/// If set, the response was an image.
image: Option<RetainedImage>,
/// If set, the response was text with some supported syntax highlighting (e.g. ".rs" or ".md").
colored_text: Option<ColoredText>,
}
impl Resource {
fn from_response(ctx: &egui::Context, response: ehttp::Response) -> Self {
let content_type = response.content_type().unwrap_or_default();
let image = if content_type.starts_with("image/") {
RetainedImage::from_image_bytes(&response.url, &response.bytes).ok()
} else {
None
};
let text = response.text();
let colored_text = text.and_then(|text| syntax_highlighting(ctx, &response, text));
let text = text.map(|text| text.to_owned());
Self {
response,
text,
image,
colored_text,
}
}
}
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct HttpApp {
url: String,
#[cfg_attr(feature = "serde", serde(skip))]
promise: Option<Promise<ehttp::Result<Resource>>>,
}
impl Default for HttpApp {
fn default() -> Self {
Self {
url: "https://raw.githubusercontent.com/emilk/egui/master/README.md".to_owned(),
promise: Default::default(),
}
}
}
impl eframe::App for HttpApp {
fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
egui::TopBottomPanel::bottom("http_bottom").show(ctx, |ui| {
let layout = egui::Layout::top_down(egui::Align::Center).with_main_justify(true);
ui.allocate_ui_with_layout(ui.available_size(), layout, |ui| {
ui.add(egui_demo_lib::egui_github_link_file!())
})
});
egui::CentralPanel::default().show(ctx, |ui| {
let trigger_fetch = ui_url(ui, frame, &mut self.url);
ui.horizontal_wrapped(|ui| {
ui.spacing_mut().item_spacing.x = 0.0;
ui.label("HTTP requests made using ");
ui.hyperlink_to("ehttp", "https://www.github.com/emilk/ehttp");
ui.label(".");
});
if trigger_fetch {
let ctx = ctx.clone();
let (sender, promise) = Promise::new();
let request = ehttp::Request::get(&self.url);
ehttp::fetch(request, move |response| {
ctx.request_repaint(); // wake up UI thread
let resource = response.map(|response| Resource::from_response(&ctx, response));
sender.send(resource);
});
self.promise = Some(promise);
}
ui.separator();
if let Some(promise) = &self.promise {
if let Some(result) = promise.ready() {
match result {
Ok(resource) => {
ui_resource(ui, resource);
}
Err(error) => {
// This should only happen if the fetch API isn't available or something similar.
ui.colored_label(
ui.visuals().error_fg_color,
if error.is_empty() { "Error" } else { error },
);
}
}
} else {
ui.spinner();
}
}
});
}
}
fn ui_url(ui: &mut egui::Ui, frame: &mut eframe::Frame, url: &mut String) -> bool {
let mut trigger_fetch = false;
ui.horizontal(|ui| {
ui.label("URL:");
trigger_fetch |= ui
.add(egui::TextEdit::singleline(url).desired_width(f32::INFINITY))
.lost_focus();
});
if frame.is_web() {
ui.label("HINT: paste the url of this page into the field above!");
}
ui.horizontal(|ui| {
if ui.button("Source code for this example").clicked() {
*url = format!(
"https://raw.githubusercontent.com/emilk/egui/master/{}",
file!()
);
trigger_fetch = true;
}
if ui.button("Random image").clicked() {
let seed = ui.input(|i| i.time);
let side = 640;
*url = format!("https://picsum.photos/seed/{}/{}", seed, side);
trigger_fetch = true;
}
});
trigger_fetch
}
fn ui_resource(ui: &mut egui::Ui, resource: &Resource) {
let Resource {
response,
text,
image,
colored_text,
} = resource;
ui.monospace(format!("url: {}", response.url));
ui.monospace(format!(
"status: {} ({})",
response.status, response.status_text
));
ui.monospace(format!(
"content-type: {}",
response.content_type().unwrap_or_default()
));
ui.monospace(format!(
"size: {:.1} kB",
response.bytes.len() as f32 / 1000.0
));
ui.separator();
egui::ScrollArea::vertical()
.auto_shrink([false; 2])
.show(ui, |ui| {
egui::CollapsingHeader::new("Response headers")
.default_open(false)
.show(ui, |ui| {
egui::Grid::new("response_headers")
.spacing(egui::vec2(ui.spacing().item_spacing.x * 2.0, 0.0))
.show(ui, |ui| {
for header in &response.headers {
ui.label(header.0);
ui.label(header.1);
ui.end_row();
}
})
});
ui.separator();
if let Some(text) = &text {
let tooltip = "Click to copy the response body";
if ui.button("📋").on_hover_text(tooltip).clicked() {
ui.output_mut(|o| o.copied_text = text.clone());
}
ui.separator();
}
if let Some(image) = image {
let mut size = image.size_vec2();
size *= (ui.available_width() / size.x).min(1.0);
image.show_size(ui, size);
} else if let Some(colored_text) = colored_text {
colored_text.ui(ui);
} else if let Some(text) = &text {
selectable_text(ui, text);
} else {
ui.monospace("[binary]");
}
});
}
fn selectable_text(ui: &mut egui::Ui, mut text: &str) {
ui.add(
egui::TextEdit::multiline(&mut text)
.desired_width(f32::INFINITY)
.font(egui::TextStyle::Monospace),
);
}
// ----------------------------------------------------------------------------
// Syntax highlighting:
#[cfg(feature = "syntect")]
fn syntax_highlighting(
ctx: &egui::Context,
response: &ehttp::Response,
text: &str,
) -> Option<ColoredText> {
let extension_and_rest: Vec<&str> = response.url.rsplitn(2, '.').collect();
let extension = extension_and_rest.get(0)?;
let theme = crate::syntax_highlighting::CodeTheme::from_style(&ctx.style());
Some(ColoredText(crate::syntax_highlighting::highlight(
ctx, &theme, text, extension,
)))
}
#[cfg(not(feature = "syntect"))]
fn syntax_highlighting(_ctx: &egui::Context, _: &ehttp::Response, _: &str) -> Option<ColoredText> {
None
}
struct ColoredText(egui::text::LayoutJob);
impl ColoredText {
pub fn ui(&self, ui: &mut egui::Ui) {
if true {
// Selectable text:
let mut layouter = |ui: &egui::Ui, _string: &str, wrap_width: f32| {
let mut layout_job = self.0.clone();
layout_job.wrap.max_width = wrap_width;
ui.fonts(|f| f.layout_job(layout_job))
};
let mut text = self.0.text.as_str();
ui.add(
egui::TextEdit::multiline(&mut text)
.font(egui::TextStyle::Monospace)
.desired_width(f32::INFINITY)
.layouter(&mut layouter),
);
} else {
let mut job = self.0.clone();
job.wrap.max_width = ui.available_width();
let galley = ui.fonts(|f| f.layout_job(job));
let (response, painter) = ui.allocate_painter(galley.size(), egui::Sense::hover());
painter.add(egui::Shape::galley(response.rect.min, galley));
}
}
}

View file

@ -0,0 +1,21 @@
#[cfg(all(feature = "glow", not(feature = "wgpu")))]
mod custom3d_glow;
#[cfg(feature = "wgpu")]
mod custom3d_wgpu;
mod fractal_clock;
#[cfg(feature = "http")]
mod http_app;
#[cfg(all(feature = "glow", not(feature = "wgpu")))]
pub use custom3d_glow::Custom3d;
#[cfg(feature = "wgpu")]
pub use custom3d_wgpu::Custom3d;
pub use fractal_clock::FractalClock;
#[cfg(feature = "http")]
pub use http_app::HttpApp;

View file

@ -0,0 +1,396 @@
use egui::Widget;
/// How often we repaint the demo app by default
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum RunMode {
/// This is the default for the demo.
///
/// If this is selected, egui is only updated if are input events
/// (like mouse movements) or there are some animations in the GUI.
///
/// Reactive mode saves CPU.
///
/// The downside is that the UI can become out-of-date if something it is supposed to monitor changes.
/// For instance, a GUI for a thermostat need to repaint each time the temperature changes.
/// To ensure the UI is up to date you need to call `egui::Context::request_repaint()` each
/// time such an event happens. You can also chose to call `request_repaint()` once every second
/// or after every single frame - this is called [`Continuous`](RunMode::Continuous) mode,
/// and for games and interactive tools that need repainting every frame anyway, this should be the default.
Reactive,
/// This will call `egui::Context::request_repaint()` at the end of each frame
/// to request the backend to repaint as soon as possible.
///
/// On most platforms this will mean that egui will run at the display refresh rate of e.g. 60 Hz.
///
/// For this demo it is not any reason to do so except to
/// demonstrate how quickly egui runs.
///
/// For games or other interactive apps, this is probably what you want to do.
/// It will guarantee that egui is always up-to-date.
Continuous,
}
/// Default for demo is Reactive since
/// 1) We want to use minimal CPU
/// 2) There are no external events that could invalidate the UI
/// so there are no events to miss.
impl Default for RunMode {
fn default() -> Self {
RunMode::Reactive
}
}
// ----------------------------------------------------------------------------
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "serde", serde(default))]
pub struct BackendPanel {
pub open: bool,
#[cfg_attr(feature = "serde", serde(skip))]
// go back to [`RunMode::Reactive`] mode each time we start
run_mode: RunMode,
#[cfg_attr(feature = "serde", serde(skip))]
repaint_after_seconds: f32,
/// current slider value for current gui scale
#[cfg_attr(feature = "serde", serde(skip))]
pixels_per_point: Option<f32>,
#[cfg_attr(feature = "serde", serde(skip))]
frame_history: crate::frame_history::FrameHistory,
egui_windows: EguiWindows,
}
impl Default for BackendPanel {
fn default() -> Self {
Self {
open: false,
run_mode: Default::default(),
repaint_after_seconds: 1.0,
pixels_per_point: None,
frame_history: Default::default(),
egui_windows: Default::default(),
}
}
}
impl BackendPanel {
pub fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
self.frame_history
.on_new_frame(ctx.input(|i| i.time), frame.info().cpu_usage);
match self.run_mode {
RunMode::Continuous => {
// Tell the backend to repaint as soon as possible
ctx.request_repaint();
}
RunMode::Reactive => {
// let the computer rest for a bit
ctx.request_repaint_after(std::time::Duration::from_secs_f32(
self.repaint_after_seconds,
));
}
}
}
pub fn end_of_frame(&mut self, ctx: &egui::Context) {
self.egui_windows.windows(ctx);
}
pub fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame) {
egui::trace!(ui);
self.integration_ui(ui, frame);
ui.separator();
self.run_mode_ui(ui);
ui.separator();
self.frame_history.ui(ui);
ui.separator();
ui.label("egui windows:");
self.egui_windows.checkboxes(ui);
ui.separator();
{
let mut debug_on_hover = ui.ctx().debug_on_hover();
ui.checkbox(&mut debug_on_hover, "🐛 Debug on hover")
.on_hover_text("Show structure of the ui when you hover with the mouse");
ui.ctx().set_debug_on_hover(debug_on_hover);
}
ui.separator();
#[cfg(target_arch = "wasm32")]
#[cfg(feature = "web_screen-reader")]
{
let mut screen_reader = ui.ctx().options(|o| o.screen_reader);
ui.checkbox(&mut screen_reader, "🔈 Screen reader").on_hover_text("Experimental feature: checking this will turn on the screen reader on supported platforms");
ui.ctx().options_mut(|o| o.screen_reader = screen_reader);
}
#[cfg(not(target_arch = "wasm32"))]
{
ui.separator();
if ui.button("Quit").clicked() {
frame.close();
}
}
}
fn integration_ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame) {
ui.horizontal(|ui| {
ui.spacing_mut().item_spacing.x = 0.0;
ui.label("egui running inside ");
ui.hyperlink_to(
"eframe",
"https://github.com/emilk/egui/tree/master/crates/eframe",
);
ui.label(".");
});
#[cfg(target_arch = "wasm32")]
ui.collapsing("Web info (location)", |ui| {
ui.monospace(format!("{:#?}", frame.info().web_info.location));
});
// On web, the browser controls `pixels_per_point`.
let integration_controls_pixels_per_point = frame.is_web();
if !integration_controls_pixels_per_point {
self.pixels_per_point_ui(ui, &frame.info());
}
#[cfg(not(target_arch = "wasm32"))]
{
ui.horizontal(|ui| {
{
let mut fullscreen = frame.info().window_info.fullscreen;
if ui
.checkbox(&mut fullscreen, "🗖 Fullscreen (F11)")
.on_hover_text("Fullscreen the window")
.changed()
{
frame.set_fullscreen(fullscreen);
}
}
if ui
.button("📱 Phone Size")
.on_hover_text("Resize the window to be small like a phone.")
.clicked()
{
// frame.set_window_size(egui::vec2(375.0, 812.0)); // iPhone 12 mini
frame.set_window_size(egui::vec2(375.0, 667.0)); // iPhone SE 2nd gen
frame.set_fullscreen(false);
ui.close_menu();
}
});
if !frame.info().window_info.fullscreen
&& ui
.button("Drag me to drag window")
.is_pointer_button_down_on()
{
frame.drag_window();
}
}
}
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;
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 = ui.ctx().pixels_per_point() != native_pixels_per_point;
if ui
.add_enabled(enabled, egui::Button::new("Reset"))
.on_hover_text(format!(
"Reset scale to native value ({:.1})",
native_pixels_per_point
))
.clicked()
{
ui.ctx().set_pixels_per_point(native_pixels_per_point);
}
}
});
if reset {
self.pixels_per_point = None;
}
}
fn run_mode_ui(&mut self, ui: &mut egui::Ui) {
ui.horizontal(|ui| {
let run_mode = &mut self.run_mode;
ui.label("Mode:");
ui.radio_value(run_mode, RunMode::Reactive, "Reactive")
.on_hover_text("Repaint when there are animations or input (e.g. mouse movement)");
ui.radio_value(run_mode, RunMode::Continuous, "Continuous")
.on_hover_text("Repaint everything each frame");
});
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.");
ui.horizontal(|ui| {
ui.spacing_mut().item_spacing.x = 0.0;
ui.label("(but at least every ");
egui::DragValue::new(&mut self.repaint_after_seconds)
.clamp_range(0.1..=10.0)
.speed(0.1)
.suffix(" s")
.ui(ui)
.on_hover_text("Repaint this often, even if there is no input.");
ui.label(")");
});
}
}
}
// ----------------------------------------------------------------------------
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
struct EguiWindows {
// egui stuff:
settings: bool,
inspection: bool,
memory: bool,
output_events: bool,
#[cfg_attr(feature = "serde", serde(skip))]
output_event_history: std::collections::VecDeque<egui::output::OutputEvent>,
}
impl Default for EguiWindows {
fn default() -> Self {
EguiWindows::none()
}
}
impl EguiWindows {
fn none() -> Self {
Self {
settings: false,
inspection: false,
memory: false,
output_events: false,
output_event_history: Default::default(),
}
}
fn checkboxes(&mut self, ui: &mut egui::Ui) {
let Self {
settings,
inspection,
memory,
output_events,
output_event_history: _,
} = self;
ui.checkbox(settings, "🔧 Settings");
ui.checkbox(inspection, "🔍 Inspection");
ui.checkbox(memory, "📝 Memory");
ui.checkbox(output_events, "📤 Output Events");
}
fn windows(&mut self, ctx: &egui::Context) {
let Self {
settings,
inspection,
memory,
output_events,
output_event_history,
} = self;
ctx.output(|o| {
for event in &o.events {
output_event_history.push_back(event.clone());
}
});
while output_event_history.len() > 1000 {
output_event_history.pop_front();
}
egui::Window::new("🔧 Settings")
.open(settings)
.vscroll(true)
.show(ctx, |ui| {
ctx.settings_ui(ui);
});
egui::Window::new("🔍 Inspection")
.open(inspection)
.vscroll(true)
.show(ctx, |ui| {
ctx.inspection_ui(ui);
});
egui::Window::new("📝 Memory")
.open(memory)
.resizable(false)
.show(ctx, |ui| {
ctx.memory_ui(ui);
});
egui::Window::new("📤 Output Events")
.open(output_events)
.resizable(true)
.default_width(520.0)
.show(ctx, |ui| {
ui.label(
"Recent output events from egui. \
These are emitted when you interact with widgets, or move focus between them with TAB. \
They can be hooked up to a screen reader on supported platforms.",
);
ui.separator();
egui::ScrollArea::vertical()
.stick_to_bottom(true)
.show(ui, |ui| {
for event in output_event_history {
ui.label(format!("{:?}", event));
}
});
});
}
}

View file

@ -0,0 +1,132 @@
use egui::util::History;
pub struct FrameHistory {
frame_times: History<f32>,
}
impl Default for FrameHistory {
fn default() -> Self {
let max_age: f32 = 1.0;
let max_len = (max_age * 300.0).round() as usize;
Self {
frame_times: History::new(0..max_len, max_age),
}
}
}
impl FrameHistory {
// Called first
pub fn on_new_frame(&mut self, now: f64, previous_frame_time: Option<f32>) {
let previous_frame_time = previous_frame_time.unwrap_or_default();
if let Some(latest) = self.frame_times.latest_mut() {
*latest = previous_frame_time; // rewrite history now that we know
}
self.frame_times.add(now, previous_frame_time); // projected
}
pub fn mean_frame_time(&self) -> f32 {
self.frame_times.average().unwrap_or_default()
}
pub fn fps(&self) -> f32 {
1.0 / self.frame_times.mean_time_interval().unwrap_or_default()
}
pub fn ui(&mut self, ui: &mut egui::Ui) {
ui.label(format!(
"Total frames painted: {}",
self.frame_times.total_count()
))
.on_hover_text("Includes this frame.");
ui.label(format!(
"Mean CPU usage: {:.2} ms / frame",
1e3 * self.mean_frame_time()
))
.on_hover_text(
"Includes egui layout and tessellation time.\n\
Does not include GPU usage, nor overhead for sending data to GPU.",
);
egui::warn_if_debug_build(ui);
if !cfg!(target_arch = "wasm32") {
egui::CollapsingHeader::new("📊 CPU usage history")
.default_open(false)
.show(ui, |ui| {
self.graph(ui);
});
}
}
fn graph(&mut self, ui: &mut egui::Ui) -> egui::Response {
use egui::*;
ui.label("egui CPU usage history");
let history = &self.frame_times;
// TODO(emilk): we should not use `slider_width` as default graph width.
let height = ui.spacing().slider_width;
let size = vec2(ui.available_size_before_wrap().x, height);
let (rect, response) = ui.allocate_at_least(size, Sense::hover());
let style = ui.style().noninteractive();
let graph_top_cpu_usage = 0.010;
let graph_rect = Rect::from_x_y_ranges(history.max_age()..=0.0, graph_top_cpu_usage..=0.0);
let to_screen = emath::RectTransform::from_to(graph_rect, rect);
let mut shapes = Vec::with_capacity(3 + 2 * history.len());
shapes.push(Shape::Rect(epaint::RectShape {
rect,
rounding: style.rounding,
fill: ui.visuals().extreme_bg_color,
stroke: ui.style().noninteractive().bg_stroke,
}));
let rect = rect.shrink(4.0);
let color = ui.visuals().text_color();
let line_stroke = Stroke::new(1.0, color);
if let Some(pointer_pos) = response.hover_pos() {
let y = pointer_pos.y;
shapes.push(Shape::line_segment(
[pos2(rect.left(), y), pos2(rect.right(), y)],
line_stroke,
));
let cpu_usage = to_screen.inverse().transform_pos(pointer_pos).y;
let text = format!("{:.1} ms", 1e3 * cpu_usage);
shapes.push(ui.fonts(|f| {
Shape::text(
f,
pos2(rect.left(), y),
egui::Align2::LEFT_BOTTOM,
text,
TextStyle::Monospace.resolve(ui.style()),
color,
)
}));
}
let circle_color = color;
let radius = 2.0;
let right_side_time = ui.input(|i| i.time); // Time at right side of screen
for (time, cpu_usage) in history.iter() {
let age = (right_side_time - time) as f32;
let pos = to_screen.transform_pos_clamped(Pos2::new(age, cpu_usage));
shapes.push(Shape::line_segment(
[pos2(pos.x, rect.bottom()), pos],
line_stroke,
));
if cpu_usage < graph_top_cpu_usage {
shapes.push(Shape::circle_filled(pos, radius, circle_color));
}
}
ui.painter().extend(shapes);
response
}
}

View file

@ -0,0 +1,80 @@
//! Demo app for egui
#![allow(clippy::missing_errors_doc)]
mod apps;
mod backend_panel;
pub(crate) mod frame_history;
mod wrap_app;
#[cfg(target_arch = "wasm32")]
use eframe::web::AppRunnerRef;
pub use wrap_app::WrapApp;
/// Time of day as seconds since midnight. Used for clock in demo app.
pub(crate) fn seconds_since_midnight() -> f64 {
use chrono::Timelike;
let time = chrono::Local::now().time();
time.num_seconds_from_midnight() as f64 + 1e-9 * (time.nanosecond() as f64)
}
// ----------------------------------------------------------------------------
#[cfg(target_arch = "wasm32")]
use eframe::wasm_bindgen::{self, prelude::*};
#[cfg(target_arch = "wasm32")]
#[wasm_bindgen]
pub struct WebHandle {
handle: AppRunnerRef,
}
#[cfg(target_arch = "wasm32")]
#[wasm_bindgen]
impl WebHandle {
#[wasm_bindgen]
pub fn stop_web(&self) -> Result<(), wasm_bindgen::JsValue> {
let mut app = self.handle.lock();
app.destroy()
}
#[wasm_bindgen]
pub fn set_some_content_from_javascript(&mut self, _some_data: &str) {
let _app = self.handle.lock().app_mut::<WrapApp>();
// _app.data = some_data;
}
}
#[cfg(target_arch = "wasm32")]
#[wasm_bindgen]
pub fn init_wasm_hooks() {
// Make sure panics are logged using `console.error`.
console_error_panic_hook::set_once();
// Redirect tracing to console.log and friends:
tracing_wasm::set_as_global_default();
}
#[cfg(target_arch = "wasm32")]
#[wasm_bindgen]
pub async fn start_separate(canvas_id: &str) -> Result<WebHandle, wasm_bindgen::JsValue> {
let web_options = eframe::WebOptions::default();
eframe::start_web(
canvas_id,
web_options,
Box::new(|cc| Box::new(WrapApp::new(cc))),
)
.await
.map(|handle| WebHandle { handle })
}
/// This is the entry-point for all the web-assembly.
/// This is called once from the HTML.
/// It loads the app, installs some callbacks, then returns.
/// You can add more callbacks like this if you want to call in to your code.
#[cfg(target_arch = "wasm32")]
#[wasm_bindgen]
pub async fn start(canvas_id: &str) -> Result<WebHandle, wasm_bindgen::JsValue> {
init_wasm_hooks();
start_separate(canvas_id).await
}

View file

@ -0,0 +1,36 @@
//! Demo app for egui
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
// When compiling natively:
fn main() -> Result<(), eframe::Error> {
{
// Silence wgpu log spam (https://github.com/gfx-rs/wgpu/issues/3206)
let mut rust_log = std::env::var("RUST_LOG").unwrap_or_else(|_| "info".to_owned());
for loud_crate in ["naga", "wgpu_core", "wgpu_hal"] {
if !rust_log.contains(&format!("{loud_crate}=")) {
rust_log += &format!(",{loud_crate}=warn");
}
}
std::env::set_var("RUST_LOG", rust_log);
}
// Log to stdout (if you run with `RUST_LOG=debug`).
tracing_subscriber::fmt::init();
let options = eframe::NativeOptions {
drag_and_drop_support: true,
initial_window_size: Some([1280.0, 1024.0].into()),
#[cfg(feature = "wgpu")]
renderer: eframe::Renderer::Wgpu,
..Default::default()
};
eframe::run_native(
"egui demo app",
options,
Box::new(|cc| Box::new(egui_demo_app::WrapApp::new(cc))),
)
}

View file

@ -0,0 +1,457 @@
use egui_demo_lib::is_mobile;
#[cfg(feature = "glow")]
use eframe::glow;
#[cfg(target_arch = "wasm32")]
use core::any::Any;
#[derive(Default)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
struct EasyMarkApp {
editor: egui_demo_lib::easy_mark::EasyMarkEditor,
}
impl eframe::App for EasyMarkApp {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
self.editor.panels(ctx);
}
}
// ----------------------------------------------------------------------------
#[derive(Default)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct DemoApp {
demo_windows: egui_demo_lib::DemoWindows,
}
impl eframe::App for DemoApp {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
self.demo_windows.ui(ctx);
}
}
// ----------------------------------------------------------------------------
#[derive(Default)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct FractalClockApp {
fractal_clock: crate::apps::FractalClock,
}
impl eframe::App for FractalClockApp {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
egui::CentralPanel::default()
.frame(egui::Frame::dark_canvas(&ctx.style()))
.show(ctx, |ui| {
self.fractal_clock
.ui(ui, Some(crate::seconds_since_midnight()));
});
}
}
// ----------------------------------------------------------------------------
#[derive(Default)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct ColorTestApp {
color_test: egui_demo_lib::ColorTest,
}
impl eframe::App for ColorTestApp {
fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
egui::CentralPanel::default().show(ctx, |ui| {
if frame.is_web() {
ui.label(
"NOTE: Some old browsers stuck on WebGL1 without sRGB support will not pass the color test.",
);
ui.separator();
}
egui::ScrollArea::both().auto_shrink([false; 2]).show(ui, |ui| {
self.color_test.ui(ui);
});
});
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
enum Anchor {
Demo,
EasyMarkEditor,
#[cfg(feature = "http")]
Http,
Clock,
#[cfg(any(feature = "glow", feature = "wgpu"))]
Custom3d,
Colors,
}
impl Anchor {
#[cfg(target_arch = "wasm32")]
fn all() -> Vec<Self> {
vec![
Anchor::Demo,
Anchor::EasyMarkEditor,
#[cfg(feature = "http")]
Anchor::Http,
Anchor::Clock,
#[cfg(any(feature = "glow", feature = "wgpu"))]
Anchor::Custom3d,
Anchor::Colors,
]
}
}
impl std::fmt::Display for Anchor {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{self:?}")
}
}
impl From<Anchor> for egui::WidgetText {
fn from(value: Anchor) -> Self {
Self::RichText(egui::RichText::new(value.to_string()))
}
}
impl Default for Anchor {
fn default() -> Self {
Self::Demo
}
}
// ----------------------------------------------------------------------------
/// The state that we persist (serialize).
#[derive(Default)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "serde", serde(default))]
pub struct State {
demo: DemoApp,
easy_mark_editor: EasyMarkApp,
#[cfg(feature = "http")]
http: crate::apps::HttpApp,
clock: FractalClockApp,
color_test: ColorTestApp,
selected_anchor: Anchor,
backend_panel: super::backend_panel::BackendPanel,
}
/// Wraps many demo/test apps into one.
pub struct WrapApp {
state: State,
#[cfg(any(feature = "glow", feature = "wgpu"))]
custom3d: Option<crate::apps::Custom3d>,
dropped_files: Vec<egui::DroppedFile>,
}
impl WrapApp {
pub fn new(_cc: &eframe::CreationContext<'_>) -> Self {
#[allow(unused_mut)]
let mut slf = Self {
state: State::default(),
#[cfg(any(feature = "glow", feature = "wgpu"))]
custom3d: crate::apps::Custom3d::new(_cc),
dropped_files: Default::default(),
};
#[cfg(feature = "persistence")]
if let Some(storage) = _cc.storage {
if let Some(state) = eframe::get_value(storage, eframe::APP_KEY) {
slf.state = state;
}
}
slf
}
fn apps_iter_mut(&mut self) -> impl Iterator<Item = (&str, Anchor, &mut dyn eframe::App)> {
let mut vec = vec![
(
"✨ Demos",
Anchor::Demo,
&mut self.state.demo as &mut dyn eframe::App,
),
(
"🖹 EasyMark editor",
Anchor::EasyMarkEditor,
&mut self.state.easy_mark_editor as &mut dyn eframe::App,
),
#[cfg(feature = "http")]
(
"⬇ HTTP",
Anchor::Http,
&mut self.state.http as &mut dyn eframe::App,
),
(
"🕑 Fractal Clock",
Anchor::Clock,
&mut self.state.clock as &mut dyn eframe::App,
),
];
#[cfg(any(feature = "glow", feature = "wgpu"))]
if let Some(custom3d) = &mut self.custom3d {
vec.push((
"🔺 3D painting",
Anchor::Custom3d,
custom3d as &mut dyn eframe::App,
));
}
vec.push((
"🎨 Color test",
Anchor::Colors,
&mut self.state.color_test as &mut dyn eframe::App,
));
vec.into_iter()
}
}
impl eframe::App for WrapApp {
#[cfg(feature = "persistence")]
fn save(&mut self, storage: &mut dyn eframe::Storage) {
eframe::set_value(storage, eframe::APP_KEY, &self.state);
}
fn clear_color(&self, visuals: &egui::Visuals) -> [f32; 4] {
visuals.panel_fill.to_normalized_gamma_f32()
}
fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
#[cfg(target_arch = "wasm32")]
if let Some(anchor) = frame.info().web_info.location.hash.strip_prefix('#') {
let anchor = Anchor::all().into_iter().find(|x| x.to_string() == anchor);
if let Some(v) = anchor {
self.state.selected_anchor = v;
}
}
#[cfg(not(target_arch = "wasm32"))]
if ctx.input_mut(|i| i.consume_key(egui::Modifiers::NONE, egui::Key::F11)) {
frame.set_fullscreen(!frame.info().window_info.fullscreen);
}
egui::TopBottomPanel::top("wrap_app_top_bar").show(ctx, |ui| {
egui::trace!(ui);
ui.horizontal_wrapped(|ui| {
ui.visuals_mut().button_frame = false;
self.bar_contents(ui, frame);
});
});
self.state.backend_panel.update(ctx, frame);
if !is_mobile(ctx) {
self.backend_panel(ctx, frame);
}
self.show_selected_app(ctx, frame);
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")]
fn on_exit(&mut self, gl: Option<&glow::Context>) {
if let Some(custom3d) = &mut self.custom3d {
custom3d.on_exit(gl);
}
}
#[cfg(target_arch = "wasm32")]
fn as_any_mut(&mut self) -> Option<&mut dyn Any> {
Some(&mut *self)
}
}
impl WrapApp {
fn backend_panel(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
// The backend-panel can be toggled on/off.
// We show a little animation when the user switches it.
let is_open =
self.state.backend_panel.open || ctx.memory(|mem| mem.everything_is_visible());
egui::SidePanel::left("backend_panel")
.resizable(false)
.show_animated(ctx, is_open, |ui| {
ui.vertical_centered(|ui| {
ui.heading("💻 Backend");
});
ui.separator();
self.backend_panel_contents(ui, frame);
});
}
fn backend_panel_contents(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame) {
self.state.backend_panel.ui(ui, frame);
ui.separator();
ui.horizontal(|ui| {
if ui
.button("Reset egui")
.on_hover_text("Forget scroll, positions, sizes etc")
.clicked()
{
ui.ctx().memory_mut(|mem| *mem = Default::default());
ui.close_menu();
}
if ui.button("Reset everything").clicked() {
self.state = Default::default();
ui.ctx().memory_mut(|mem| *mem = Default::default());
ui.close_menu();
}
});
}
fn show_selected_app(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
let selected_anchor = self.state.selected_anchor;
for (_name, anchor, app) in self.apps_iter_mut() {
if anchor == selected_anchor || ctx.memory(|mem| mem.everything_is_visible()) {
app.update(ctx, frame);
}
}
}
fn bar_contents(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame) {
egui::widgets::global_dark_light_mode_switch(ui);
ui.separator();
if is_mobile(ui.ctx()) {
ui.menu_button("💻 Backend", |ui| {
ui.set_style(ui.ctx().style()); // ignore the "menu" style set by `menu_button`.
self.backend_panel_contents(ui, frame);
});
} else {
ui.toggle_value(&mut self.state.backend_panel.open, "💻 Backend");
}
ui.separator();
let mut selected_anchor = self.state.selected_anchor;
for (name, anchor, _app) in self.apps_iter_mut() {
if ui
.selectable_label(selected_anchor == anchor, name)
.clicked()
{
selected_anchor = anchor;
if frame.is_web() {
ui.output_mut(|o| o.open_url(format!("#{}", anchor)));
}
}
}
self.state.selected_anchor = selected_anchor;
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
if false {
// TODO(emilk): fix the overlap on small screens
if clock_button(ui, crate::seconds_since_midnight()).clicked() {
self.state.selected_anchor = Anchor::Clock;
if frame.is_web() {
ui.output_mut(|o| o.open_url("#clock"));
}
}
}
egui::warn_if_debug_build(ui);
});
}
fn ui_file_drag_and_drop(&mut self, ctx: &egui::Context) {
use egui::*;
use std::fmt::Write as _;
// Preview hovering files:
if !ctx.input(|i| i.raw.hovered_files.is_empty()) {
let text = ctx.input(|i| {
let mut text = "Dropping files:\n".to_owned();
for file in &i.raw.hovered_files {
if let Some(path) = &file.path {
write!(text, "\n{}", path.display()).ok();
} else if !file.mime.is_empty() {
write!(text, "\n{}", file.mime).ok();
} else {
text += "\n???";
}
}
text
});
let painter =
ctx.layer_painter(LayerId::new(Order::Foreground, Id::new("file_drop_target")));
let screen_rect = ctx.screen_rect();
painter.rect_filled(screen_rect, 0.0, Color32::from_black_alpha(192));
painter.text(
screen_rect.center(),
Align2::CENTER_CENTER,
text,
TextStyle::Heading.resolve(&ctx.style()),
Color32::WHITE,
);
}
// Collect dropped files:
ctx.input(|i| {
if !i.raw.dropped_files.is_empty() {
self.dropped_files = i.raw.dropped_files.clone();
}
});
// Show dropped files (if any):
if !self.dropped_files.is_empty() {
let mut open = true;
egui::Window::new("Dropped files")
.open(&mut open)
.show(ctx, |ui| {
for file in &self.dropped_files {
let mut info = if let Some(path) = &file.path {
path.display().to_string()
} else if !file.name.is_empty() {
file.name.clone()
} else {
"???".to_owned()
};
if let Some(bytes) = &file.bytes {
write!(info, " ({} bytes)", bytes.len()).ok();
}
ui.label(info);
}
});
if !open {
self.dropped_files.clear();
}
}
}
}
fn clock_button(ui: &mut egui::Ui, seconds_since_midnight: f64) -> egui::Response {
let time = seconds_since_midnight;
let time = format!(
"{:02}:{:02}:{:02}.{:02}",
(time % (24.0 * 60.0 * 60.0) / 3600.0).floor(),
(time % (60.0 * 60.0) / 60.0).floor(),
(time % 60.0).floor(),
(time % 1.0 * 100.0).floor()
);
ui.button(egui::RichText::new(time).monospace())
}