mirror of
https://github.com/emilk/egui.git
synced 2026-08-31 05:40:03 -04:00
Merge example_web into egui_demo_lib
This commit is contained in:
349
egui_demo_lib/src/apps/demo/app.rs
Normal file
349
egui_demo_lib/src/apps/demo/app.rs
Normal file
@@ -0,0 +1,349 @@
|
||||
use egui::{util::History, *};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// 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` 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
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
struct FrameHistory {
|
||||
frame_times: History<f32>,
|
||||
}
|
||||
|
||||
impl Default for FrameHistory {
|
||||
fn default() -> Self {
|
||||
let max_age: f64 = 1.0;
|
||||
Self {
|
||||
frame_times: History::from_max_len_age((max_age * 300.0).round() as usize, max_age),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FrameHistory {
|
||||
// Called first
|
||||
pub fn on_new_frame(&mut self, now: f64, previus_frame_time: Option<f32>) {
|
||||
let previus_frame_time = previus_frame_time.unwrap_or_default();
|
||||
if let Some(latest) = self.frame_times.latest_mut() {
|
||||
*latest = previus_frame_time; // rewrite history now that we know
|
||||
}
|
||||
self.frame_times.add(now, previus_frame_time); // projected
|
||||
}
|
||||
|
||||
fn mean_frame_time(&self) -> f32 {
|
||||
self.frame_times.average().unwrap_or_default()
|
||||
}
|
||||
|
||||
fn fps(&self) -> f32 {
|
||||
1.0 / self.frame_times.mean_time_interval().unwrap_or_default()
|
||||
}
|
||||
|
||||
fn ui(&mut self, ui: &mut Ui) {
|
||||
ui.label(format!(
|
||||
"Total frames painted (including this one): {}",
|
||||
self.frame_times.total_count()
|
||||
));
|
||||
|
||||
ui.label(format!(
|
||||
"Mean CPU usage per frame: {:.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);
|
||||
|
||||
egui::CollapsingHeader::new("📊 CPU usage history")
|
||||
.default_open(false)
|
||||
.show(ui, |ui| {
|
||||
self.graph(ui);
|
||||
});
|
||||
}
|
||||
|
||||
fn graph(&mut self, ui: &mut Ui) -> Response {
|
||||
let graph_top_cpu_usage = 0.010;
|
||||
ui.label("Egui CPU usage history");
|
||||
|
||||
let history = &self.frame_times;
|
||||
|
||||
// TODO: we should not use `slider_width` as default graph width.
|
||||
let height = ui.style().spacing.slider_width;
|
||||
let size = vec2(ui.available_size_before_wrap_finite().x, height);
|
||||
let response = ui.allocate_response(size, Sense::hover());
|
||||
let rect = response.rect;
|
||||
let style = ui.style().noninteractive();
|
||||
|
||||
let mut cmds = vec![PaintCmd::Rect {
|
||||
rect,
|
||||
corner_radius: style.corner_radius,
|
||||
fill: ui.style().visuals.dark_bg_color,
|
||||
stroke: ui.style().noninteractive().bg_stroke,
|
||||
}];
|
||||
|
||||
let rect = rect.shrink(4.0);
|
||||
let line_stroke = Stroke::new(1.0, Srgba::additive_luminance(128));
|
||||
|
||||
if let Some(mouse_pos) = ui.input().mouse.pos {
|
||||
if rect.contains(mouse_pos) {
|
||||
let y = mouse_pos.y;
|
||||
cmds.push(PaintCmd::line_segment(
|
||||
[pos2(rect.left(), y), pos2(rect.right(), y)],
|
||||
line_stroke,
|
||||
));
|
||||
let cpu_usage = remap(y, rect.bottom_up_range(), 0.0..=graph_top_cpu_usage);
|
||||
let text = format!("{:.1} ms", 1e3 * cpu_usage);
|
||||
cmds.push(PaintCmd::text(
|
||||
ui.fonts(),
|
||||
pos2(rect.left(), y),
|
||||
align::LEFT_BOTTOM,
|
||||
text,
|
||||
TextStyle::Monospace,
|
||||
color::WHITE,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let circle_color = Srgba::additive_luminance(196);
|
||||
let radius = 2.0;
|
||||
let right_side_time = ui.input().time; // Time at right side of screen
|
||||
|
||||
for (time, cpu_usage) in history.iter() {
|
||||
let age = (right_side_time - time) as f32;
|
||||
let x = remap(age, history.max_age()..=0.0, rect.x_range());
|
||||
let y = remap_clamp(cpu_usage, 0.0..=graph_top_cpu_usage, rect.bottom_up_range());
|
||||
|
||||
cmds.push(PaintCmd::line_segment(
|
||||
[pos2(x, rect.bottom()), pos2(x, y)],
|
||||
line_stroke,
|
||||
));
|
||||
|
||||
if cpu_usage < graph_top_cpu_usage {
|
||||
cmds.push(PaintCmd::circle_filled(pos2(x, y), radius, circle_color));
|
||||
}
|
||||
}
|
||||
|
||||
ui.painter().extend(cmds);
|
||||
|
||||
response
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// Demonstrates how to make an app using Egui.
|
||||
///
|
||||
/// Implements `epi::App` so it can be used with
|
||||
/// [`egui_glium`](https://crates.io/crates/egui_glium) and [`egui_web`](https://crates.io/crates/egui_web).
|
||||
#[derive(Default, serde::Deserialize, serde::Serialize)]
|
||||
#[serde(default)]
|
||||
pub struct DemoApp {
|
||||
demo_windows: super::DemoWindows,
|
||||
|
||||
backend_window_open: bool,
|
||||
|
||||
#[serde(skip)] // go back to `Reactive` mode each time we start
|
||||
run_mode: RunMode,
|
||||
|
||||
/// current slider value for current gui scale (backend demo only)
|
||||
pixels_per_point: Option<f32>,
|
||||
|
||||
#[serde(skip)]
|
||||
frame_history: FrameHistory,
|
||||
}
|
||||
|
||||
impl DemoApp {
|
||||
fn backend_ui(&mut self, ui: &mut Ui, frame: &mut epi::Frame<'_>) {
|
||||
let is_web = frame.is_web();
|
||||
|
||||
if is_web {
|
||||
ui.label("Egui is an immediate mode GUI written in Rust, compiled to WebAssembly, rendered with WebGL.");
|
||||
ui.label(
|
||||
"Everything you see is rendered as textured triangles. There is no DOM. There are no HTML elements. \
|
||||
This is not JavaScript. This is Rust, running at 60 FPS. This is the web page, reinvented with game tech.");
|
||||
ui.label("This is also work in progress, and not ready for production... yet :)");
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Project home page:");
|
||||
ui.hyperlink("https://github.com/emilk/egui");
|
||||
});
|
||||
ui.separator();
|
||||
}
|
||||
|
||||
self.run_mode_ui(ui);
|
||||
|
||||
ui.separator();
|
||||
|
||||
self.frame_history.ui(ui);
|
||||
|
||||
if !is_web {
|
||||
// web browsers have their own way of zooming, which egui_web respects
|
||||
ui.separator();
|
||||
if let Some(new_pixels_per_point) = self.pixels_per_point_ui(ui, frame.info()) {
|
||||
frame.set_pixels_per_point(new_pixels_per_point);
|
||||
}
|
||||
}
|
||||
|
||||
if !is_web {
|
||||
ui.separator();
|
||||
if ui.button("Quit").clicked {
|
||||
frame.quit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn pixels_per_point_ui(&mut self, ui: &mut Ui, info: &epi::IntegrationInfo) -> 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(
|
||||
egui::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) {
|
||||
ui.horizontal(|ui| {
|
||||
let run_mode = &mut self.run_mode;
|
||||
ui.label("Run mode:");
|
||||
ui.radio_value(run_mode, RunMode::Continuous, "Continuous")
|
||||
.on_hover_text("Repaint everything each frame");
|
||||
ui.radio_value(run_mode, RunMode::Reactive, "Reactive")
|
||||
.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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl epi::App for DemoApp {
|
||||
fn name(&self) -> &str {
|
||||
"Egui Demo"
|
||||
}
|
||||
|
||||
fn load(&mut self, storage: &dyn epi::Storage) {
|
||||
*self = epi::get_value(storage, epi::APP_KEY).unwrap_or_default()
|
||||
}
|
||||
|
||||
fn save(&mut self, storage: &mut dyn epi::Storage) {
|
||||
epi::set_value(storage, epi::APP_KEY, self);
|
||||
}
|
||||
|
||||
fn ui(&mut self, ctx: &CtxRef, frame: &mut epi::Frame<'_>) {
|
||||
self.frame_history
|
||||
.on_new_frame(ctx.input().time, frame.info().cpu_usage);
|
||||
|
||||
let web_location_hash = frame
|
||||
.info()
|
||||
.web_info
|
||||
.as_ref()
|
||||
.map(|info| info.web_location_hash.clone())
|
||||
.unwrap_or_default();
|
||||
|
||||
let link = if web_location_hash == "clock" {
|
||||
Some(super::DemoLink::Clock)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let demo_environment = super::DemoEnvironment {
|
||||
seconds_since_midnight: frame.info().seconds_since_midnight,
|
||||
link,
|
||||
};
|
||||
|
||||
let mean_frame_time = self.frame_history.mean_frame_time();
|
||||
|
||||
let Self {
|
||||
demo_windows,
|
||||
backend_window_open,
|
||||
..
|
||||
} = self;
|
||||
|
||||
demo_windows.ui(ctx, &demo_environment, frame.tex_allocator(), |ui| {
|
||||
ui.separator();
|
||||
ui.checkbox(backend_window_open, "💻 Backend");
|
||||
|
||||
ui.label(format!("{:.2} ms / frame", 1e3 * mean_frame_time))
|
||||
.on_hover_text("CPU usage.");
|
||||
});
|
||||
|
||||
let mut backend_window_open = self.backend_window_open;
|
||||
egui::Window::new("💻 Backend")
|
||||
.min_width(360.0)
|
||||
.scroll(false)
|
||||
.open(&mut backend_window_open)
|
||||
.show(ctx, |ui| {
|
||||
self.backend_ui(ui, frame);
|
||||
});
|
||||
self.backend_window_open = backend_window_open;
|
||||
|
||||
if self.run_mode == RunMode::Continuous {
|
||||
// Tell the backend to repaint as soon as possible
|
||||
ctx.request_repaint();
|
||||
}
|
||||
}
|
||||
}
|
||||
373
egui_demo_lib/src/apps/demo/color_test.rs
Normal file
373
egui_demo_lib/src/apps/demo/color_test.rs
Normal file
@@ -0,0 +1,373 @@
|
||||
use egui::{color::*, widgets::color_picker::show_color, *};
|
||||
use std::collections::HashMap;
|
||||
|
||||
const GRADIENT_SIZE: Vec2 = vec2(256.0, 24.0);
|
||||
|
||||
pub struct ColorTest {
|
||||
tex_mngr: TextureManager,
|
||||
vertex_gradients: bool,
|
||||
texture_gradients: bool,
|
||||
srgb: bool,
|
||||
}
|
||||
|
||||
impl Default for ColorTest {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
tex_mngr: Default::default(),
|
||||
vertex_gradients: true,
|
||||
texture_gradients: true,
|
||||
srgb: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ColorTest {
|
||||
pub fn ui(
|
||||
&mut self,
|
||||
ui: &mut Ui,
|
||||
mut tex_allocator: &mut Option<&mut dyn epi::TextureAllocator>,
|
||||
) {
|
||||
ui.label("This is made to test if your Egui painter backend is set up correctly");
|
||||
ui.label("It is meant to ensure you do proper sRGBA decoding of both texture and vertex colors, and blend using premultiplied alpha.");
|
||||
ui.label("If everything is set up correctly, all groups of gradients will look uniform");
|
||||
|
||||
ui.checkbox(&mut self.vertex_gradients, "Vertex gradients");
|
||||
ui.checkbox(&mut self.texture_gradients, "Texture gradients");
|
||||
ui.checkbox(&mut self.srgb, "Show naive sRGBA horror");
|
||||
|
||||
ui.heading("sRGB color test");
|
||||
ui.label("Use a color picker to ensure this color is (255, 165, 0) / #ffa500");
|
||||
ui.wrap(|ui| {
|
||||
ui.style_mut().spacing.item_spacing.y = 0.0; // No spacing between gradients
|
||||
let g = Gradient::one_color(Srgba::from_rgb(255, 165, 0));
|
||||
self.vertex_gradient(ui, "orange rgb(255, 165, 0) - vertex", WHITE, &g);
|
||||
self.tex_gradient(
|
||||
ui,
|
||||
tex_allocator,
|
||||
"orange rgb(255, 165, 0) - texture",
|
||||
WHITE,
|
||||
&g,
|
||||
);
|
||||
});
|
||||
|
||||
ui.separator();
|
||||
|
||||
ui.label("Test that vertex color times texture color is done in linear space:");
|
||||
ui.wrap(|ui| {
|
||||
ui.style_mut().spacing.item_spacing.y = 0.0; // No spacing between gradients
|
||||
|
||||
let tex_color = Rgba::new(1.0, 0.25, 0.25, 1.0);
|
||||
let vertex_color = Rgba::new(0.5, 0.75, 0.75, 1.0);
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
let color_size = ui.style().spacing.interact_size;
|
||||
ui.label("texture");
|
||||
show_color(ui, tex_color, color_size);
|
||||
ui.label(" * ");
|
||||
show_color(ui, vertex_color, color_size);
|
||||
ui.label(" vertex color =");
|
||||
});
|
||||
{
|
||||
let g = Gradient::one_color(Srgba::from(tex_color * vertex_color));
|
||||
self.vertex_gradient(ui, "Ground truth (vertices)", WHITE, &g);
|
||||
self.tex_gradient(ui, tex_allocator, "Ground truth (texture)", WHITE, &g);
|
||||
}
|
||||
if let Some(tex_allocator) = &mut tex_allocator {
|
||||
ui.horizontal(|ui| {
|
||||
let g = Gradient::one_color(Srgba::from(tex_color));
|
||||
let tex = self.tex_mngr.get(*tex_allocator, &g);
|
||||
let texel_offset = 0.5 / (g.0.len() as f32);
|
||||
let uv =
|
||||
Rect::from_min_max(pos2(texel_offset, 0.0), pos2(1.0 - texel_offset, 1.0));
|
||||
ui.add(Image::new(tex, GRADIENT_SIZE).tint(vertex_color).uv(uv))
|
||||
.on_hover_text(format!("A texture that is {} texels wide", g.0.len()));
|
||||
ui.label("GPU result");
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
ui.separator();
|
||||
|
||||
// TODO: test color multiplication (image tint),
|
||||
// to make sure vertex and texture color multiplication is done in linear space.
|
||||
|
||||
self.show_gradients(ui, tex_allocator, WHITE, (RED, GREEN));
|
||||
if self.srgb {
|
||||
ui.label("Notice the darkening in the center of the naive sRGB interpolation.");
|
||||
}
|
||||
|
||||
ui.separator();
|
||||
|
||||
self.show_gradients(ui, tex_allocator, RED, (TRANSPARENT, GREEN));
|
||||
|
||||
ui.separator();
|
||||
|
||||
self.show_gradients(ui, tex_allocator, WHITE, (TRANSPARENT, GREEN));
|
||||
if self.srgb {
|
||||
ui.label(
|
||||
"Notice how the linear blend stays green while the naive sRGBA interpolation looks gray in the middle.",
|
||||
);
|
||||
}
|
||||
|
||||
ui.separator();
|
||||
|
||||
// TODO: another ground truth where we do the alpha-blending against the background also.
|
||||
// TODO: exactly the same thing, but with vertex colors (no textures)
|
||||
self.show_gradients(ui, tex_allocator, WHITE, (TRANSPARENT, BLACK));
|
||||
ui.separator();
|
||||
self.show_gradients(ui, tex_allocator, BLACK, (TRANSPARENT, WHITE));
|
||||
ui.separator();
|
||||
|
||||
ui.label("Additive blending: add more and more blue to the red background:");
|
||||
self.show_gradients(
|
||||
ui,
|
||||
tex_allocator,
|
||||
RED,
|
||||
(TRANSPARENT, Srgba::from_rgba_premultiplied(0, 0, 255, 0)),
|
||||
);
|
||||
|
||||
ui.separator();
|
||||
}
|
||||
|
||||
fn show_gradients(
|
||||
&mut self,
|
||||
ui: &mut Ui,
|
||||
tex_allocator: &mut Option<&mut dyn epi::TextureAllocator>,
|
||||
bg_fill: Srgba,
|
||||
(left, right): (Srgba, Srgba),
|
||||
) {
|
||||
let is_opaque = left.is_opaque() && right.is_opaque();
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
let color_size = ui.style().spacing.interact_size;
|
||||
if !is_opaque {
|
||||
ui.label("Background:");
|
||||
show_color(ui, bg_fill, color_size);
|
||||
}
|
||||
ui.label("gradient");
|
||||
show_color(ui, left, color_size);
|
||||
ui.label("-");
|
||||
show_color(ui, right, color_size);
|
||||
});
|
||||
|
||||
ui.wrap(|ui| {
|
||||
ui.style_mut().spacing.item_spacing.y = 0.0; // No spacing between gradients
|
||||
if is_opaque {
|
||||
let g = Gradient::ground_truth_linear_gradient(left, right);
|
||||
self.vertex_gradient(ui, "Ground Truth (CPU gradient) - vertices", bg_fill, &g);
|
||||
self.tex_gradient(
|
||||
ui,
|
||||
tex_allocator,
|
||||
"Ground Truth (CPU gradient) - texture",
|
||||
bg_fill,
|
||||
&g,
|
||||
);
|
||||
} else {
|
||||
let g = Gradient::ground_truth_linear_gradient(left, right).with_bg_fill(bg_fill);
|
||||
self.vertex_gradient(
|
||||
ui,
|
||||
"Ground Truth (CPU gradient, CPU blending) - vertices",
|
||||
bg_fill,
|
||||
&g,
|
||||
);
|
||||
self.tex_gradient(
|
||||
ui,
|
||||
tex_allocator,
|
||||
"Ground Truth (CPU gradient, CPU blending) - texture",
|
||||
bg_fill,
|
||||
&g,
|
||||
);
|
||||
let g = Gradient::ground_truth_linear_gradient(left, right);
|
||||
self.vertex_gradient(ui, "CPU gradient, GPU blending - vertices", bg_fill, &g);
|
||||
self.tex_gradient(
|
||||
ui,
|
||||
tex_allocator,
|
||||
"CPU gradient, GPU blending - texture",
|
||||
bg_fill,
|
||||
&g,
|
||||
);
|
||||
}
|
||||
|
||||
let g = Gradient::texture_gradient(left, right);
|
||||
self.vertex_gradient(
|
||||
ui,
|
||||
"Triangle mesh of width 2 (test vertex decode and interpolation)",
|
||||
bg_fill,
|
||||
&g,
|
||||
);
|
||||
self.tex_gradient(
|
||||
ui,
|
||||
tex_allocator,
|
||||
"Texture of width 2 (test texture sampler)",
|
||||
bg_fill,
|
||||
&g,
|
||||
);
|
||||
|
||||
if self.srgb {
|
||||
let g =
|
||||
Gradient::ground_truth_bad_srgba_gradient(left, right).with_bg_fill(bg_fill);
|
||||
self.vertex_gradient(
|
||||
ui,
|
||||
"Triangle mesh with naive sRGBA interpolation (WRONG)",
|
||||
bg_fill,
|
||||
&g,
|
||||
);
|
||||
self.tex_gradient(
|
||||
ui,
|
||||
tex_allocator,
|
||||
"Naive sRGBA interpolation (WRONG)",
|
||||
bg_fill,
|
||||
&g,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn tex_gradient(
|
||||
&mut self,
|
||||
ui: &mut Ui,
|
||||
tex_allocator: &mut Option<&mut dyn epi::TextureAllocator>,
|
||||
label: &str,
|
||||
bg_fill: Srgba,
|
||||
gradient: &Gradient,
|
||||
) {
|
||||
if !self.texture_gradients {
|
||||
return;
|
||||
}
|
||||
if let Some(tex_allocator) = tex_allocator {
|
||||
ui.horizontal(|ui| {
|
||||
let tex = self.tex_mngr.get(*tex_allocator, gradient);
|
||||
let texel_offset = 0.5 / (gradient.0.len() as f32);
|
||||
let uv = Rect::from_min_max(pos2(texel_offset, 0.0), pos2(1.0 - texel_offset, 1.0));
|
||||
ui.add(Image::new(tex, GRADIENT_SIZE).bg_fill(bg_fill).uv(uv))
|
||||
.on_hover_text(format!(
|
||||
"A texture that is {} texels wide",
|
||||
gradient.0.len()
|
||||
));
|
||||
ui.label(label);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn vertex_gradient(&mut self, ui: &mut Ui, label: &str, bg_fill: Srgba, gradient: &Gradient) {
|
||||
if !self.vertex_gradients {
|
||||
return;
|
||||
}
|
||||
ui.horizontal(|ui| {
|
||||
vertex_gradient(ui, bg_fill, gradient).on_hover_text(format!(
|
||||
"A triangle mesh that is {} vertices wide",
|
||||
gradient.0.len()
|
||||
));
|
||||
ui.label(label);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn vertex_gradient(ui: &mut Ui, bg_fill: Srgba, gradient: &Gradient) -> Response {
|
||||
use egui::paint::*;
|
||||
let response = ui.allocate_response(GRADIENT_SIZE, Sense::hover());
|
||||
if bg_fill != Default::default() {
|
||||
let mut triangles = Triangles::default();
|
||||
triangles.add_colored_rect(response.rect, bg_fill);
|
||||
ui.painter().add(PaintCmd::triangles(triangles));
|
||||
}
|
||||
{
|
||||
let n = gradient.0.len();
|
||||
assert!(n >= 2);
|
||||
let mut triangles = Triangles::default();
|
||||
for (i, &color) in gradient.0.iter().enumerate() {
|
||||
let t = i as f32 / (n as f32 - 1.0);
|
||||
let x = lerp(response.rect.x_range(), t);
|
||||
triangles.colored_vertex(pos2(x, response.rect.top()), color);
|
||||
triangles.colored_vertex(pos2(x, response.rect.bottom()), color);
|
||||
if i < n - 1 {
|
||||
let i = i as u32;
|
||||
triangles.add_triangle(2 * i, 2 * i + 1, 2 * i + 2);
|
||||
triangles.add_triangle(2 * i + 1, 2 * i + 2, 2 * i + 3);
|
||||
}
|
||||
}
|
||||
ui.painter().add(PaintCmd::triangles(triangles));
|
||||
}
|
||||
response
|
||||
}
|
||||
|
||||
#[derive(Clone, Hash, PartialEq, Eq)]
|
||||
struct Gradient(pub Vec<Srgba>);
|
||||
|
||||
impl Gradient {
|
||||
pub fn one_color(srgba: Srgba) -> Self {
|
||||
Self(vec![srgba, srgba])
|
||||
}
|
||||
pub fn texture_gradient(left: Srgba, right: Srgba) -> Self {
|
||||
Self(vec![left, right])
|
||||
}
|
||||
pub fn ground_truth_linear_gradient(left: Srgba, right: Srgba) -> Self {
|
||||
let left = Rgba::from(left);
|
||||
let right = Rgba::from(right);
|
||||
|
||||
let n = 255;
|
||||
Self(
|
||||
(0..=n)
|
||||
.map(|i| {
|
||||
let t = i as f32 / n as f32;
|
||||
Srgba::from(lerp(left..=right, t))
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
/// This is how a bad person blends `sRGBA`
|
||||
pub fn ground_truth_bad_srgba_gradient(left: Srgba, right: Srgba) -> Self {
|
||||
let n = 255;
|
||||
Self(
|
||||
(0..=n)
|
||||
.map(|i| {
|
||||
let t = i as f32 / n as f32;
|
||||
Srgba::from_rgba_premultiplied(
|
||||
lerp((left[0] as f32)..=(right[0] as f32), t).round() as u8, // Don't ever do this please!
|
||||
lerp((left[1] as f32)..=(right[1] as f32), t).round() as u8, // Don't ever do this please!
|
||||
lerp((left[2] as f32)..=(right[2] as f32), t).round() as u8, // Don't ever do this please!
|
||||
lerp((left[3] as f32)..=(right[3] as f32), t).round() as u8, // Don't ever do this please!
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Do premultiplied alpha-aware blending of the gradient on top of the fill color
|
||||
pub fn with_bg_fill(self, bg: Srgba) -> Self {
|
||||
let bg = Rgba::from(bg);
|
||||
Self(
|
||||
self.0
|
||||
.into_iter()
|
||||
.map(|fg| {
|
||||
let fg = Rgba::from(fg);
|
||||
Srgba::from(bg * (1.0 - fg.a()) + fg)
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn to_pixel_row(&self) -> Vec<Srgba> {
|
||||
self.0.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct TextureManager(HashMap<Gradient, TextureId>);
|
||||
|
||||
impl TextureManager {
|
||||
fn get(
|
||||
&mut self,
|
||||
tex_allocator: &mut dyn epi::TextureAllocator,
|
||||
gradient: &Gradient,
|
||||
) -> TextureId {
|
||||
*self.0.entry(gradient.clone()).or_insert_with(|| {
|
||||
let pixels = gradient.to_pixel_row();
|
||||
let width = pixels.len();
|
||||
let height = 1;
|
||||
let id = tex_allocator.alloc();
|
||||
tex_allocator.set_srgba_premultiplied(id, (width, height), &pixels);
|
||||
id
|
||||
})
|
||||
}
|
||||
}
|
||||
68
egui_demo_lib/src/apps/demo/dancing_strings.rs
Normal file
68
egui_demo_lib/src/apps/demo/dancing_strings.rs
Normal file
@@ -0,0 +1,68 @@
|
||||
use egui::{containers::*, *};
|
||||
|
||||
#[derive(serde::Deserialize, serde::Serialize)]
|
||||
#[serde(default)]
|
||||
pub struct DancingStrings {}
|
||||
|
||||
impl Default for DancingStrings {
|
||||
fn default() -> Self {
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
|
||||
impl super::Demo for DancingStrings {
|
||||
fn name(&self) -> &str {
|
||||
"♫ Dancing Strings"
|
||||
}
|
||||
|
||||
fn show(&mut self, ctx: &CtxRef, open: &mut bool) {
|
||||
use super::View;
|
||||
Window::new(self.name())
|
||||
.open(open)
|
||||
.default_size(vec2(512.0, 256.0))
|
||||
.scroll(false)
|
||||
.show(ctx, |ui| self.ui(ui));
|
||||
}
|
||||
}
|
||||
|
||||
impl super::View for DancingStrings {
|
||||
fn ui(&mut self, ui: &mut Ui) {
|
||||
Frame::dark_canvas(ui.style()).show(ui, |ui| {
|
||||
ui.ctx().request_repaint();
|
||||
let time = ui.input().time;
|
||||
|
||||
let desired_size = ui.available_width() * vec2(1.0, 0.35);
|
||||
let (_id, rect) = ui.allocate_space(desired_size);
|
||||
|
||||
let mut cmds = vec![];
|
||||
|
||||
for &mode in &[2, 3, 5] {
|
||||
let mode = mode as f32;
|
||||
let n = 120;
|
||||
let speed = 1.5;
|
||||
|
||||
let points: Vec<Pos2> = (0..=n)
|
||||
.map(|i| {
|
||||
let t = i as f32 / (n as f32);
|
||||
let amp = (time as f32 * speed * mode).sin() / mode;
|
||||
let y = amp * (t * std::f32::consts::TAU / 2.0 * mode).sin();
|
||||
|
||||
pos2(
|
||||
lerp(rect.x_range(), t),
|
||||
remap(y, -1.0..=1.0, rect.y_range()),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let thickness = 10.0 / mode;
|
||||
cmds.push(paint::PaintCmd::line(
|
||||
points,
|
||||
Stroke::new(thickness, Srgba::additive_luminance(196)),
|
||||
));
|
||||
}
|
||||
|
||||
ui.painter().extend(cmds);
|
||||
});
|
||||
ui.add(crate::__egui_github_link_file!());
|
||||
}
|
||||
}
|
||||
414
egui_demo_lib/src/apps/demo/demo_window.rs
Normal file
414
egui_demo_lib/src/apps/demo/demo_window.rs
Normal file
@@ -0,0 +1,414 @@
|
||||
use super::*;
|
||||
use egui::{color::*, *};
|
||||
|
||||
/// Showcase some ui code
|
||||
#[derive(serde::Deserialize, serde::Serialize)]
|
||||
#[serde(default)]
|
||||
pub struct DemoWindow {
|
||||
num_columns: usize,
|
||||
|
||||
widgets: Widgets,
|
||||
scrolls: Scrolls,
|
||||
colors: ColorWidgets,
|
||||
layout: LayoutDemo,
|
||||
tree: Tree,
|
||||
box_painting: BoxPainting,
|
||||
}
|
||||
|
||||
impl Default for DemoWindow {
|
||||
fn default() -> DemoWindow {
|
||||
DemoWindow {
|
||||
num_columns: 2,
|
||||
|
||||
scrolls: Default::default(),
|
||||
widgets: Default::default(),
|
||||
colors: Default::default(),
|
||||
layout: Default::default(),
|
||||
tree: Tree::demo(),
|
||||
box_painting: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DemoWindow {
|
||||
pub fn ui(&mut self, ui: &mut Ui) {
|
||||
CollapsingHeader::new("Widgets")
|
||||
.default_open(true)
|
||||
.show(ui, |ui| {
|
||||
self.widgets.ui(ui);
|
||||
});
|
||||
|
||||
CollapsingHeader::new("Colors")
|
||||
.default_open(false)
|
||||
.show(ui, |ui| {
|
||||
self.colors.ui(ui);
|
||||
});
|
||||
|
||||
CollapsingHeader::new("Layout")
|
||||
.default_open(false)
|
||||
.show(ui, |ui| self.layout.ui(ui));
|
||||
|
||||
CollapsingHeader::new("Tree")
|
||||
.default_open(false)
|
||||
.show(ui, |ui| self.tree.ui(ui));
|
||||
|
||||
ui.collapsing("Columns", |ui| {
|
||||
ui.add(Slider::usize(&mut self.num_columns, 1..=10).text("Columns"));
|
||||
ui.columns(self.num_columns, |cols| {
|
||||
for (i, col) in cols.iter_mut().enumerate() {
|
||||
col.label(format!("Column {} out of {}", i + 1, self.num_columns));
|
||||
if i + 1 == self.num_columns && col.button("Delete this").clicked {
|
||||
self.num_columns -= 1;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
CollapsingHeader::new("Test box rendering")
|
||||
.default_open(false)
|
||||
.show(ui, |ui| self.box_painting.ui(ui));
|
||||
|
||||
CollapsingHeader::new("Scroll area")
|
||||
.default_open(false)
|
||||
.show(ui, |ui| {
|
||||
self.scrolls.ui(ui);
|
||||
});
|
||||
|
||||
CollapsingHeader::new("Resize")
|
||||
.default_open(false)
|
||||
.show(ui, |ui| {
|
||||
Resize::default().default_height(100.0).show(ui, |ui| {
|
||||
ui.label("This ui can be resized!");
|
||||
ui.label("Just pull the handle on the bottom right");
|
||||
});
|
||||
});
|
||||
|
||||
CollapsingHeader::new("Misc")
|
||||
.default_open(false)
|
||||
.show(ui, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("You can pretty easily paint your own small icons:");
|
||||
use std::f32::consts::TAU;
|
||||
let response = ui.allocate_response(Vec2::splat(16.0), Sense::hover());
|
||||
let painter = ui.painter();
|
||||
let c = response.rect.center();
|
||||
let r = response.rect.width() / 2.0 - 1.0;
|
||||
let color = Srgba::gray(128);
|
||||
let stroke = Stroke::new(1.0, color);
|
||||
painter.circle_stroke(c, r, stroke);
|
||||
painter.line_segment([c - vec2(0.0, r), c + vec2(0.0, r)], stroke);
|
||||
painter.line_segment([c, c + r * Vec2::angled(TAU * 1.0 / 8.0)], stroke);
|
||||
painter.line_segment([c, c + r * Vec2::angled(TAU * 3.0 / 8.0)], stroke);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[derive(serde::Deserialize, serde::Serialize)]
|
||||
#[serde(default)]
|
||||
struct ColorWidgets {
|
||||
srgba_unmul: [u8; 4],
|
||||
srgba_premul: [u8; 4],
|
||||
rgba_unmul: [f32; 4],
|
||||
rgba_premul: [f32; 4],
|
||||
}
|
||||
|
||||
impl Default for ColorWidgets {
|
||||
fn default() -> Self {
|
||||
// Approximately the same color.
|
||||
ColorWidgets {
|
||||
srgba_unmul: [0, 255, 183, 127],
|
||||
srgba_premul: [0, 187, 140, 127],
|
||||
rgba_unmul: [0.0, 1.0, 0.5, 0.5],
|
||||
rgba_premul: [0.0, 0.5, 0.25, 0.5],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ColorWidgets {
|
||||
fn ui(&mut self, ui: &mut Ui) {
|
||||
if ui.button("Reset").clicked {
|
||||
*self = Default::default();
|
||||
}
|
||||
|
||||
ui.label("Egui lets you edit colors stored as either sRGBA or linear RGBA and with or without premultiplied alpha");
|
||||
|
||||
let Self {
|
||||
srgba_unmul,
|
||||
srgba_premul,
|
||||
rgba_unmul,
|
||||
rgba_premul,
|
||||
} = self;
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.color_edit_button_srgba_unmultiplied(srgba_unmul);
|
||||
ui.label(format!(
|
||||
"sRGBA: {} {} {} {}",
|
||||
srgba_unmul[0], srgba_unmul[1], srgba_unmul[2], srgba_unmul[3],
|
||||
));
|
||||
});
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.color_edit_button_srgba_premultiplied(srgba_premul);
|
||||
ui.label(format!(
|
||||
"sRGBA with premultiplied alpha: {} {} {} {}",
|
||||
srgba_premul[0], srgba_premul[1], srgba_premul[2], srgba_premul[3],
|
||||
));
|
||||
});
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.color_edit_button_rgba_unmultiplied(rgba_unmul);
|
||||
ui.label(format!(
|
||||
"Linear RGBA: {:.02} {:.02} {:.02} {:.02}",
|
||||
rgba_unmul[0], rgba_unmul[1], rgba_unmul[2], rgba_unmul[3],
|
||||
));
|
||||
});
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.color_edit_button_rgba_premultiplied(rgba_premul);
|
||||
ui.label(format!(
|
||||
"Linear RGBA with premultiplied alpha: {:.02} {:.02} {:.02} {:.02}",
|
||||
rgba_premul[0], rgba_premul[1], rgba_premul[2], rgba_premul[3],
|
||||
));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[derive(serde::Deserialize, serde::Serialize)]
|
||||
#[serde(default)]
|
||||
struct BoxPainting {
|
||||
size: Vec2,
|
||||
corner_radius: f32,
|
||||
stroke_width: f32,
|
||||
num_boxes: usize,
|
||||
}
|
||||
|
||||
impl Default for BoxPainting {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
size: vec2(64.0, 32.0),
|
||||
corner_radius: 5.0,
|
||||
stroke_width: 2.0,
|
||||
num_boxes: 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BoxPainting {
|
||||
pub fn ui(&mut self, ui: &mut Ui) {
|
||||
ui.add(Slider::f32(&mut self.size.x, 0.0..=500.0).text("width"));
|
||||
ui.add(Slider::f32(&mut self.size.y, 0.0..=500.0).text("height"));
|
||||
ui.add(Slider::f32(&mut self.corner_radius, 0.0..=50.0).text("corner_radius"));
|
||||
ui.add(Slider::f32(&mut self.stroke_width, 0.0..=10.0).text("stroke_width"));
|
||||
ui.add(Slider::usize(&mut self.num_boxes, 0..=8).text("num_boxes"));
|
||||
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
for _ in 0..self.num_boxes {
|
||||
let response = ui.allocate_response(self.size, Sense::hover());
|
||||
ui.painter().rect(
|
||||
response.rect,
|
||||
self.corner_radius,
|
||||
Srgba::gray(64),
|
||||
Stroke::new(self.stroke_width, WHITE),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[derive(serde::Deserialize, serde::Serialize)]
|
||||
#[serde(default)]
|
||||
struct LayoutDemo {
|
||||
// Identical to contents of `egui::Layout`
|
||||
main_dir: Direction,
|
||||
main_wrap: bool,
|
||||
cross_align: Align,
|
||||
cross_justify: bool,
|
||||
|
||||
// Extra for testing wrapping:
|
||||
wrap_column_width: f32,
|
||||
wrap_row_height: f32,
|
||||
}
|
||||
|
||||
impl Default for LayoutDemo {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
main_dir: Direction::TopDown,
|
||||
main_wrap: false,
|
||||
cross_align: Align::Min,
|
||||
cross_justify: false,
|
||||
wrap_column_width: 150.0,
|
||||
wrap_row_height: 20.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LayoutDemo {
|
||||
fn layout(&self) -> Layout {
|
||||
Layout::from_main_dir_and_cross_align(self.main_dir, self.cross_align)
|
||||
.with_main_wrap(self.main_wrap)
|
||||
.with_cross_justify(self.cross_justify)
|
||||
}
|
||||
|
||||
pub fn ui(&mut self, ui: &mut Ui) {
|
||||
self.content_ui(ui);
|
||||
Resize::default()
|
||||
.default_size([300.0, 200.0])
|
||||
.show(ui, |ui| {
|
||||
if self.main_wrap {
|
||||
if self.main_dir.is_horizontal() {
|
||||
ui.allocate_ui(
|
||||
vec2(
|
||||
ui.available_size_before_wrap_finite().x,
|
||||
self.wrap_row_height,
|
||||
),
|
||||
|ui| ui.with_layout(self.layout(), |ui| self.demo_ui(ui)),
|
||||
);
|
||||
} else {
|
||||
ui.allocate_ui(
|
||||
vec2(
|
||||
self.wrap_column_width,
|
||||
ui.available_size_before_wrap_finite().y,
|
||||
),
|
||||
|ui| ui.with_layout(self.layout(), |ui| self.demo_ui(ui)),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
ui.with_layout(self.layout(), |ui| self.demo_ui(ui));
|
||||
}
|
||||
});
|
||||
ui.label("Resize to see effect");
|
||||
}
|
||||
|
||||
pub fn content_ui(&mut self, ui: &mut Ui) {
|
||||
ui.horizontal(|ui| {
|
||||
if ui.button("Top-down").clicked {
|
||||
*self = Default::default();
|
||||
}
|
||||
if ui.button("Top-down, centered and justified").clicked {
|
||||
*self = Default::default();
|
||||
self.cross_align = Align::Center;
|
||||
self.cross_justify = true;
|
||||
}
|
||||
if ui.button("Horizontal wrapped").clicked {
|
||||
*self = Default::default();
|
||||
self.main_dir = Direction::LeftToRight;
|
||||
self.cross_align = Align::Center;
|
||||
self.main_wrap = true;
|
||||
}
|
||||
});
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Main Direction:");
|
||||
for &dir in &[
|
||||
Direction::LeftToRight,
|
||||
Direction::RightToLeft,
|
||||
Direction::TopDown,
|
||||
Direction::BottomUp,
|
||||
] {
|
||||
ui.radio_value(&mut self.main_dir, dir, format!("{:?}", dir));
|
||||
}
|
||||
});
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.checkbox(&mut self.main_wrap, "Main wrap")
|
||||
.on_hover_text("Wrap when next widget doesn't fit the current row/column");
|
||||
|
||||
if self.main_wrap {
|
||||
if self.main_dir.is_horizontal() {
|
||||
ui.add(Slider::f32(&mut self.wrap_row_height, 0.0..=200.0).text("Row height"));
|
||||
} else {
|
||||
ui.add(
|
||||
Slider::f32(&mut self.wrap_column_width, 0.0..=200.0).text("Column width"),
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Cross Align:");
|
||||
for &align in &[Align::Min, Align::Center, Align::Max] {
|
||||
ui.radio_value(&mut self.cross_align, align, format!("{:?}", align));
|
||||
}
|
||||
});
|
||||
|
||||
ui.checkbox(&mut self.cross_justify, "Cross Justified")
|
||||
.on_hover_text("Try to fill full width/height (e.g. buttons)");
|
||||
}
|
||||
|
||||
pub fn demo_ui(&mut self, ui: &mut Ui) {
|
||||
ui.monospace("Example widgets:");
|
||||
for _ in 0..3 {
|
||||
ui.label("label");
|
||||
}
|
||||
for _ in 0..3 {
|
||||
let mut dummy = false;
|
||||
ui.checkbox(&mut dummy, "checkbox");
|
||||
}
|
||||
for _ in 0..3 {
|
||||
let _ = ui.button("button");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
enum Action {
|
||||
Keep,
|
||||
Delete,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, serde::Deserialize, serde::Serialize)]
|
||||
struct Tree(Vec<Tree>);
|
||||
|
||||
impl Tree {
|
||||
pub fn demo() -> Self {
|
||||
Self(vec![
|
||||
Tree(vec![Tree::default(); 4]),
|
||||
Tree(vec![Tree(vec![Tree::default(); 2]); 3]),
|
||||
])
|
||||
}
|
||||
pub fn ui(&mut self, ui: &mut Ui) -> Action {
|
||||
self.ui_impl(ui, 0, "root")
|
||||
}
|
||||
|
||||
fn ui_impl(&mut self, ui: &mut Ui, depth: usize, name: &str) -> Action {
|
||||
CollapsingHeader::new(name)
|
||||
.default_open(depth < 1)
|
||||
.show(ui, |ui| self.children_ui(ui, depth))
|
||||
.body_returned
|
||||
.unwrap_or(Action::Keep)
|
||||
}
|
||||
|
||||
fn children_ui(&mut self, ui: &mut Ui, depth: usize) -> Action {
|
||||
if depth > 0 && ui.add(Button::new("delete").text_color(color::RED)).clicked {
|
||||
return Action::Delete;
|
||||
}
|
||||
|
||||
self.0 = std::mem::take(self)
|
||||
.0
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.filter_map(|(i, mut tree)| {
|
||||
if tree.ui_impl(ui, depth + 1, &format!("child #{}", i)) == Action::Keep {
|
||||
Some(tree)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
if ui.button("+").clicked {
|
||||
self.0.push(Tree::default());
|
||||
}
|
||||
|
||||
Action::Keep
|
||||
}
|
||||
}
|
||||
360
egui_demo_lib/src/apps/demo/demo_windows.rs
Normal file
360
egui_demo_lib/src/apps/demo/demo_windows.rs
Normal file
@@ -0,0 +1,360 @@
|
||||
use egui::{CtxRef, Resize, ScrollArea, Ui, Window};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// Link to show a specific part of the demo app.
|
||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||
pub enum DemoLink {
|
||||
Clock,
|
||||
}
|
||||
|
||||
/// Special input to the demo-app.
|
||||
#[derive(Default)]
|
||||
pub struct DemoEnvironment {
|
||||
/// Local time. Used for the clock in the demo app.
|
||||
pub seconds_since_midnight: Option<f64>,
|
||||
|
||||
/// Set to `Some` to open a specific part of the demo app.
|
||||
pub link: Option<DemoLink>,
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[derive(serde::Deserialize, serde::Serialize)]
|
||||
#[serde(default)]
|
||||
struct Demos {
|
||||
/// open, view
|
||||
#[serde(skip)] // TODO: serialize the `open` state.
|
||||
demos: Vec<(bool, Box<dyn super::Demo>)>,
|
||||
}
|
||||
impl Default for Demos {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
demos: vec![
|
||||
(false, Box::new(super::FontBook::default())),
|
||||
(false, Box::new(super::Painting::default())),
|
||||
(false, Box::new(super::DancingStrings::default())),
|
||||
(false, Box::new(super::DragAndDropDemo::default())),
|
||||
(false, Box::new(super::Tests::default())),
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Demos {
|
||||
pub fn checkboxes(&mut self, ui: &mut Ui) {
|
||||
for (ref mut open, demo) in &mut self.demos {
|
||||
ui.checkbox(open, demo.name());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn show(&mut self, ctx: &CtxRef) {
|
||||
for (ref mut open, demo) in &mut self.demos {
|
||||
demo.show(ctx, open);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// A menu bar in which you can select different demo windows to show.
|
||||
#[derive(Default, serde::Deserialize, serde::Serialize)]
|
||||
#[serde(default)]
|
||||
pub struct DemoWindows {
|
||||
open_windows: OpenWindows,
|
||||
|
||||
demo_window: super::DemoWindow,
|
||||
|
||||
#[serde(skip)]
|
||||
color_test: super::ColorTest,
|
||||
|
||||
fractal_clock: super::FractalClock,
|
||||
|
||||
/// open, title, view
|
||||
demos: Demos,
|
||||
|
||||
#[serde(skip)]
|
||||
previous_link: Option<DemoLink>,
|
||||
}
|
||||
|
||||
impl DemoWindows {
|
||||
/// Show the app ui (menu bar and windows).
|
||||
/// `sidebar_ui` can be used to optionally show some things in the sidebar
|
||||
pub fn ui(
|
||||
&mut self,
|
||||
ctx: &CtxRef,
|
||||
env: &DemoEnvironment,
|
||||
tex_allocator: &mut Option<&mut dyn epi::TextureAllocator>,
|
||||
sidebar_ui: impl FnOnce(&mut Ui),
|
||||
) {
|
||||
if self.previous_link != env.link {
|
||||
match env.link {
|
||||
None => {}
|
||||
Some(DemoLink::Clock) => {
|
||||
self.open_windows = OpenWindows {
|
||||
fractal_clock: true,
|
||||
..OpenWindows::none()
|
||||
};
|
||||
}
|
||||
}
|
||||
self.previous_link = env.link;
|
||||
}
|
||||
|
||||
egui::SidePanel::left("side_panel", 190.0).show(ctx, |ui| {
|
||||
ui.heading("✒ Egui Demo");
|
||||
egui::warn_if_debug_build(ui);
|
||||
|
||||
ui.separator();
|
||||
|
||||
ScrollArea::auto_sized().show(ui, |ui| {
|
||||
ui.label("Egui is an immediate mode GUI library written in Rust.");
|
||||
ui.add(
|
||||
egui::Hyperlink::new("https://github.com/emilk/egui").text(" Egui home page"),
|
||||
);
|
||||
|
||||
ui.label("Egui can be run on the web, or natively on 🐧");
|
||||
|
||||
ui.separator();
|
||||
|
||||
ui.heading("Windows:");
|
||||
ui.indent("windows", |ui| {
|
||||
self.open_windows.checkboxes(ui);
|
||||
self.demos.checkboxes(ui);
|
||||
});
|
||||
|
||||
ui.separator();
|
||||
|
||||
if ui.button("Organize windows").clicked {
|
||||
ui.ctx().memory().reset_areas();
|
||||
}
|
||||
|
||||
sidebar_ui(ui);
|
||||
});
|
||||
});
|
||||
|
||||
egui::TopPanel::top("menu_bar").show(ctx, |ui| {
|
||||
show_menu_bar(ui, &mut self.open_windows, env.seconds_since_midnight);
|
||||
});
|
||||
|
||||
self.windows(ctx, env, tex_allocator);
|
||||
}
|
||||
|
||||
/// Show the open windows.
|
||||
fn windows(
|
||||
&mut self,
|
||||
ctx: &CtxRef,
|
||||
env: &DemoEnvironment,
|
||||
tex_allocator: &mut Option<&mut dyn epi::TextureAllocator>,
|
||||
) {
|
||||
let Self {
|
||||
open_windows,
|
||||
demo_window,
|
||||
color_test,
|
||||
fractal_clock,
|
||||
demos,
|
||||
..
|
||||
} = self;
|
||||
|
||||
Window::new("✨ Demo")
|
||||
.open(&mut open_windows.demo)
|
||||
.scroll(true)
|
||||
.show(ctx, |ui| {
|
||||
demo_window.ui(ui);
|
||||
});
|
||||
|
||||
Window::new("🔧 Settings")
|
||||
.open(&mut open_windows.settings)
|
||||
.show(ctx, |ui| {
|
||||
ctx.settings_ui(ui);
|
||||
});
|
||||
|
||||
Window::new("🔍 Inspection")
|
||||
.open(&mut open_windows.inspection)
|
||||
.scroll(true)
|
||||
.show(ctx, |ui| {
|
||||
ctx.inspection_ui(ui);
|
||||
});
|
||||
|
||||
Window::new("📝 Memory")
|
||||
.open(&mut open_windows.memory)
|
||||
.resizable(false)
|
||||
.show(ctx, |ui| {
|
||||
ctx.memory_ui(ui);
|
||||
});
|
||||
|
||||
Window::new("🎨 Color Test")
|
||||
.default_size([800.0, 1024.0])
|
||||
.scroll(true)
|
||||
.open(&mut open_windows.color_test)
|
||||
.show(ctx, |ui| {
|
||||
color_test.ui(ui, tex_allocator);
|
||||
});
|
||||
|
||||
demos.show(ctx);
|
||||
|
||||
fractal_clock.window(
|
||||
ctx,
|
||||
&mut open_windows.fractal_clock,
|
||||
env.seconds_since_midnight,
|
||||
);
|
||||
|
||||
self.resize_windows(ctx);
|
||||
}
|
||||
|
||||
fn resize_windows(&mut self, ctx: &CtxRef) {
|
||||
let open = &mut self.open_windows.resize;
|
||||
|
||||
Window::new("resizable")
|
||||
.open(open)
|
||||
.scroll(false)
|
||||
.resizable(true)
|
||||
.show(ctx, |ui| {
|
||||
ui.label("scroll: NO");
|
||||
ui.label("resizable: YES");
|
||||
ui.label(crate::LOREM_IPSUM);
|
||||
});
|
||||
|
||||
Window::new("resizable + embedded scroll")
|
||||
.open(open)
|
||||
.scroll(false)
|
||||
.resizable(true)
|
||||
.default_height(300.0)
|
||||
.show(ctx, |ui| {
|
||||
ui.label("scroll: NO");
|
||||
ui.label("resizable: YES");
|
||||
ui.heading("We have a sub-region with scroll bar:");
|
||||
ScrollArea::auto_sized().show(ui, |ui| {
|
||||
ui.label(crate::LOREM_IPSUM_LONG);
|
||||
ui.label(crate::LOREM_IPSUM_LONG);
|
||||
});
|
||||
// ui.heading("Some additional text here, that should also be visible"); // this works, but messes with the resizing a bit
|
||||
});
|
||||
|
||||
Window::new("resizable + scroll")
|
||||
.open(open)
|
||||
.scroll(true)
|
||||
.resizable(true)
|
||||
.default_height(300.0)
|
||||
.show(ctx, |ui| {
|
||||
ui.label("scroll: YES");
|
||||
ui.label("resizable: YES");
|
||||
ui.label(crate::LOREM_IPSUM_LONG);
|
||||
});
|
||||
|
||||
Window::new("auto_sized")
|
||||
.open(open)
|
||||
.auto_sized()
|
||||
.show(ctx, |ui| {
|
||||
ui.label("This window will auto-size based on its contents.");
|
||||
ui.heading("Resize this area:");
|
||||
Resize::default().show(ui, |ui| {
|
||||
ui.label(crate::LOREM_IPSUM);
|
||||
});
|
||||
ui.heading("Resize the above area!");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[derive(serde::Deserialize, serde::Serialize)]
|
||||
struct OpenWindows {
|
||||
demo: bool,
|
||||
fractal_clock: bool,
|
||||
|
||||
// egui stuff:
|
||||
settings: bool,
|
||||
inspection: bool,
|
||||
memory: bool,
|
||||
resize: bool,
|
||||
|
||||
// debug stuff:
|
||||
color_test: bool,
|
||||
}
|
||||
|
||||
impl Default for OpenWindows {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
demo: true,
|
||||
..OpenWindows::none()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl OpenWindows {
|
||||
fn none() -> Self {
|
||||
Self {
|
||||
demo: false,
|
||||
fractal_clock: false,
|
||||
|
||||
settings: false,
|
||||
inspection: false,
|
||||
memory: false,
|
||||
resize: false,
|
||||
|
||||
color_test: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn checkboxes(&mut self, ui: &mut Ui) {
|
||||
let Self {
|
||||
demo,
|
||||
fractal_clock,
|
||||
settings,
|
||||
inspection,
|
||||
memory,
|
||||
resize,
|
||||
color_test,
|
||||
} = self;
|
||||
ui.label("Egui:");
|
||||
ui.checkbox(settings, "🔧 Settings");
|
||||
ui.checkbox(inspection, "🔍 Inspection");
|
||||
ui.checkbox(memory, "📝 Memory");
|
||||
ui.separator();
|
||||
ui.checkbox(demo, "✨ Demo");
|
||||
ui.separator();
|
||||
ui.checkbox(resize, "↔ Resize examples");
|
||||
ui.checkbox(color_test, "🎨 Color test")
|
||||
.on_hover_text("For testing the integrations painter");
|
||||
ui.separator();
|
||||
ui.label("Misc:");
|
||||
ui.checkbox(fractal_clock, "🕑 Fractal Clock");
|
||||
}
|
||||
}
|
||||
|
||||
fn show_menu_bar(ui: &mut Ui, windows: &mut OpenWindows, seconds_since_midnight: Option<f64>) {
|
||||
use egui::*;
|
||||
|
||||
menu::bar(ui, |ui| {
|
||||
menu::menu(ui, "File", |ui| {
|
||||
if ui.button("Organize windows").clicked {
|
||||
ui.ctx().memory().reset_areas();
|
||||
}
|
||||
if ui
|
||||
.button("Clear Egui memory")
|
||||
.on_hover_text("Forget scroll, collapsing headers etc")
|
||||
.clicked
|
||||
{
|
||||
*ui.ctx().memory() = Default::default();
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(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.with_layout(Layout::right_to_left(), |ui| {
|
||||
if ui
|
||||
.add(Button::new(time).text_style(TextStyle::Monospace))
|
||||
.clicked
|
||||
{
|
||||
windows.fractal_clock = !windows.fractal_clock;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
156
egui_demo_lib/src/apps/demo/drag_and_drop.rs
Normal file
156
egui_demo_lib/src/apps/demo/drag_and_drop.rs
Normal file
@@ -0,0 +1,156 @@
|
||||
use egui::*;
|
||||
|
||||
pub fn drag_source(ui: &mut Ui, id: Id, body: impl FnOnce(&mut Ui)) {
|
||||
let is_being_dragged = ui.memory().is_being_dragged(id);
|
||||
|
||||
if !is_being_dragged {
|
||||
let response = ui.wrap(body).1;
|
||||
|
||||
// Check for drags:
|
||||
let response = ui.interact(response.rect, id, Sense::drag());
|
||||
if response.hovered {
|
||||
ui.output().cursor_icon = CursorIcon::Grab;
|
||||
}
|
||||
} else {
|
||||
ui.output().cursor_icon = CursorIcon::Grabbing;
|
||||
|
||||
// Paint the body to a new layer:
|
||||
let layer_id = LayerId::new(Order::Tooltip, id);
|
||||
let response = ui.with_layer_id(layer_id, body).1;
|
||||
|
||||
// Now we move the visuals of the body to where the mouse is.
|
||||
// Normally you need to decide a location for a widget first,
|
||||
// because otherwise that widget cannot interact with the mouse.
|
||||
// However, a dragged component cannot be interacted with anyway
|
||||
// (anything with `Order::Tooltip` always gets an empty `Response`)
|
||||
// So this is fine!
|
||||
|
||||
if let Some(mouse_pos) = ui.input().mouse.pos {
|
||||
let delta = mouse_pos - response.rect.center();
|
||||
ui.ctx().translate_layer(layer_id, delta);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn drop_target<R>(
|
||||
ui: &mut Ui,
|
||||
can_accept_what_is_being_dragged: bool,
|
||||
body: impl FnOnce(&mut Ui) -> R,
|
||||
) -> (R, Response) {
|
||||
let is_being_dragged = ui.memory().is_anything_being_dragged();
|
||||
|
||||
let margin = Vec2::splat(4.0);
|
||||
|
||||
let outer_rect_bounds = ui.available_rect_before_wrap();
|
||||
let inner_rect = outer_rect_bounds.shrink2(margin);
|
||||
let where_to_put_background = ui.painter().add(PaintCmd::Noop);
|
||||
let mut content_ui = ui.child_ui(inner_rect, *ui.layout());
|
||||
let ret = body(&mut content_ui);
|
||||
let outer_rect = Rect::from_min_max(outer_rect_bounds.min, content_ui.min_rect().max + margin);
|
||||
let response = ui.allocate_response(outer_rect.size(), Sense::hover());
|
||||
|
||||
let style = if is_being_dragged && can_accept_what_is_being_dragged && response.hovered {
|
||||
ui.style().visuals.widgets.active
|
||||
} else if is_being_dragged && can_accept_what_is_being_dragged {
|
||||
ui.style().visuals.widgets.inactive
|
||||
} else if is_being_dragged && !can_accept_what_is_being_dragged {
|
||||
ui.style().visuals.widgets.disabled
|
||||
} else {
|
||||
ui.style().visuals.widgets.inactive
|
||||
};
|
||||
|
||||
ui.painter().set(
|
||||
where_to_put_background,
|
||||
PaintCmd::Rect {
|
||||
corner_radius: style.corner_radius,
|
||||
fill: style.bg_fill,
|
||||
stroke: style.bg_stroke,
|
||||
rect: response.rect,
|
||||
},
|
||||
);
|
||||
|
||||
(ret, response)
|
||||
}
|
||||
|
||||
pub struct DragAndDropDemo {
|
||||
/// columns with items
|
||||
columns: Vec<Vec<&'static str>>,
|
||||
}
|
||||
|
||||
impl Default for DragAndDropDemo {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
columns: vec![
|
||||
vec!["Item A", "Item B", "Item C"],
|
||||
vec!["Item D", "Item E"],
|
||||
vec!["Item F", "Item G", "Item H"],
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl super::Demo for DragAndDropDemo {
|
||||
fn name(&self) -> &str {
|
||||
"✋ Drag and Drop"
|
||||
}
|
||||
|
||||
fn show(&mut self, ctx: &CtxRef, open: &mut bool) {
|
||||
use super::View;
|
||||
Window::new(self.name())
|
||||
.open(open)
|
||||
.default_size(vec2(256.0, 256.0))
|
||||
.scroll(false)
|
||||
.resizable(false)
|
||||
.show(ctx, |ui| self.ui(ui));
|
||||
}
|
||||
}
|
||||
|
||||
impl super::View for DragAndDropDemo {
|
||||
fn ui(&mut self, ui: &mut Ui) {
|
||||
ui.label("This is a proof-of-concept of drag-and-drop in Egui");
|
||||
ui.label("Drag items between columns.");
|
||||
|
||||
let mut source_col_row = None;
|
||||
let mut drop_col = None;
|
||||
|
||||
ui.columns(self.columns.len(), |uis| {
|
||||
for (col_idx, column) in self.columns.iter().enumerate() {
|
||||
let ui = &mut uis[col_idx];
|
||||
let can_accept_what_is_being_dragged = true; // We accept anything being dragged (for now) ¯\_(ツ)_/¯
|
||||
let response = drop_target(ui, can_accept_what_is_being_dragged, |ui| {
|
||||
ui.set_min_size(vec2(64.0, 100.0));
|
||||
|
||||
for (row_idx, &item) in column.iter().enumerate() {
|
||||
let item_id = Id::new("item").with(col_idx).with(row_idx);
|
||||
drag_source(ui, item_id, |ui| {
|
||||
ui.label(item);
|
||||
});
|
||||
|
||||
let this_item_being_dragged = ui.memory().is_being_dragged(item_id);
|
||||
if this_item_being_dragged {
|
||||
source_col_row = Some((col_idx, row_idx));
|
||||
}
|
||||
}
|
||||
})
|
||||
.1;
|
||||
|
||||
let is_being_dragged = ui.memory().is_anything_being_dragged();
|
||||
if is_being_dragged && can_accept_what_is_being_dragged && response.hovered {
|
||||
drop_col = Some(col_idx);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if let Some((source_col, source_row)) = source_col_row {
|
||||
if let Some(drop_col) = drop_col {
|
||||
if ui.input().mouse.released {
|
||||
// do the drop:
|
||||
let item = self.columns[source_col].remove(source_row);
|
||||
self.columns[drop_col].push(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ui.add(crate::__egui_github_link_file!());
|
||||
}
|
||||
}
|
||||
104
egui_demo_lib/src/apps/demo/font_book.rs
Normal file
104
egui_demo_lib/src/apps/demo/font_book.rs
Normal file
@@ -0,0 +1,104 @@
|
||||
pub struct FontBook {
|
||||
standard: bool,
|
||||
emojis: bool,
|
||||
filter: String,
|
||||
text_style: egui::TextStyle,
|
||||
}
|
||||
|
||||
impl Default for FontBook {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
standard: false,
|
||||
emojis: true,
|
||||
filter: Default::default(),
|
||||
text_style: egui::TextStyle::Button,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FontBook {
|
||||
fn characters_ui(&self, ui: &mut egui::Ui, characters: &[(u32, char, &str)]) {
|
||||
use egui::{Button, Label};
|
||||
for &(_, chr, name) in characters {
|
||||
if self.filter.is_empty()
|
||||
|| name.contains(&self.filter)
|
||||
|| self.filter == chr.to_string()
|
||||
{
|
||||
let button = Button::new(chr).text_style(self.text_style).frame(false);
|
||||
|
||||
let tooltip_ui = |ui: &mut egui::Ui| {
|
||||
ui.add(Label::new(chr).text_style(self.text_style));
|
||||
ui.label(format!("{}\nU+{:X}\n\nClick to copy", name, chr as u32));
|
||||
};
|
||||
|
||||
if ui.add(button).on_hover_ui(tooltip_ui).clicked {
|
||||
ui.output().copied_text = chr.to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl super::Demo for FontBook {
|
||||
fn name(&self) -> &str {
|
||||
"🔤 Font Book"
|
||||
}
|
||||
|
||||
fn show(&mut self, ctx: &egui::CtxRef, open: &mut bool) {
|
||||
egui::Window::new(self.name()).open(open).show(ctx, |ui| {
|
||||
use super::View;
|
||||
self.ui(ui);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl super::View for FontBook {
|
||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
use super::font_contents_emoji::FULL_EMOJI_LIST;
|
||||
use super::font_contents_ubuntu::UBUNTU_FONT_CHARACTERS;
|
||||
|
||||
ui.label(format!(
|
||||
"Egui supports {} standard characters and {} emojis.\nClick on a character to copy it.",
|
||||
UBUNTU_FONT_CHARACTERS.len(),
|
||||
FULL_EMOJI_LIST.len(),
|
||||
));
|
||||
|
||||
ui.separator();
|
||||
|
||||
egui::combo_box_with_label(ui, "Text style", format!("{:?}", self.text_style), |ui| {
|
||||
for style in egui::TextStyle::all() {
|
||||
ui.selectable_value(&mut self.text_style, style, format!("{:?}", style));
|
||||
}
|
||||
});
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Show:");
|
||||
ui.checkbox(&mut self.standard, "Standard");
|
||||
ui.checkbox(&mut self.emojis, "Emojis");
|
||||
});
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Filter:");
|
||||
ui.text_edit_singleline(&mut self.filter);
|
||||
self.filter = self.filter.to_lowercase();
|
||||
if ui.button("x").clicked {
|
||||
self.filter.clear();
|
||||
}
|
||||
});
|
||||
|
||||
ui.separator();
|
||||
|
||||
egui::ScrollArea::auto_sized().show(ui, |ui| {
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
ui.style_mut().spacing.item_spacing = egui::Vec2::splat(2.0);
|
||||
|
||||
if self.standard {
|
||||
self.characters_ui(ui, UBUNTU_FONT_CHARACTERS);
|
||||
}
|
||||
if self.emojis {
|
||||
self.characters_ui(ui, FULL_EMOJI_LIST);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
2756
egui_demo_lib/src/apps/demo/font_contents_emoji.rs
Normal file
2756
egui_demo_lib/src/apps/demo/font_contents_emoji.rs
Normal file
File diff suppressed because it is too large
Load Diff
1191
egui_demo_lib/src/apps/demo/font_contents_ubuntu.rs
Normal file
1191
egui_demo_lib/src/apps/demo/font_contents_ubuntu.rs
Normal file
File diff suppressed because it is too large
Load Diff
202
egui_demo_lib/src/apps/demo/fractal_clock.rs
Normal file
202
egui_demo_lib/src/apps/demo/fractal_clock.rs
Normal file
@@ -0,0 +1,202 @@
|
||||
use egui::{containers::*, widgets::*, *};
|
||||
use std::f32::consts::TAU;
|
||||
|
||||
#[derive(serde::Deserialize, serde::Serialize)]
|
||||
#[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,
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FractalClock {
|
||||
pub fn window(&mut self, ctx: &CtxRef, open: &mut bool, seconds_since_midnight: Option<f64>) {
|
||||
Window::new("🕑 Fractal Clock")
|
||||
.open(open)
|
||||
.default_size(vec2(512.0, 512.0))
|
||||
.scroll(false)
|
||||
// Dark background frame to make it pop:
|
||||
.frame(Frame::window(&ctx.style()).fill(Srgba::black_alpha(250)))
|
||||
.show(ctx, |ui| self.ui(ui, seconds_since_midnight));
|
||||
}
|
||||
|
||||
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().time);
|
||||
ui.ctx().request_repaint();
|
||||
}
|
||||
|
||||
let painter = Painter::new(
|
||||
ui.ctx().clone(),
|
||||
ui.layer_id(),
|
||||
ui.available_rect_before_wrap_finite(),
|
||||
);
|
||||
self.paint(&painter);
|
||||
// Make sure we allocate what we used (everything)
|
||||
ui.expand_to_include_rect(painter.clip_rect());
|
||||
|
||||
Frame::popup(ui.style())
|
||||
.fill(Rgba::luminance_alpha(0.02, 0.5).into())
|
||||
.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.checkbox(&mut self.paused, "Paused");
|
||||
ui.add(Slider::f32(&mut self.zoom, 0.0..=1.0).text("zoom"));
|
||||
ui.add(Slider::f32(&mut self.start_line_width, 0.0..=5.0).text("Start line width"));
|
||||
ui.add(Slider::usize(&mut self.depth, 0..=14).text("depth"));
|
||||
ui.add(Slider::f32(&mut self.length_factor, 0.0..=1.0).text("length factor"));
|
||||
ui.add(Slider::f32(&mut self.luminance_factor, 0.0..=1.0).text("luminance factor"));
|
||||
ui.add(Slider::f32(&mut self.width_factor, 0.0..=1.0).text("width factor"));
|
||||
if ui.button("Reset").clicked {
|
||||
*self = Default::default();
|
||||
}
|
||||
|
||||
ui.add(
|
||||
Hyperlink::new("http://www.dqd.com/~mayoff/programs/FractalClock/")
|
||||
.text("Inspired by a screensaver by Rob Mayoff"),
|
||||
);
|
||||
}
|
||||
|
||||
fn paint(&mut self, painter: &Painter) {
|
||||
let rect = painter.clip_rect();
|
||||
|
||||
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 scale = self.zoom * rect.width().min(rect.height());
|
||||
let paint_line = |points: [Pos2; 2], color: Srgba, width: f32| {
|
||||
let line = [
|
||||
rect.center() + scale * points[0].to_vec2(),
|
||||
rect.center() + scale * points[1].to_vec2(),
|
||||
];
|
||||
|
||||
painter.line_segment([line[0], line[1]], (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 * Rot2::from_angle(hand_rotations[0]),
|
||||
hands[1].length * 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], Srgba::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;
|
||||
|
||||
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],
|
||||
Srgba::additive_luminance(luminance_u8),
|
||||
width,
|
||||
);
|
||||
new_nodes.push(b);
|
||||
}
|
||||
}
|
||||
|
||||
std::mem::swap(&mut nodes, &mut new_nodes);
|
||||
}
|
||||
}
|
||||
}
|
||||
43
egui_demo_lib/src/apps/demo/mod.rs
Normal file
43
egui_demo_lib/src/apps/demo/mod.rs
Normal file
@@ -0,0 +1,43 @@
|
||||
//! Demo-code for showing how Egui is used.
|
||||
//!
|
||||
//! The demo-code is also used in benchmarks and tests.
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
mod app;
|
||||
mod color_test;
|
||||
mod dancing_strings;
|
||||
pub mod demo_window;
|
||||
mod demo_windows;
|
||||
mod drag_and_drop;
|
||||
mod font_book;
|
||||
pub mod font_contents_emoji;
|
||||
pub mod font_contents_ubuntu;
|
||||
mod fractal_clock;
|
||||
mod painting;
|
||||
mod scrolls;
|
||||
mod sliders;
|
||||
mod tests;
|
||||
pub mod toggle_switch;
|
||||
mod widgets;
|
||||
|
||||
pub use {
|
||||
app::*, color_test::ColorTest, dancing_strings::DancingStrings, demo_window::DemoWindow,
|
||||
demo_windows::*, drag_and_drop::*, font_book::FontBook, fractal_clock::FractalClock,
|
||||
painting::Painting, scrolls::Scrolls, sliders::Sliders, tests::Tests, widgets::Widgets,
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// Something to view in the demo windows
|
||||
pub trait View {
|
||||
fn ui(&mut self, ui: &mut egui::Ui);
|
||||
}
|
||||
|
||||
/// Something to view
|
||||
pub trait Demo {
|
||||
fn name(&self) -> &str;
|
||||
|
||||
/// Show windows, etc
|
||||
fn show(&mut self, ctx: &egui::CtxRef, open: &mut bool);
|
||||
}
|
||||
85
egui_demo_lib/src/apps/demo/painting.rs
Normal file
85
egui_demo_lib/src/apps/demo/painting.rs
Normal file
@@ -0,0 +1,85 @@
|
||||
use egui::*;
|
||||
|
||||
#[derive(serde::Deserialize, serde::Serialize)]
|
||||
#[serde(default)]
|
||||
pub struct Painting {
|
||||
lines: Vec<Vec<Vec2>>,
|
||||
stroke: Stroke,
|
||||
}
|
||||
|
||||
impl Default for Painting {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
lines: Default::default(),
|
||||
stroke: Stroke::new(1.0, color::LIGHT_BLUE),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Painting {
|
||||
pub fn ui_control(&mut self, ui: &mut Ui) {
|
||||
ui.horizontal(|ui| {
|
||||
self.stroke.ui(ui, "Stroke");
|
||||
ui.separator();
|
||||
if ui.button("Clear Painting").clicked {
|
||||
self.lines.clear();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn ui_content(&mut self, ui: &mut Ui) {
|
||||
let (response, painter) =
|
||||
ui.allocate_painter(ui.available_size_before_wrap_finite(), Sense::drag());
|
||||
let rect = response.rect;
|
||||
|
||||
if self.lines.is_empty() {
|
||||
self.lines.push(vec![]);
|
||||
}
|
||||
|
||||
let current_line = self.lines.last_mut().unwrap();
|
||||
|
||||
if response.active {
|
||||
if let Some(mouse_pos) = ui.input().mouse.pos {
|
||||
let canvas_pos = mouse_pos - rect.min;
|
||||
if current_line.last() != Some(&canvas_pos) {
|
||||
current_line.push(canvas_pos);
|
||||
}
|
||||
}
|
||||
} else if !current_line.is_empty() {
|
||||
self.lines.push(vec![]);
|
||||
}
|
||||
|
||||
for line in &self.lines {
|
||||
if line.len() >= 2 {
|
||||
let points: Vec<Pos2> = line.iter().map(|p| rect.min + *p).collect();
|
||||
painter.add(PaintCmd::line(points, self.stroke));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl super::Demo for Painting {
|
||||
fn name(&self) -> &str {
|
||||
"🖊 Painting"
|
||||
}
|
||||
|
||||
fn show(&mut self, ctx: &CtxRef, open: &mut bool) {
|
||||
use super::View;
|
||||
Window::new(self.name())
|
||||
.open(open)
|
||||
.default_size(vec2(512.0, 512.0))
|
||||
.scroll(false)
|
||||
.show(ctx, |ui| self.ui(ui));
|
||||
}
|
||||
}
|
||||
|
||||
impl super::View for Painting {
|
||||
fn ui(&mut self, ui: &mut Ui) {
|
||||
ui.add(crate::__egui_github_link_file!("(source code)"));
|
||||
self.ui_control(ui);
|
||||
ui.label("Paint with your mouse/touch!");
|
||||
Frame::dark_canvas(ui.style()).show(ui, |ui| {
|
||||
self.ui_content(ui);
|
||||
});
|
||||
}
|
||||
}
|
||||
87
egui_demo_lib/src/apps/demo/scrolls.rs
Normal file
87
egui_demo_lib/src/apps/demo/scrolls.rs
Normal file
@@ -0,0 +1,87 @@
|
||||
use egui::{color::*, *};
|
||||
|
||||
#[derive(serde::Deserialize, serde::Serialize)]
|
||||
#[serde(default)]
|
||||
pub struct Scrolls {
|
||||
track_item: usize,
|
||||
tracking: bool,
|
||||
offset: f32,
|
||||
}
|
||||
|
||||
impl Default for Scrolls {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
track_item: 25,
|
||||
tracking: true,
|
||||
offset: 0.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Scrolls {
|
||||
pub fn ui(&mut self, ui: &mut Ui) {
|
||||
ScrollArea::from_max_height(200.0).show(ui, |ui| {
|
||||
ui.label(crate::LOREM_IPSUM_LONG);
|
||||
ui.label(crate::LOREM_IPSUM_LONG);
|
||||
});
|
||||
|
||||
ui.separator();
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.checkbox(&mut self.tracking, "Track")
|
||||
.on_hover_text("The scroll position will track the selected item");
|
||||
ui.add(Slider::usize(&mut self.track_item, 1..=50).text("Track Item"));
|
||||
});
|
||||
let (scroll_offset, _) = ui.horizontal(|ui| {
|
||||
let scroll_offset = ui.small_button("Scroll Offset").clicked;
|
||||
ui.add(DragValue::f32(&mut self.offset).speed(1.0).suffix("px"));
|
||||
scroll_offset
|
||||
});
|
||||
|
||||
let scroll_top = ui.button("Scroll to top").clicked;
|
||||
let scroll_bottom = ui.button("Scroll to bottom").clicked;
|
||||
if scroll_bottom || scroll_top {
|
||||
self.tracking = false;
|
||||
}
|
||||
|
||||
const TITLES: [&str; 3] = ["Top", "Middle", "Bottom"];
|
||||
const ALIGNS: [Align; 3] = [Align::Min, Align::Center, Align::Max];
|
||||
ui.columns(3, |cols| {
|
||||
for (i, col) in cols.iter_mut().enumerate() {
|
||||
col.colored_label(WHITE, TITLES[i]);
|
||||
let mut scroll_area = ScrollArea::from_max_height(200.0).id_source(i);
|
||||
if scroll_offset {
|
||||
self.tracking = false;
|
||||
scroll_area = scroll_area.scroll_offset(self.offset);
|
||||
}
|
||||
|
||||
let (current_scroll, max_scroll) = scroll_area.show(col, |ui| {
|
||||
if scroll_top {
|
||||
ui.scroll_to_cursor(Align::top());
|
||||
}
|
||||
ui.vertical(|ui| {
|
||||
for item in 1..=50 {
|
||||
if self.tracking && item == self.track_item {
|
||||
let response = ui.colored_label(YELLOW, format!("Item {}", item));
|
||||
response.scroll_to_me(ALIGNS[i]);
|
||||
} else {
|
||||
ui.label(format!("Item {}", item));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if scroll_bottom {
|
||||
ui.scroll_to_cursor(Align::bottom());
|
||||
}
|
||||
|
||||
let margin = ui.style().visuals.clip_rect_margin;
|
||||
(
|
||||
ui.clip_rect().top() - ui.min_rect().top() + margin,
|
||||
ui.min_rect().height() - ui.clip_rect().height() + 2.0 * margin,
|
||||
)
|
||||
});
|
||||
col.colored_label(WHITE, format!("{:.0}/{:.0}", current_scroll, max_scroll));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
115
egui_demo_lib/src/apps/demo/sliders.rs
Normal file
115
egui_demo_lib/src/apps/demo/sliders.rs
Normal file
@@ -0,0 +1,115 @@
|
||||
use egui::*;
|
||||
use std::f64::INFINITY;
|
||||
|
||||
/// Showcase sliders
|
||||
#[derive(serde::Deserialize, serde::Serialize)]
|
||||
#[serde(default)]
|
||||
pub struct Sliders {
|
||||
pub min: f64,
|
||||
pub max: f64,
|
||||
pub logarithmic: bool,
|
||||
pub smart_aim: bool,
|
||||
pub integer: bool,
|
||||
pub value: f64,
|
||||
}
|
||||
|
||||
impl Default for Sliders {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
min: 0.0,
|
||||
max: 10000.0,
|
||||
logarithmic: true,
|
||||
smart_aim: true,
|
||||
integer: false,
|
||||
value: 10.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Sliders {
|
||||
pub fn ui(&mut self, ui: &mut Ui) {
|
||||
let Self {
|
||||
min,
|
||||
max,
|
||||
logarithmic,
|
||||
smart_aim,
|
||||
integer,
|
||||
value,
|
||||
} = self;
|
||||
|
||||
ui.label("You can click a slider value to edit it with the keyboard.");
|
||||
|
||||
let full_range = if *integer {
|
||||
(i32::MIN as f64)..=(i32::MAX as f64)
|
||||
} else if *logarithmic {
|
||||
-INFINITY..=INFINITY
|
||||
} else {
|
||||
-1e5..=1e5 // linear sliders make little sense with huge numbers
|
||||
};
|
||||
|
||||
*min = clamp(*min, full_range.clone());
|
||||
*max = clamp(*max, full_range.clone());
|
||||
|
||||
if *integer {
|
||||
let mut value_i32 = *value as i32;
|
||||
ui.add(
|
||||
Slider::i32(&mut value_i32, (*min as i32)..=(*max as i32))
|
||||
.logarithmic(*logarithmic)
|
||||
.smart_aim(*smart_aim)
|
||||
.text("i32 demo slider"),
|
||||
);
|
||||
*value = value_i32 as f64;
|
||||
} else {
|
||||
ui.add(
|
||||
Slider::f64(value, (*min)..=(*max))
|
||||
.logarithmic(*logarithmic)
|
||||
.smart_aim(*smart_aim)
|
||||
.text("f64 demo slider"),
|
||||
);
|
||||
|
||||
ui.label(
|
||||
"Sliders will intelligently pick how many decimals to show. \
|
||||
You can always see the full precision value by hovering the value.",
|
||||
);
|
||||
|
||||
if ui.button("Assign PI").clicked {
|
||||
self.value = std::f64::consts::PI;
|
||||
}
|
||||
}
|
||||
|
||||
ui.separator();
|
||||
ui.label("Demo slider range:");
|
||||
ui.add(
|
||||
Slider::f64(min, full_range.clone())
|
||||
.logarithmic(true)
|
||||
.smart_aim(*smart_aim)
|
||||
.text("left"),
|
||||
);
|
||||
ui.add(
|
||||
Slider::f64(max, full_range)
|
||||
.logarithmic(true)
|
||||
.smart_aim(*smart_aim)
|
||||
.text("right"),
|
||||
);
|
||||
|
||||
ui.separator();
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Slider type:");
|
||||
ui.radio_value(integer, true, "i32");
|
||||
ui.radio_value(integer, false, "f64");
|
||||
});
|
||||
ui.label("(f32, usize etc are also possible)");
|
||||
|
||||
ui.checkbox(logarithmic, "Logarithmic");
|
||||
ui.label("Logarithmic sliders are great for when you want to span a huge range, i.e. from zero to a million.");
|
||||
ui.label("Logarithmic sliders can include infinity and zero.");
|
||||
|
||||
ui.checkbox(smart_aim, "Smart Aim");
|
||||
ui.label("Smart Aim will guide you towards round values when you drag the slider so you you are more likely to hit 250 than 247.23");
|
||||
|
||||
if ui.button("Reset slider demo").clicked {
|
||||
*self = Default::default();
|
||||
}
|
||||
}
|
||||
}
|
||||
49
egui_demo_lib/src/apps/demo/tests.rs
Normal file
49
egui_demo_lib/src/apps/demo/tests.rs
Normal file
@@ -0,0 +1,49 @@
|
||||
#[derive(Default)]
|
||||
pub struct Tests {}
|
||||
|
||||
impl super::Demo for Tests {
|
||||
fn name(&self) -> &str {
|
||||
"📋 Tests"
|
||||
}
|
||||
|
||||
fn show(&mut self, ctx: &egui::CtxRef, open: &mut bool) {
|
||||
egui::Window::new(self.name()).open(open).show(ctx, |ui| {
|
||||
use super::View;
|
||||
self.ui(ui);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl super::View for Tests {
|
||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
ui.heading("Name collision example");
|
||||
|
||||
ui.label("\
|
||||
Widgets that store state require unique and persisting identifiers so we can track their state between frames.\n\
|
||||
For instance, collapsable headers needs to store wether or not they are open. \
|
||||
Their Id:s are derived from their names. \
|
||||
If you fail to give them unique names then clicking one will open both. \
|
||||
To help you debug this, an error message is printed on screen:");
|
||||
|
||||
ui.collapsing("Collapsing header", |ui| {
|
||||
ui.label("Contents of first foldable ui");
|
||||
});
|
||||
ui.collapsing("Collapsing header", |ui| {
|
||||
ui.label("Contents of second foldable ui");
|
||||
});
|
||||
|
||||
ui.label("\
|
||||
Any widget that can be interacted with also need a unique Id. \
|
||||
For most widgets the Id is generated by a running counter. \
|
||||
As long as elements are not added or removed, the Id stays the same. \
|
||||
This is fine, because during interaction (i.e. while dragging a slider), \
|
||||
the number of widgets previously in the same window is most likely not changing \
|
||||
(and if it is, the window will have a new layout, and the slider will endup somewhere else, and so aborthing the interaction probably makes sense).");
|
||||
|
||||
ui.label("So these buttons have automatic Id:s, and therefore there is no name clash:");
|
||||
let _ = ui.button("Button");
|
||||
let _ = ui.button("Button");
|
||||
|
||||
ui.add(crate::__egui_github_link_file!());
|
||||
}
|
||||
}
|
||||
91
egui_demo_lib/src/apps/demo/toggle_switch.rs
Normal file
91
egui_demo_lib/src/apps/demo/toggle_switch.rs
Normal file
@@ -0,0 +1,91 @@
|
||||
//! Source code example of how to create your own widget.
|
||||
//! This is meant to be read as a tutorial, hence the plethora of comments.
|
||||
|
||||
/// iOS-style toggle switch:
|
||||
///
|
||||
/// ``` text
|
||||
/// _____________
|
||||
/// / /.....\
|
||||
/// | |.......|
|
||||
/// \_______\_____/
|
||||
/// ```
|
||||
pub fn toggle(ui: &mut egui::Ui, on: &mut bool) -> egui::Response {
|
||||
// Widget code can be broken up in four steps:
|
||||
// 1. Decide a size for the widget
|
||||
// 2. Allocate space for it
|
||||
// 3. Handle interactions with the widget (if any)
|
||||
// 4. Paint the widget
|
||||
|
||||
// 1. Deciding widget size:
|
||||
// You can query the `ui` how much space is available,
|
||||
// but in this example we have a fixed size widget of the default size for a button:
|
||||
let desired_size = ui.style().spacing.interact_size;
|
||||
|
||||
// 2. Allocating space:
|
||||
// This is where we get a region of the screen assigned.
|
||||
// We also tell the Ui to sense clicks in the allocated region.
|
||||
let response = ui.allocate_response(desired_size, egui::Sense::click());
|
||||
|
||||
// 3. Interact: Time to check for clicks!.
|
||||
if response.clicked {
|
||||
*on = !*on;
|
||||
}
|
||||
|
||||
// 4. Paint!
|
||||
// First let's ask for a simple animation from Egui.
|
||||
// Egui keeps track of changes in the boolean associated with the id and
|
||||
// returns an animated value in the 0-1 range for how much "on" we are.
|
||||
let how_on = ui.ctx().animate_bool(response.id, *on);
|
||||
// We will follow the current style by asking
|
||||
// "how should something that is being interacted with be painted?".
|
||||
// This will, for instance, give us different colors when the widget is hovered or clicked.
|
||||
let visuals = ui.style().interact(&response);
|
||||
let off_bg_fill = egui::Rgba::new(0.0, 0.0, 0.0, 0.0);
|
||||
let on_bg_fill = egui::Rgba::new(0.0, 0.5, 0.25, 1.0);
|
||||
let bg_fill = egui::lerp(off_bg_fill..=on_bg_fill, how_on);
|
||||
// All coordinates are in absolute screen coordinates so we use `rect` to place the elements.
|
||||
let rect = response.rect;
|
||||
let radius = 0.5 * rect.height();
|
||||
ui.painter().rect(rect, radius, bg_fill, visuals.bg_stroke);
|
||||
// Paint the circle, animating it from left to right with `how_on`:
|
||||
let circle_x = egui::lerp((rect.left() + radius)..=(rect.right() - radius), how_on);
|
||||
let center = egui::pos2(circle_x, rect.center().y);
|
||||
ui.painter()
|
||||
.circle(center, 0.75 * radius, visuals.fg_fill, visuals.fg_stroke);
|
||||
|
||||
// All done! Return the interaction response so the user can check what happened
|
||||
// (hovered, clicked, ...) and maybe show a tooltip:
|
||||
response
|
||||
}
|
||||
|
||||
/// Here is the same code again, but a bit more compact:
|
||||
#[allow(dead_code)]
|
||||
fn toggle_compact(ui: &mut egui::Ui, on: &mut bool) -> egui::Response {
|
||||
let desired_size = ui.style().spacing.interact_size;
|
||||
let response = ui.allocate_response(desired_size, egui::Sense::click());
|
||||
*on ^= response.clicked; // toggle if clicked
|
||||
|
||||
let how_on = ui.ctx().animate_bool(response.id, *on);
|
||||
let visuals = ui.style().interact(&response);
|
||||
let off_bg_fill = egui::Rgba::new(0.0, 0.0, 0.0, 0.0);
|
||||
let on_bg_fill = egui::Rgba::new(0.0, 0.5, 0.25, 1.0);
|
||||
let bg_fill = egui::lerp(off_bg_fill..=on_bg_fill, how_on);
|
||||
let rect = response.rect;
|
||||
let radius = 0.5 * rect.height();
|
||||
ui.painter().rect(rect, radius, bg_fill, visuals.bg_stroke);
|
||||
let circle_x = egui::lerp((rect.left() + radius)..=(rect.right() - radius), how_on);
|
||||
let center = egui::pos2(circle_x, rect.center().y);
|
||||
ui.painter()
|
||||
.circle(center, 0.75 * radius, visuals.fg_fill, visuals.fg_stroke);
|
||||
|
||||
response
|
||||
}
|
||||
|
||||
pub fn demo(ui: &mut egui::Ui, on: &mut bool) {
|
||||
ui.horizontal_wrapped_for_text(egui::TextStyle::Button, |ui| {
|
||||
ui.label("It's easy to create your own widgets!");
|
||||
ui.label("This toggle switch is just one function and 15 lines of code:");
|
||||
toggle(ui, on).on_hover_text("Click to toggle");
|
||||
ui.add(crate::__egui_github_link_file!());
|
||||
});
|
||||
}
|
||||
158
egui_demo_lib/src/apps/demo/widgets.rs
Normal file
158
egui_demo_lib/src/apps/demo/widgets.rs
Normal file
@@ -0,0 +1,158 @@
|
||||
use egui::{color::*, *};
|
||||
|
||||
#[derive(Debug, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
enum Enum {
|
||||
First,
|
||||
Second,
|
||||
Third,
|
||||
}
|
||||
|
||||
impl Default for Enum {
|
||||
fn default() -> Self {
|
||||
Enum::First
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, serde::Serialize)]
|
||||
#[serde(default)]
|
||||
pub struct Widgets {
|
||||
button_enabled: bool,
|
||||
count: usize,
|
||||
radio: Enum,
|
||||
sliders: super::Sliders,
|
||||
angle: f32,
|
||||
color: Srgba,
|
||||
single_line_text_input: String,
|
||||
multiline_text_input: String,
|
||||
toggle_switch: bool,
|
||||
}
|
||||
|
||||
impl Default for Widgets {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
button_enabled: true,
|
||||
radio: Enum::First,
|
||||
count: 0,
|
||||
sliders: Default::default(),
|
||||
angle: std::f32::consts::TAU / 3.0,
|
||||
color: (Rgba::new(0.0, 1.0, 0.5, 1.0) * 0.75).into(),
|
||||
single_line_text_input: "Hello World!".to_owned(),
|
||||
multiline_text_input: "Text can both be so wide that it needs a line break, but you can also add manual line break by pressing enter, creating new paragraphs.\nThis is the start of the next paragraph.\n\nClick me to edit me!".to_owned(),
|
||||
toggle_switch: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Widgets {
|
||||
pub fn ui(&mut self, ui: &mut Ui) {
|
||||
ui.add(crate::__egui_github_link_file_line!());
|
||||
|
||||
ui.horizontal_wrapped_for_text(TextStyle::Body, |ui| {
|
||||
ui.add(Label::new("Text can have").text_color(srgba(110, 255, 110, 255)));
|
||||
ui.colored_label(srgba(128, 140, 255, 255), "color"); // Shortcut version
|
||||
ui.label("and tooltips.").on_hover_text(
|
||||
"This is a multiline tooltip that demonstrates that you can easily add tooltips to any element.\nThis is the second line.\nThis is the third.",
|
||||
);
|
||||
|
||||
ui.label("You can mix in other widgets into text, like");
|
||||
let _ = ui.small_button("this button");
|
||||
ui.label(".");
|
||||
|
||||
ui.label("The default font supports all latin and cyrillic characters (ИÅđ…), common math symbols (∫√∞²⅓…), and many emojis (💓🌟🖩…).")
|
||||
.on_hover_text("There is currently no support for right-to-left languages.");
|
||||
ui.label("See the 🔤 Font Book for more!");
|
||||
|
||||
ui.monospace("There is also a monospace font.");
|
||||
});
|
||||
|
||||
let tooltip_ui = |ui: &mut Ui| {
|
||||
ui.heading("The name of the tooltip");
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("This tooltip was created with");
|
||||
ui.monospace(".on_hover_ui(...)");
|
||||
});
|
||||
let _ = ui.button("A button you can never press");
|
||||
};
|
||||
ui.label("Tooltips can be more than just simple text.")
|
||||
.on_hover_ui(tooltip_ui);
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.radio_value(&mut self.radio, Enum::First, "First");
|
||||
ui.radio_value(&mut self.radio, Enum::Second, "Second");
|
||||
ui.radio_value(&mut self.radio, Enum::Third, "Third");
|
||||
});
|
||||
|
||||
egui::combo_box_with_label(ui, "Combo Box", format!("{:?}", self.radio), |ui| {
|
||||
ui.selectable_value(&mut self.radio, Enum::First, "First");
|
||||
ui.selectable_value(&mut self.radio, Enum::Second, "Second");
|
||||
ui.selectable_value(&mut self.radio, Enum::Third, "Third");
|
||||
});
|
||||
|
||||
ui.checkbox(&mut self.button_enabled, "Button enabled");
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
if ui
|
||||
.add(Button::new("Click me").enabled(self.button_enabled))
|
||||
.on_hover_text("This will just increase a counter.")
|
||||
.clicked
|
||||
{
|
||||
self.count += 1;
|
||||
}
|
||||
ui.label(format!("The button has been clicked {} times.", self.count));
|
||||
});
|
||||
|
||||
ui.separator();
|
||||
{
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Drag this value to change it:");
|
||||
ui.add(DragValue::f64(&mut self.sliders.value).speed(0.01));
|
||||
});
|
||||
|
||||
ui.add(
|
||||
egui::Slider::f64(&mut self.sliders.value, 1.0..=100.0)
|
||||
.logarithmic(true)
|
||||
.text("A slider"),
|
||||
);
|
||||
|
||||
egui::CollapsingHeader::new("More sliders")
|
||||
.default_open(false)
|
||||
.show(ui, |ui| {
|
||||
self.sliders.ui(ui);
|
||||
});
|
||||
}
|
||||
|
||||
ui.separator();
|
||||
|
||||
ui.horizontal_for_text(TextStyle::Body, |ui| {
|
||||
ui.label("An angle:");
|
||||
ui.drag_angle(&mut self.angle);
|
||||
ui.label(format!("≈ {:.3}τ", self.angle / std::f32::consts::TAU))
|
||||
.on_hover_text("Each τ represents one turn (τ = 2π)");
|
||||
})
|
||||
.1
|
||||
.on_hover_text("The angle is stored in radians, but presented in degrees");
|
||||
|
||||
ui.separator();
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.colored_label(self.color, "Click to select a different text color: ");
|
||||
ui.color_edit_button_srgba(&mut self.color);
|
||||
});
|
||||
|
||||
ui.separator();
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Single line text input:");
|
||||
let response = ui.text_edit_singleline(&mut self.single_line_text_input);
|
||||
if response.lost_kb_focus {
|
||||
// The user pressed enter.
|
||||
}
|
||||
});
|
||||
|
||||
ui.label("Multiline text input:");
|
||||
ui.text_edit_multiline(&mut self.multiline_text_input);
|
||||
|
||||
ui.separator();
|
||||
super::toggle_switch::demo(ui, &mut self.toggle_switch);
|
||||
}
|
||||
}
|
||||
296
egui_demo_lib/src/apps/http_app.rs
Normal file
296
egui_demo_lib/src/apps/http_app.rs
Normal file
@@ -0,0 +1,296 @@
|
||||
use epi::http::Response;
|
||||
use std::sync::mpsc::Receiver;
|
||||
|
||||
struct Resource {
|
||||
/// HTTP response
|
||||
response: Response,
|
||||
|
||||
/// If set, the response was an image.
|
||||
image: Option<Image>,
|
||||
|
||||
/// 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(response: Response) -> Self {
|
||||
let image = if response.header_content_type.starts_with("image/") {
|
||||
Image::decode(&response.bytes)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let colored_text = syntax_highlighting(&response);
|
||||
|
||||
Self {
|
||||
response,
|
||||
image,
|
||||
colored_text,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, serde::Serialize)]
|
||||
pub struct HttpApp {
|
||||
url: String,
|
||||
|
||||
#[serde(skip)]
|
||||
in_progress: Option<Receiver<Result<Response, String>>>,
|
||||
|
||||
#[serde(skip)]
|
||||
result: Option<Result<Resource, String>>,
|
||||
|
||||
#[serde(skip)]
|
||||
tex_mngr: TexMngr,
|
||||
}
|
||||
|
||||
impl Default for HttpApp {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
url: "https://raw.githubusercontent.com/emilk/egui/master/README.md".to_owned(),
|
||||
in_progress: Default::default(),
|
||||
result: Default::default(),
|
||||
tex_mngr: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl epi::App for HttpApp {
|
||||
fn name(&self) -> &str {
|
||||
"HTTP Fetch"
|
||||
}
|
||||
|
||||
/// Called each time the UI needs repainting, which may be many times per second.
|
||||
/// Put your widgets into a `SidePanel`, `TopPanel`, `CentralPanel`, `Window` or `Area`.
|
||||
fn ui(&mut self, ctx: &egui::CtxRef, frame: &mut epi::Frame<'_>) {
|
||||
if let Some(receiver) = &mut self.in_progress {
|
||||
// Are we there yet?
|
||||
if let Ok(result) = receiver.try_recv() {
|
||||
self.in_progress = None;
|
||||
self.result = Some(result.map(Resource::from_response));
|
||||
}
|
||||
}
|
||||
|
||||
egui::CentralPanel::default().show(ctx, |ui| {
|
||||
ui.heading("Egui Fetch Example");
|
||||
ui.add(egui::github_link_file!(
|
||||
"https://github.com/emilk/egui/blob/master/",
|
||||
"(source code)"
|
||||
));
|
||||
|
||||
if let Some(url) = ui_url(ui, &mut self.url) {
|
||||
let repaint_signal = frame.repaint_signal();
|
||||
let (sender, receiver) = std::sync::mpsc::channel();
|
||||
self.in_progress = Some(receiver);
|
||||
|
||||
frame.http_fetch(epi::http::Request::get(url), move |response| {
|
||||
sender.send(response).ok();
|
||||
repaint_signal.request_repaint();
|
||||
});
|
||||
}
|
||||
|
||||
ui.separator();
|
||||
|
||||
if self.in_progress.is_some() {
|
||||
ui.label("Please wait...");
|
||||
} else if let Some(result) = &self.result {
|
||||
match result {
|
||||
Ok(resource) => {
|
||||
ui_resouce(ui, frame, &mut self.tex_mngr, resource);
|
||||
}
|
||||
Err(error) => {
|
||||
// This should only happen if the fetch API isn't available or something similar.
|
||||
ui.add(egui::Label::new(error).text_color(egui::color::RED));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn ui_url(ui: &mut egui::Ui, url: &mut String) -> Option<String> {
|
||||
let mut trigger_fetch = false;
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("URL:");
|
||||
trigger_fetch |= ui.text_edit_singleline(url).lost_kb_focus;
|
||||
trigger_fetch |= ui.button("GET").clicked;
|
||||
});
|
||||
|
||||
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().time;
|
||||
let width = 640;
|
||||
let height = 480;
|
||||
*url = format!("https://picsum.photos/seed/{}/{}/{}", seed, width, height);
|
||||
trigger_fetch = true;
|
||||
}
|
||||
});
|
||||
|
||||
if trigger_fetch {
|
||||
Some(url.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn ui_resouce(
|
||||
ui: &mut egui::Ui,
|
||||
frame: &mut epi::Frame<'_>,
|
||||
tex_mngr: &mut TexMngr,
|
||||
resource: &Resource,
|
||||
) {
|
||||
let Resource {
|
||||
response,
|
||||
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.header_content_type));
|
||||
ui.monospace(format!(
|
||||
"Size: {:.1} kB",
|
||||
response.bytes.len() as f32 / 1000.0
|
||||
));
|
||||
|
||||
if let Some(text) = &response.text {
|
||||
let tooltip = "Click to copy the response body";
|
||||
if ui.button("📋").on_hover_text(tooltip).clicked {
|
||||
ui.output().copied_text = text.clone();
|
||||
}
|
||||
}
|
||||
|
||||
ui.separator();
|
||||
|
||||
egui::ScrollArea::auto_sized().show(ui, |ui| {
|
||||
if let Some(image) = image {
|
||||
if let Some(texture_id) = tex_mngr.texture(frame, &response.url, image) {
|
||||
let size = egui::Vec2::new(image.size.0 as f32, image.size.1 as f32);
|
||||
ui.image(texture_id, size);
|
||||
}
|
||||
} else if let Some(colored_text) = colored_text {
|
||||
colored_text.ui(ui);
|
||||
} else if let Some(text) = &response.text {
|
||||
ui.monospace(text);
|
||||
} else {
|
||||
ui.monospace("[binary]");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Syntax highlighting:
|
||||
|
||||
fn syntax_highlighting(response: &Response) -> Option<ColoredText> {
|
||||
let text = response.text.as_ref()?;
|
||||
let extension_and_rest: Vec<&str> = response.url.rsplitn(2, '.').collect();
|
||||
let extension = extension_and_rest.get(0)?;
|
||||
ColoredText::text_with_extension(text, extension)
|
||||
}
|
||||
|
||||
/// Lines of text fragments
|
||||
struct ColoredText(Vec<Vec<(syntect::highlighting::Style, String)>>);
|
||||
|
||||
impl ColoredText {
|
||||
/// e.g. `text_with_extension("fn foo() {}", "rs")`
|
||||
pub fn text_with_extension(text: &str, extension: &str) -> Option<ColoredText> {
|
||||
use syntect::easy::HighlightLines;
|
||||
use syntect::highlighting::ThemeSet;
|
||||
use syntect::parsing::SyntaxSet;
|
||||
use syntect::util::LinesWithEndings;
|
||||
|
||||
let ps = SyntaxSet::load_defaults_newlines(); // should be cached and reused
|
||||
let ts = ThemeSet::load_defaults(); // should be cached and reused
|
||||
|
||||
let syntax = ps.find_syntax_by_extension(extension)?;
|
||||
|
||||
let mut h = HighlightLines::new(syntax, &ts.themes["base16-mocha.dark"]);
|
||||
|
||||
let lines = LinesWithEndings::from(text)
|
||||
.map(|line| {
|
||||
h.highlight(line, &ps)
|
||||
.into_iter()
|
||||
.map(|(style, range)| (style, range.trim_end_matches('\n').to_owned()))
|
||||
.collect()
|
||||
})
|
||||
.collect();
|
||||
|
||||
Some(ColoredText(lines))
|
||||
}
|
||||
|
||||
pub fn ui(&self, ui: &mut egui::Ui) {
|
||||
for line in &self.0 {
|
||||
ui.horizontal_wrapped_for_text(egui::TextStyle::Monospace, |ui| {
|
||||
ui.style_mut().spacing.item_spacing.x = 0.0;
|
||||
for (style, range) in line {
|
||||
let fg = style.foreground;
|
||||
let text_color = egui::Srgba::from_rgb(fg.r, fg.g, fg.b);
|
||||
ui.add(egui::Label::new(range).monospace().text_color(text_color));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Texture/image handling is very manual at the moment.
|
||||
|
||||
/// Immediate mode texture manager that supports at most one texture at the time :)
|
||||
#[derive(Default)]
|
||||
struct TexMngr {
|
||||
loaded_url: String,
|
||||
texture_id: Option<egui::TextureId>,
|
||||
}
|
||||
|
||||
impl TexMngr {
|
||||
fn texture(
|
||||
&mut self,
|
||||
frame: &mut epi::Frame<'_>,
|
||||
url: &str,
|
||||
image: &Image,
|
||||
) -> Option<egui::TextureId> {
|
||||
let tex_allocator = frame.tex_allocator().as_mut()?;
|
||||
let texture_id = self.texture_id.unwrap_or_else(|| tex_allocator.alloc());
|
||||
self.texture_id = Some(texture_id);
|
||||
if self.loaded_url != url {
|
||||
self.loaded_url = url.to_owned();
|
||||
tex_allocator.set_srgba_premultiplied(texture_id, image.size, &image.pixels);
|
||||
}
|
||||
Some(texture_id)
|
||||
}
|
||||
}
|
||||
|
||||
struct Image {
|
||||
size: (usize, usize),
|
||||
pixels: Vec<egui::Srgba>,
|
||||
}
|
||||
|
||||
impl Image {
|
||||
fn decode(bytes: &[u8]) -> Option<Image> {
|
||||
use image::GenericImageView;
|
||||
let image = image::load_from_memory(bytes).ok()?;
|
||||
let image_buffer = image.to_rgba8();
|
||||
let size = (image.width() as usize, image.height() as usize);
|
||||
let pixels = image_buffer.into_vec();
|
||||
assert_eq!(size.0 * size.1 * 4, pixels.len());
|
||||
let pixels = pixels
|
||||
.chunks(4)
|
||||
.map(|p| egui::Srgba::from_rgba_unmultiplied(p[0], p[1], p[2], p[3]))
|
||||
.collect();
|
||||
|
||||
Some(Image { size, pixels })
|
||||
}
|
||||
}
|
||||
7
egui_demo_lib/src/apps/mod.rs
Normal file
7
egui_demo_lib/src/apps/mod.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
mod demo;
|
||||
mod http_app;
|
||||
|
||||
pub use demo::DemoApp;
|
||||
pub use http_app::HttpApp;
|
||||
|
||||
pub use demo::DemoWindows; // used for tests
|
||||
Reference in New Issue
Block a user