1
0
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:
Emil Ernerfeldt
2021-01-01 17:11:05 +01:00
parent 375e317547
commit defad4ed51
38 changed files with 441 additions and 1334 deletions

View 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();
}
}
}

View 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
})
}
}

View 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!());
}
}

View 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
}
}

View 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;
}
});
}
});
}

View 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!());
}
}

View 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("").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);
}
});
});
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View 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);
}
}
}

View 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);
}

View 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);
});
}
}

View 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));
}
});
}
}

View 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();
}
}
}

View 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!());
}
}

View 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!());
});
}

View 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);
}
}

View 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 })
}
}

View 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