mirror of
https://github.com/emilk/egui.git
synced 2026-09-01 06:10:06 -04:00
Split out the Egui demo code to new crate egui_demo_lib
This commit is contained in:
@@ -26,9 +26,6 @@ rusttype = "0.9"
|
||||
serde = { version = "1", features = ["derive"], optional = true }
|
||||
serde_json = { version = "1", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = { version = "0.3", default-features = false }
|
||||
|
||||
[features]
|
||||
default = ["atomic_refcell", "default_fonts"]
|
||||
|
||||
@@ -38,7 +35,3 @@ default_fonts = []
|
||||
|
||||
# Only needed if you plan to use the same egui::Context from multiple threads.
|
||||
multi_threaded = ["parking_lot"]
|
||||
|
||||
[[bench]]
|
||||
name = "benchmark"
|
||||
harness = false
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
use criterion::{criterion_group, criterion_main, Criterion};
|
||||
|
||||
pub fn criterion_benchmark(c: &mut Criterion) {
|
||||
let raw_input = egui::RawInput::default();
|
||||
|
||||
{
|
||||
let mut ctx = egui::CtxRef::default();
|
||||
let mut demo_windows = egui::demos::DemoWindows::default();
|
||||
|
||||
c.bench_function("demo_windows_minimal", |b| {
|
||||
b.iter(|| {
|
||||
ctx.begin_frame(raw_input.clone());
|
||||
demo_windows.ui(&ctx, &Default::default(), &mut None, |_ui| {});
|
||||
ctx.end_frame()
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let mut ctx = egui::CtxRef::default();
|
||||
ctx.memory().all_collpasing_are_open = true; // expand the demo window with everything
|
||||
let mut demo_windows = egui::demos::DemoWindows::default();
|
||||
|
||||
c.bench_function("demo_windows_full", |b| {
|
||||
b.iter(|| {
|
||||
ctx.begin_frame(raw_input.clone());
|
||||
demo_windows.ui(&ctx, &Default::default(), &mut None, |_ui| {});
|
||||
ctx.end_frame()
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let mut ctx = egui::CtxRef::default();
|
||||
ctx.memory().all_collpasing_are_open = true; // expand the demo window with everything
|
||||
let mut demo_windows = egui::demos::DemoWindows::default();
|
||||
ctx.begin_frame(raw_input.clone());
|
||||
demo_windows.ui(&ctx, &Default::default(), &mut None, |_ui| {});
|
||||
let (_, paint_commands) = ctx.end_frame();
|
||||
|
||||
c.bench_function("tessellate", |b| {
|
||||
b.iter(|| ctx.tessellate(paint_commands.clone()))
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let mut ctx = egui::CtxRef::default();
|
||||
ctx.begin_frame(raw_input);
|
||||
egui::CentralPanel::default().show(&ctx, |ui| {
|
||||
c.bench_function("label", |b| {
|
||||
b.iter(|| {
|
||||
ui.label(egui::demos::LOREM_IPSUM_LONG);
|
||||
})
|
||||
});
|
||||
});
|
||||
let _ = ctx.end_frame();
|
||||
}
|
||||
}
|
||||
|
||||
criterion_group!(benches, criterion_benchmark);
|
||||
criterion_main!(benches);
|
||||
@@ -676,6 +676,12 @@ impl Context {
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
/// Move all the graphics at the given layer.
|
||||
/// Can be used to implement drag-and-drop (see relevant demo).
|
||||
pub fn translate_layer(&self, layer_id: LayerId, delta: Vec2) {
|
||||
self.graphics().list(layer_id).translate(delta);
|
||||
}
|
||||
|
||||
pub fn layer_id_at(&self, pos: Pos2) -> Option<LayerId> {
|
||||
let resize_grab_radius_side = self.style().interaction.resize_grab_radius_side;
|
||||
self.memory().layer_id_at(pos, resize_grab_radius_side)
|
||||
|
||||
@@ -1,356 +0,0 @@
|
||||
use crate::{app, demos, util::History, CtxRef, Response, Ui};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// 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.",
|
||||
);
|
||||
crate::demos::warn_if_debug_build(ui);
|
||||
|
||||
crate::CollapsingHeader::new("📊 CPU usage history")
|
||||
.default_open(false)
|
||||
.show(ui, |ui| {
|
||||
self.graph(ui);
|
||||
});
|
||||
}
|
||||
|
||||
fn graph(&mut self, ui: &mut Ui) -> Response {
|
||||
use crate::*;
|
||||
|
||||
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 `egui::app::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)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
pub struct DemoApp {
|
||||
demo_windows: demos::DemoWindows,
|
||||
|
||||
backend_window_open: bool,
|
||||
|
||||
#[cfg_attr(feature = "serde", 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>,
|
||||
|
||||
#[cfg_attr(feature = "serde", serde(skip))]
|
||||
frame_history: FrameHistory,
|
||||
}
|
||||
|
||||
impl DemoApp {
|
||||
fn backend_ui(&mut self, ui: &mut Ui, integration_context: &mut app::IntegrationContext<'_>) {
|
||||
let is_web = integration_context.info.web_info.is_some();
|
||||
|
||||
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();
|
||||
integration_context.output.pixels_per_point =
|
||||
self.pixels_per_point_ui(ui, &integration_context.info);
|
||||
}
|
||||
|
||||
if !is_web {
|
||||
ui.separator();
|
||||
integration_context.output.quit |= ui.button("Quit").clicked;
|
||||
}
|
||||
}
|
||||
|
||||
fn pixels_per_point_ui(&mut self, ui: &mut Ui, info: &app::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(
|
||||
crate::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 app::App for DemoApp {
|
||||
fn name(&self) -> &str {
|
||||
"Egui Demo"
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde_json")]
|
||||
fn load(&mut self, storage: &dyn crate::app::Storage) {
|
||||
*self = crate::app::get_value(storage, crate::app::APP_KEY).unwrap_or_default()
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde_json")]
|
||||
fn save(&mut self, storage: &mut dyn crate::app::Storage) {
|
||||
crate::app::set_value(storage, crate::app::APP_KEY, self);
|
||||
}
|
||||
|
||||
fn ui(&mut self, ctx: &CtxRef, integration_context: &mut crate::app::IntegrationContext<'_>) {
|
||||
self.frame_history
|
||||
.on_new_frame(ctx.input().time, integration_context.info.cpu_usage);
|
||||
|
||||
let web_location_hash = integration_context
|
||||
.info
|
||||
.web_info
|
||||
.as_ref()
|
||||
.map(|info| info.web_location_hash.clone())
|
||||
.unwrap_or_default();
|
||||
|
||||
let link = if web_location_hash == "clock" {
|
||||
Some(demos::DemoLink::Clock)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let demo_environment = demos::DemoEnvironment {
|
||||
seconds_since_midnight: integration_context.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,
|
||||
&mut integration_context.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;
|
||||
crate::Window::new("💻 Backend")
|
||||
.min_width(360.0)
|
||||
.scroll(false)
|
||||
.open(&mut backend_window_open)
|
||||
.show(ctx, |ui| {
|
||||
self.backend_ui(ui, integration_context);
|
||||
});
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,375 +0,0 @@
|
||||
use crate::widgets::color_picker::show_color;
|
||||
use crate::*;
|
||||
use 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 app::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 app::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 app::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 crate::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([
|
||||
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 app::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
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
use crate::{containers::*, demos::*, *};
|
||||
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
pub struct DancingStrings {}
|
||||
|
||||
impl Default for DancingStrings {
|
||||
fn default() -> Self {
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
|
||||
impl Demo for DancingStrings {
|
||||
fn name(&self) -> &str {
|
||||
"♫ Dancing Strings"
|
||||
}
|
||||
|
||||
fn show(&mut self, ctx: &CtxRef, open: &mut bool) {
|
||||
Window::new(self.name())
|
||||
.open(open)
|
||||
.default_size(vec2(512.0, 256.0))
|
||||
.scroll(false)
|
||||
.show(ctx, |ui| self.ui(ui));
|
||||
}
|
||||
}
|
||||
|
||||
impl 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(__egui_github_link_file!());
|
||||
}
|
||||
}
|
||||
@@ -1,416 +0,0 @@
|
||||
use crate::{color::*, demos::*, *};
|
||||
|
||||
/// Showcase some ui code
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[cfg_attr(feature = "serde", 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[cfg_attr(feature = "serde", 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],
|
||||
));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[cfg_attr(feature = "serde", 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),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
use crate::layout::*;
|
||||
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[cfg_attr(feature = "serde", 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)]
|
||||
#[cfg_attr(feature = "serde", derive(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
|
||||
}
|
||||
}
|
||||
@@ -1,365 +0,0 @@
|
||||
use crate::{
|
||||
app,
|
||||
demos::{self, Demo},
|
||||
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>,
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
struct Demos {
|
||||
/// open, view
|
||||
#[cfg_attr(feature = "serde", serde(skip))] // TODO: serialize the `open` state.
|
||||
demos: Vec<(bool, Box<dyn Demo>)>,
|
||||
}
|
||||
impl Default for Demos {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
demos: vec![
|
||||
(false, Box::new(crate::demos::FontBook::default())),
|
||||
(false, Box::new(crate::demos::Painting::default())),
|
||||
(false, Box::new(crate::demos::DancingStrings::default())),
|
||||
(false, Box::new(crate::demos::DragAndDropDemo::default())),
|
||||
(false, Box::new(crate::demos::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)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
pub struct DemoWindows {
|
||||
open_windows: OpenWindows,
|
||||
|
||||
demo_window: demos::DemoWindow,
|
||||
|
||||
#[cfg_attr(feature = "serde", serde(skip))]
|
||||
color_test: demos::ColorTest,
|
||||
|
||||
fractal_clock: demos::FractalClock,
|
||||
|
||||
/// open, title, view
|
||||
demos: Demos,
|
||||
|
||||
#[cfg_attr(feature = "serde", 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 app::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;
|
||||
}
|
||||
|
||||
crate::SidePanel::left("side_panel", 190.0).show(ctx, |ui| {
|
||||
ui.heading("✒ Egui Demo");
|
||||
crate::demos::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(
|
||||
crate::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);
|
||||
});
|
||||
});
|
||||
|
||||
crate::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 app::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(demos::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(demos::LOREM_IPSUM_LONG);
|
||||
ui.label(demos::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(demos::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(demos::LOREM_IPSUM);
|
||||
});
|
||||
ui.heading("Resize the above area!");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[cfg_attr(feature = "serde", 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 crate::*;
|
||||
|
||||
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;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1,158 +0,0 @@
|
||||
use crate::{
|
||||
demos::{Demo, View},
|
||||
*,
|
||||
};
|
||||
|
||||
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(layers::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().graphics().list(layer_id).translate(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 Demo for DragAndDropDemo {
|
||||
fn name(&self) -> &str {
|
||||
"✋ Drag and Drop"
|
||||
}
|
||||
|
||||
fn show(&mut self, ctx: &CtxRef, open: &mut bool) {
|
||||
Window::new(self.name())
|
||||
.open(open)
|
||||
.default_size(vec2(256.0, 256.0))
|
||||
.scroll(false)
|
||||
.resizable(false)
|
||||
.show(ctx, |ui| self.ui(ui));
|
||||
}
|
||||
}
|
||||
|
||||
impl 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(__egui_github_link_file!());
|
||||
}
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
use crate::*;
|
||||
|
||||
pub struct FontBook {
|
||||
standard: bool,
|
||||
emojis: bool,
|
||||
filter: String,
|
||||
text_style: TextStyle,
|
||||
}
|
||||
|
||||
impl Default for FontBook {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
standard: false,
|
||||
emojis: true,
|
||||
filter: Default::default(),
|
||||
text_style: TextStyle::Button,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FontBook {
|
||||
fn characters_ui(&self, ui: &mut Ui, characters: &[(u32, char, &str)]) {
|
||||
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 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 demos::Demo for FontBook {
|
||||
fn name(&self) -> &str {
|
||||
"🔤 Font Book"
|
||||
}
|
||||
|
||||
fn show(&mut self, ctx: &crate::CtxRef, open: &mut bool) {
|
||||
Window::new(self.name()).open(open).show(ctx, |ui| {
|
||||
use demos::View;
|
||||
self.ui(ui);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl demos::View for FontBook {
|
||||
fn ui(&mut self, ui: &mut Ui) {
|
||||
use crate::demos::font_contents_emoji::FULL_EMOJI_LIST;
|
||||
use crate::demos::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();
|
||||
|
||||
combo_box_with_label(ui, "Text style", format!("{:?}", self.text_style), |ui| {
|
||||
for style in 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();
|
||||
|
||||
crate::ScrollArea::auto_sized().show(ui, |ui| {
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
ui.style_mut().spacing.item_spacing = 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
@@ -1,202 +0,0 @@
|
||||
use crate::{containers::*, widgets::*, *};
|
||||
use std::f32::consts::TAU;
|
||||
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
pub struct FractalClock {
|
||||
paused: bool,
|
||||
time: f64,
|
||||
zoom: f32,
|
||||
start_line_width: f32,
|
||||
depth: usize,
|
||||
length_factor: f32,
|
||||
luminance_factor: f32,
|
||||
width_factor: f32,
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
//! 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,
|
||||
};
|
||||
|
||||
pub const LOREM_IPSUM: &str = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.";
|
||||
|
||||
pub const LOREM_IPSUM_LONG: &str = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
|
||||
|
||||
Curabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra, est eros bibendum elit, nec luctus magna felis sollicitudin mauris. Integer in mauris eu nibh euismod gravida. Duis ac tellus et risus vulputate vehicula. Donec lobortis risus a elit. Etiam tempor. Ut ullamcorper, ligula eu tempor congue, eros est euismod turpis, id tincidunt sapien risus a quam. Maecenas fermentum consequat mi. Donec fermentum. Pellentesque malesuada nulla a mi. Duis sapien sem, aliquet nec, commodo eget, consequat quis, neque. Aliquam faucibus, elit ut dictum aliquet, felis nisl adipiscing sapien, sed malesuada diam lacus eget erat. Cras mollis scelerisque nunc. Nullam arcu. Aliquam consequat. Curabitur augue lorem, dapibus quis, laoreet et, pretium ac, nisi. Aenean magna nisl, mollis quis, molestie eu, feugiat in, orci. In hac habitasse platea dictumst.";
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// Something to view in the demo windows
|
||||
pub trait View {
|
||||
fn ui(&mut self, ui: &mut crate::Ui);
|
||||
}
|
||||
|
||||
/// Something to view
|
||||
pub trait Demo {
|
||||
fn name(&self) -> &str;
|
||||
|
||||
/// Show windows, etc
|
||||
fn show(&mut self, ctx: &crate::CtxRef, open: &mut bool);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
pub fn warn_if_debug_build(ui: &mut crate::Ui) {
|
||||
if crate::has_debug_assertions() {
|
||||
ui.label(
|
||||
crate::Label::new("‼ Debug build ‼")
|
||||
.small()
|
||||
.text_color(crate::color::RED),
|
||||
)
|
||||
.on_hover_text("Egui was compiled with debug assertions enabled.");
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// Create a [`Hyperlink`](crate::Hyperlink) to this file (and line) on Github
|
||||
///
|
||||
/// Example: `ui.add(github_link_file_line!("https://github.com/YOUR/PROJECT/blob/master/", "(source code)"));`
|
||||
#[macro_export]
|
||||
macro_rules! github_link_file_line {
|
||||
($github_url:expr, $label:expr) => {{
|
||||
let url = format!("{}{}#L{}", $github_url, file!(), line!());
|
||||
$crate::Hyperlink::new(url).text($label)
|
||||
}};
|
||||
}
|
||||
|
||||
/// Create a [`Hyperlink`](crate::Hyperlink) to this file on github.
|
||||
///
|
||||
/// Example: `ui.add(github_link_file!("https://github.com/YOUR/PROJECT/blob/master/", "(source code)"));`
|
||||
#[macro_export]
|
||||
macro_rules! github_link_file {
|
||||
($github_url:expr, $label:expr) => {{
|
||||
let url = format!("{}{}", $github_url, file!());
|
||||
$crate::Hyperlink::new(url).text($label)
|
||||
}};
|
||||
}
|
||||
|
||||
/// Create a [`Hyperlink`](crate::Hyperlink) to this egui source code file on github.
|
||||
#[doc(hidden)]
|
||||
#[macro_export]
|
||||
macro_rules! __egui_github_link_file {
|
||||
() => {
|
||||
__egui_github_link_file!("(source code)")
|
||||
};
|
||||
($label:expr) => {
|
||||
github_link_file!("https://github.com/emilk/egui/blob/master/", $label).small()
|
||||
};
|
||||
}
|
||||
|
||||
/// Create a [`Hyperlink`](crate::Hyperlink) to this egui source code file and line on github.
|
||||
#[doc(hidden)]
|
||||
#[macro_export]
|
||||
macro_rules! __egui_github_link_file_line {
|
||||
() => {
|
||||
__egui_github_link_file_line!("(source code)")
|
||||
};
|
||||
($label:expr) => {
|
||||
github_link_file_line!("https://github.com/emilk/egui/blob/master/", $label).small()
|
||||
};
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
use crate::{demos::*, *};
|
||||
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[cfg_attr(feature = "serde", 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 demos::Demo for Painting {
|
||||
fn name(&self) -> &str {
|
||||
"🖊 Painting"
|
||||
}
|
||||
|
||||
fn show(&mut self, ctx: &CtxRef, open: &mut bool) {
|
||||
Window::new(self.name())
|
||||
.open(open)
|
||||
.default_size(vec2(512.0, 512.0))
|
||||
.scroll(false)
|
||||
.show(ctx, |ui| self.ui(ui));
|
||||
}
|
||||
}
|
||||
|
||||
impl demos::View for Painting {
|
||||
fn ui(&mut self, ui: &mut Ui) {
|
||||
ui.add(__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);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
use crate::{color::*, demos::LOREM_IPSUM_LONG, *};
|
||||
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[cfg_attr(feature = "serde", 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(LOREM_IPSUM_LONG);
|
||||
ui.label(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));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
use crate::*;
|
||||
use std::f64::INFINITY;
|
||||
|
||||
/// Showcase sliders
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[cfg_attr(feature = "serde", 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
use crate::*;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Tests {}
|
||||
|
||||
impl demos::Demo for Tests {
|
||||
fn name(&self) -> &str {
|
||||
"📋 Tests"
|
||||
}
|
||||
|
||||
fn show(&mut self, ctx: &crate::CtxRef, open: &mut bool) {
|
||||
Window::new(self.name()).open(open).show(ctx, |ui| {
|
||||
use demos::View;
|
||||
self.ui(ui);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl demos::View for Tests {
|
||||
fn ui(&mut self, ui: &mut 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(__egui_github_link_file!());
|
||||
}
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
//! Source code example of how to create your own widget.
|
||||
//! This is meant to be read as a tutorial, hence the plethora of comments.
|
||||
use crate::*;
|
||||
|
||||
/// iOS-style toggle switch:
|
||||
///
|
||||
/// ``` text
|
||||
/// _____________
|
||||
/// / /.....\
|
||||
/// | |.......|
|
||||
/// \_______\_____/
|
||||
/// ```
|
||||
pub fn toggle(ui: &mut Ui, on: &mut bool) -> 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, 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 = Rgba::new(0.0, 0.0, 0.0, 0.0);
|
||||
let on_bg_fill = Rgba::new(0.0, 0.5, 0.25, 1.0);
|
||||
let bg_fill = 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 = lerp((rect.left() + radius)..=(rect.right() - radius), how_on);
|
||||
let center = 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 Ui, on: &mut bool) -> Response {
|
||||
let desired_size = ui.style().spacing.interact_size;
|
||||
let response = ui.allocate_response(desired_size, 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 = Rgba::new(0.0, 0.0, 0.0, 0.0);
|
||||
let on_bg_fill = Rgba::new(0.0, 0.5, 0.25, 1.0);
|
||||
let bg_fill = 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 = lerp((rect.left() + radius)..=(rect.right() - radius), how_on);
|
||||
let center = 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 Ui, on: &mut bool) {
|
||||
ui.horizontal_wrapped_for_text(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(__egui_github_link_file!());
|
||||
});
|
||||
}
|
||||
@@ -1,159 +0,0 @@
|
||||
use crate::{color::*, demos::Sliders, *};
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
enum Enum {
|
||||
First,
|
||||
Second,
|
||||
Third,
|
||||
}
|
||||
|
||||
impl Default for Enum {
|
||||
fn default() -> Self {
|
||||
Enum::First
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
pub struct Widgets {
|
||||
button_enabled: bool,
|
||||
count: usize,
|
||||
radio: Enum,
|
||||
sliders: 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(__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");
|
||||
});
|
||||
|
||||
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(
|
||||
Slider::f64(&mut self.sliders.value, 1.0..=100.0)
|
||||
.logarithmic(true)
|
||||
.text("A slider"),
|
||||
);
|
||||
|
||||
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.add(Label::new("Click to select a different text color: ").text_color(self.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);
|
||||
}
|
||||
}
|
||||
@@ -130,15 +130,6 @@ impl Default for Layout {
|
||||
}
|
||||
|
||||
impl Layout {
|
||||
pub(crate) fn from_main_dir_and_cross_align(main_dir: Direction, cross_align: Align) -> Self {
|
||||
Self {
|
||||
main_dir,
|
||||
main_wrap: false,
|
||||
cross_align,
|
||||
cross_justify: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn left_to_right() -> Self {
|
||||
Self {
|
||||
main_dir: Direction::LeftToRight,
|
||||
@@ -180,6 +171,15 @@ impl Layout {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_main_dir_and_cross_align(main_dir: Direction, cross_align: Align) -> Self {
|
||||
Self {
|
||||
main_dir,
|
||||
main_wrap: false,
|
||||
cross_align,
|
||||
cross_justify: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[deprecated = "Use `top_down`"]
|
||||
pub fn vertical(cross_align: Align) -> Self {
|
||||
Self::top_down(cross_align)
|
||||
|
||||
@@ -82,7 +82,6 @@ mod animation_manager;
|
||||
pub mod app;
|
||||
pub mod containers;
|
||||
mod context;
|
||||
pub mod demos;
|
||||
mod id;
|
||||
mod input;
|
||||
mod introspection;
|
||||
@@ -103,7 +102,6 @@ pub use {
|
||||
align::Align,
|
||||
containers::*,
|
||||
context::{Context, CtxRef},
|
||||
demos::DemoApp,
|
||||
id::Id,
|
||||
input::*,
|
||||
layers::*,
|
||||
@@ -122,28 +120,50 @@ pub use {
|
||||
widgets::*,
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
pub(crate) fn has_debug_assertions() -> bool {
|
||||
pub(crate) const fn has_debug_assertions() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
pub(crate) fn has_debug_assertions() -> bool {
|
||||
pub(crate) const fn has_debug_assertions() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_egui_e2e() {
|
||||
let mut demo_windows = crate::demos::DemoWindows::default();
|
||||
let mut ctx = crate::CtxRef::default();
|
||||
let raw_input = crate::RawInput::default();
|
||||
|
||||
const NUM_FRAMES: usize = 5;
|
||||
for _ in 0..NUM_FRAMES {
|
||||
ctx.begin_frame(raw_input.clone());
|
||||
demo_windows.ui(&ctx, &Default::default(), &mut None, |_ui| {});
|
||||
let (_output, paint_commands) = ctx.end_frame();
|
||||
let paint_jobs = ctx.tessellate(paint_commands);
|
||||
assert!(!paint_jobs.is_empty());
|
||||
/// Helper function that adds a label when compiling with debug assertions enabled.
|
||||
pub fn warn_if_debug_build(ui: &mut crate::Ui) {
|
||||
if crate::has_debug_assertions() {
|
||||
ui.label(
|
||||
crate::Label::new("‼ Debug build ‼")
|
||||
.small()
|
||||
.text_color(crate::color::RED),
|
||||
)
|
||||
.on_hover_text("Egui was compiled with debug assertions enabled.");
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// Create a [`Hyperlink`](crate::Hyperlink) to this file (and line) on Github
|
||||
///
|
||||
/// Example: `ui.add(github_link_file_line!("https://github.com/YOUR/PROJECT/blob/master/", "(source code)"));`
|
||||
#[macro_export]
|
||||
macro_rules! github_link_file_line {
|
||||
($github_url:expr, $label:expr) => {{
|
||||
let url = format!("{}{}#L{}", $github_url, file!(), line!());
|
||||
$crate::Hyperlink::new(url).text($label)
|
||||
}};
|
||||
}
|
||||
|
||||
/// Create a [`Hyperlink`](crate::Hyperlink) to this file on github.
|
||||
///
|
||||
/// Example: `ui.add(github_link_file!("https://github.com/YOUR/PROJECT/blob/master/", "(source code)"));`
|
||||
#[macro_export]
|
||||
macro_rules! github_link_file {
|
||||
($github_url:expr, $label:expr) => {{
|
||||
let url = format!("{}{}", $github_url, file!());
|
||||
$crate::Hyperlink::new(url).text($label)
|
||||
}};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user