mirror of
https://github.com/emilk/egui.git
synced 2026-09-02 06:40:06 -04:00
Move all crates into a crates directory (#1940)
This commit is contained in:
94
crates/egui_demo_lib/src/demo/about.rs
Normal file
94
crates/egui_demo_lib/src/demo/about.rs
Normal file
@@ -0,0 +1,94 @@
|
||||
#[derive(Default)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
pub struct About {}
|
||||
|
||||
impl super::Demo for About {
|
||||
fn name(&self) -> &'static str {
|
||||
"About egui"
|
||||
}
|
||||
|
||||
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
|
||||
egui::Window::new(self.name())
|
||||
.default_width(320.0)
|
||||
.open(open)
|
||||
.show(ctx, |ui| {
|
||||
use super::View as _;
|
||||
self.ui(ui);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl super::View for About {
|
||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
use egui::special_emojis::{OS_APPLE, OS_LINUX, OS_WINDOWS};
|
||||
|
||||
ui.heading("egui");
|
||||
ui.label(format!(
|
||||
"egui is an immediate mode GUI library written in Rust. egui runs both on the web and natively on {}{}{}. \
|
||||
On the web it is compiled to WebAssembly and rendered with WebGL.{}",
|
||||
OS_APPLE, OS_LINUX, OS_WINDOWS,
|
||||
if cfg!(target_arch = "wasm32") {
|
||||
" Everything you see is rendered as textured triangles. There is no DOM, HTML, JS or CSS. Just Rust."
|
||||
} else {""}
|
||||
));
|
||||
ui.label("egui is designed to be easy to use, portable, and fast.");
|
||||
|
||||
ui.add_space(12.0); // ui.separator();
|
||||
ui.heading("Immediate mode");
|
||||
about_immediate_mode(ui);
|
||||
|
||||
ui.add_space(12.0); // ui.separator();
|
||||
ui.heading("Links");
|
||||
links(ui);
|
||||
}
|
||||
}
|
||||
|
||||
fn about_immediate_mode(ui: &mut egui::Ui) {
|
||||
use crate::syntax_highlighting::code_view_ui;
|
||||
ui.style_mut().spacing.interact_size.y = 0.0; // hack to make `horizontal_wrapped` work better with text.
|
||||
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = 0.0;
|
||||
ui.label("Immediate mode is a GUI paradigm that lets you create a GUI with less code and simpler control flow. For example, this is how you create a ");
|
||||
let _ = ui.small_button("button");
|
||||
ui.label(" in egui:");
|
||||
});
|
||||
|
||||
ui.add_space(8.0);
|
||||
code_view_ui(
|
||||
ui,
|
||||
r#"
|
||||
if ui.button("Save").clicked() {
|
||||
my_state.save();
|
||||
}"#
|
||||
.trim_start_matches('\n'),
|
||||
);
|
||||
ui.add_space(8.0);
|
||||
|
||||
ui.label("Note how there are no callbacks or messages, and no button state to store.");
|
||||
|
||||
ui.label("Immediate mode has its roots in gaming, where everything on the screen is painted at the display refresh rate, i.e. at 60+ frames per second. \
|
||||
In immediate mode GUIs, the entire interface is layed out and painted at the same high rate. \
|
||||
This makes immediate mode GUIs especially well suited for highly interactive applications.");
|
||||
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = 0.0;
|
||||
ui.label("More about immediate mode ");
|
||||
ui.hyperlink_to("here", "https://github.com/emilk/egui#why-immediate-mode");
|
||||
ui.label(".");
|
||||
});
|
||||
}
|
||||
|
||||
fn links(ui: &mut egui::Ui) {
|
||||
use egui::special_emojis::{GITHUB, TWITTER};
|
||||
ui.hyperlink_to(
|
||||
format!("{} egui on GitHub", GITHUB),
|
||||
"https://github.com/emilk/egui",
|
||||
);
|
||||
ui.hyperlink_to(
|
||||
format!("{} @ernerfeldt", TWITTER),
|
||||
"https://twitter.com/ernerfeldt",
|
||||
);
|
||||
ui.hyperlink_to("egui documentation", "https://docs.rs/egui/");
|
||||
}
|
||||
97
crates/egui_demo_lib/src/demo/code_editor.rs
Normal file
97
crates/egui_demo_lib/src/demo/code_editor.rs
Normal file
@@ -0,0 +1,97 @@
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
pub struct CodeEditor {
|
||||
language: String,
|
||||
code: String,
|
||||
}
|
||||
|
||||
impl Default for CodeEditor {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
language: "rs".into(),
|
||||
code: "// A very simple example\n\
|
||||
fn main() {\n\
|
||||
\tprintln!(\"Hello world!\");\n\
|
||||
}\n\
|
||||
"
|
||||
.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl super::Demo for CodeEditor {
|
||||
fn name(&self) -> &'static str {
|
||||
"🖮 Code Editor"
|
||||
}
|
||||
|
||||
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
|
||||
use super::View as _;
|
||||
egui::Window::new(self.name())
|
||||
.open(open)
|
||||
.default_height(500.0)
|
||||
.show(ctx, |ui| self.ui(ui));
|
||||
}
|
||||
}
|
||||
|
||||
impl super::View for CodeEditor {
|
||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
let Self { language, code } = self;
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.set_height(0.0);
|
||||
ui.label("An example of syntax highlighting in a TextEdit.");
|
||||
ui.add(crate::egui_github_link_file!());
|
||||
});
|
||||
|
||||
if cfg!(feature = "syntect") {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Language:");
|
||||
ui.text_edit_singleline(language);
|
||||
});
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = 0.0;
|
||||
ui.label("Syntax highlighting powered by ");
|
||||
ui.hyperlink_to("syntect", "https://github.com/trishume/syntect");
|
||||
ui.label(".");
|
||||
});
|
||||
} else {
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = 0.0;
|
||||
ui.label("Compile the demo with the ");
|
||||
ui.code("syntax_highlighting");
|
||||
ui.label(" feature to enable more accurate syntax highlighting using ");
|
||||
ui.hyperlink_to("syntect", "https://github.com/trishume/syntect");
|
||||
ui.label(".");
|
||||
});
|
||||
}
|
||||
|
||||
let mut theme = crate::syntax_highlighting::CodeTheme::from_memory(ui.ctx());
|
||||
ui.collapsing("Theme", |ui| {
|
||||
ui.group(|ui| {
|
||||
theme.ui(ui);
|
||||
theme.clone().store_in_memory(ui.ctx());
|
||||
});
|
||||
});
|
||||
|
||||
let mut layouter = |ui: &egui::Ui, string: &str, wrap_width: f32| {
|
||||
let mut layout_job =
|
||||
crate::syntax_highlighting::highlight(ui.ctx(), &theme, string, language);
|
||||
layout_job.wrap.max_width = wrap_width;
|
||||
ui.fonts().layout_job(layout_job)
|
||||
};
|
||||
|
||||
egui::ScrollArea::vertical().show(ui, |ui| {
|
||||
ui.add(
|
||||
egui::TextEdit::multiline(code)
|
||||
.font(egui::TextStyle::Monospace) // for cursor height
|
||||
.code_editor()
|
||||
.desired_rows(10)
|
||||
.lock_focus(true)
|
||||
.desired_width(f32::INFINITY)
|
||||
.layouter(&mut layouter),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
161
crates/egui_demo_lib/src/demo/code_example.rs
Normal file
161
crates/egui_demo_lib/src/demo/code_example.rs
Normal file
@@ -0,0 +1,161 @@
|
||||
#[derive(Debug)]
|
||||
pub struct CodeExample {
|
||||
name: String,
|
||||
age: u32,
|
||||
}
|
||||
|
||||
impl Default for CodeExample {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
name: "Arthur".to_owned(),
|
||||
age: 42,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CodeExample {
|
||||
fn samples_in_grid(&mut self, ui: &mut egui::Ui) {
|
||||
show_code(ui, r#"ui.heading("Code samples");"#);
|
||||
ui.heading("Code samples");
|
||||
ui.end_row();
|
||||
|
||||
show_code(
|
||||
ui,
|
||||
r#"
|
||||
// Putting things on the same line using ui.horizontal:
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Your name: ");
|
||||
ui.text_edit_singleline(&mut self.name);
|
||||
});"#,
|
||||
);
|
||||
// Putting things on the same line using ui.horizontal:
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Your name: ");
|
||||
ui.text_edit_singleline(&mut self.name);
|
||||
});
|
||||
ui.end_row();
|
||||
|
||||
show_code(
|
||||
ui,
|
||||
r#"egui::Slider::new(&mut self.age, 0..=120).text("age")"#,
|
||||
);
|
||||
ui.add(egui::Slider::new(&mut self.age, 0..=120).text("age"));
|
||||
ui.end_row();
|
||||
|
||||
show_code(
|
||||
ui,
|
||||
r#"
|
||||
if ui.button("Click each year").clicked() {
|
||||
self.age += 1;
|
||||
}"#,
|
||||
);
|
||||
if ui.button("Click each year").clicked() {
|
||||
self.age += 1;
|
||||
}
|
||||
ui.end_row();
|
||||
|
||||
show_code(
|
||||
ui,
|
||||
r#"ui.label(format!("Hello '{}', age {}", self.name, self.age));"#,
|
||||
);
|
||||
ui.label(format!("Hello '{}', age {}", self.name, self.age));
|
||||
ui.end_row();
|
||||
}
|
||||
}
|
||||
|
||||
impl super::Demo for CodeExample {
|
||||
fn name(&self) -> &'static str {
|
||||
"🖮 Code Example"
|
||||
}
|
||||
|
||||
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
|
||||
use super::View;
|
||||
egui::Window::new(self.name())
|
||||
.open(open)
|
||||
.default_size([800.0, 400.0])
|
||||
.vscroll(false)
|
||||
.hscroll(true)
|
||||
.show(ctx, |ui| self.ui(ui));
|
||||
}
|
||||
}
|
||||
|
||||
impl super::View for CodeExample {
|
||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
use crate::syntax_highlighting::code_view_ui;
|
||||
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.add(crate::egui_github_link_file!());
|
||||
});
|
||||
|
||||
code_view_ui(
|
||||
ui,
|
||||
r"
|
||||
pub struct CodeExample {
|
||||
name: String,
|
||||
age: u32,
|
||||
}
|
||||
|
||||
impl CodeExample {
|
||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
"
|
||||
.trim(),
|
||||
);
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
let font_id = egui::TextStyle::Monospace.resolve(ui.style());
|
||||
let indentation = 8.0 * ui.fonts().glyph_width(&font_id, ' ');
|
||||
let item_spacing = ui.spacing_mut().item_spacing;
|
||||
ui.add_space(indentation - item_spacing.x);
|
||||
|
||||
egui::Grid::new("code_samples")
|
||||
.striped(true)
|
||||
.num_columns(2)
|
||||
.min_col_width(16.0)
|
||||
.spacing([16.0, 8.0])
|
||||
.show(ui, |ui| {
|
||||
self.samples_in_grid(ui);
|
||||
});
|
||||
});
|
||||
|
||||
code_view_ui(ui, " }\n}");
|
||||
|
||||
ui.separator();
|
||||
|
||||
code_view_ui(ui, &format!("{:#?}", self));
|
||||
|
||||
ui.separator();
|
||||
|
||||
let mut theme = crate::syntax_highlighting::CodeTheme::from_memory(ui.ctx());
|
||||
ui.collapsing("Theme", |ui| {
|
||||
theme.ui(ui);
|
||||
theme.store_in_memory(ui.ctx());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn show_code(ui: &mut egui::Ui, code: &str) {
|
||||
let code = remove_leading_indentation(code.trim_start_matches('\n'));
|
||||
crate::syntax_highlighting::code_view_ui(ui, &code);
|
||||
}
|
||||
|
||||
fn remove_leading_indentation(code: &str) -> String {
|
||||
fn is_indent(c: &u8) -> bool {
|
||||
matches!(*c, b' ' | b'\t')
|
||||
}
|
||||
|
||||
let first_line_indent = code.bytes().take_while(is_indent).count();
|
||||
|
||||
let mut out = String::new();
|
||||
|
||||
let mut code = code;
|
||||
while !code.is_empty() {
|
||||
let indent = code.bytes().take_while(is_indent).count();
|
||||
let start = first_line_indent.min(indent);
|
||||
let end = code
|
||||
.find('\n')
|
||||
.map_or_else(|| code.len(), |endline| endline + 1);
|
||||
out += &code[start..end];
|
||||
code = &code[end..];
|
||||
}
|
||||
out
|
||||
}
|
||||
182
crates/egui_demo_lib/src/demo/context_menu.rs
Normal file
182
crates/egui_demo_lib/src/demo/context_menu.rs
Normal file
@@ -0,0 +1,182 @@
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
enum Plot {
|
||||
Sin,
|
||||
Bell,
|
||||
Sigmoid,
|
||||
}
|
||||
|
||||
fn gaussian(x: f64) -> f64 {
|
||||
let var: f64 = 2.0;
|
||||
f64::exp(-(x / var).powi(2)) / (var * f64::sqrt(std::f64::consts::TAU))
|
||||
}
|
||||
|
||||
fn sigmoid(x: f64) -> f64 {
|
||||
-1.0 + 2.0 / (1.0 + f64::exp(-x))
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct ContextMenus {
|
||||
plot: Plot,
|
||||
show_axes: [bool; 2],
|
||||
allow_drag: bool,
|
||||
allow_zoom: bool,
|
||||
allow_scroll: bool,
|
||||
center_x_axis: bool,
|
||||
center_y_axis: bool,
|
||||
width: f32,
|
||||
height: f32,
|
||||
}
|
||||
|
||||
impl Default for ContextMenus {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
plot: Plot::Sin,
|
||||
show_axes: [true, true],
|
||||
allow_drag: true,
|
||||
allow_zoom: true,
|
||||
allow_scroll: true,
|
||||
center_x_axis: false,
|
||||
center_y_axis: false,
|
||||
width: 400.0,
|
||||
height: 200.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl super::Demo for ContextMenus {
|
||||
fn name(&self) -> &'static str {
|
||||
"☰ Context Menus"
|
||||
}
|
||||
|
||||
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
|
||||
use super::View;
|
||||
egui::Window::new(self.name())
|
||||
.vscroll(false)
|
||||
.resizable(false)
|
||||
.open(open)
|
||||
.show(ctx, |ui| self.ui(ui));
|
||||
}
|
||||
}
|
||||
|
||||
impl super::View for ContextMenus {
|
||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
ui.horizontal(|ui| {
|
||||
ui.menu_button("Click for menu", Self::nested_menus);
|
||||
ui.button("Right-click for menu")
|
||||
.context_menu(Self::nested_menus);
|
||||
});
|
||||
|
||||
ui.separator();
|
||||
|
||||
ui.label("Right-click plot to edit it!");
|
||||
ui.horizontal(|ui| {
|
||||
self.example_plot(ui).context_menu(|ui| {
|
||||
ui.menu_button("Plot", |ui| {
|
||||
if ui.radio_value(&mut self.plot, Plot::Sin, "Sin").clicked()
|
||||
|| ui
|
||||
.radio_value(&mut self.plot, Plot::Bell, "Gaussian")
|
||||
.clicked()
|
||||
|| ui
|
||||
.radio_value(&mut self.plot, Plot::Sigmoid, "Sigmoid")
|
||||
.clicked()
|
||||
{
|
||||
ui.close_menu();
|
||||
}
|
||||
});
|
||||
egui::Grid::new("button_grid").show(ui, |ui| {
|
||||
ui.add(
|
||||
egui::DragValue::new(&mut self.width)
|
||||
.speed(1.0)
|
||||
.prefix("Width:"),
|
||||
);
|
||||
ui.add(
|
||||
egui::DragValue::new(&mut self.height)
|
||||
.speed(1.0)
|
||||
.prefix("Height:"),
|
||||
);
|
||||
ui.end_row();
|
||||
ui.checkbox(&mut self.show_axes[0], "x-Axis");
|
||||
ui.checkbox(&mut self.show_axes[1], "y-Axis");
|
||||
ui.end_row();
|
||||
if ui.checkbox(&mut self.allow_drag, "Drag").changed()
|
||||
|| ui.checkbox(&mut self.allow_zoom, "Zoom").changed()
|
||||
|| ui.checkbox(&mut self.allow_scroll, "Scroll").changed()
|
||||
{
|
||||
ui.close_menu();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.add(crate::egui_github_link_file!());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl ContextMenus {
|
||||
fn example_plot(&self, ui: &mut egui::Ui) -> egui::Response {
|
||||
use egui::plot::{Line, PlotPoints};
|
||||
let n = 128;
|
||||
let line = Line::new(
|
||||
(0..=n)
|
||||
.map(|i| {
|
||||
use std::f64::consts::TAU;
|
||||
let x = egui::remap(i as f64, 0.0..=n as f64, -TAU..=TAU);
|
||||
match self.plot {
|
||||
Plot::Sin => [x, x.sin()],
|
||||
Plot::Bell => [x, 10.0 * gaussian(x)],
|
||||
Plot::Sigmoid => [x, sigmoid(x)],
|
||||
}
|
||||
})
|
||||
.collect::<PlotPoints>(),
|
||||
);
|
||||
egui::plot::Plot::new("example_plot")
|
||||
.show_axes(self.show_axes)
|
||||
.allow_drag(self.allow_drag)
|
||||
.allow_zoom(self.allow_zoom)
|
||||
.allow_scroll(self.allow_scroll)
|
||||
.center_x_axis(self.center_x_axis)
|
||||
.center_x_axis(self.center_y_axis)
|
||||
.width(self.width)
|
||||
.height(self.height)
|
||||
.data_aspect(1.0)
|
||||
.show(ui, |plot_ui| plot_ui.line(line))
|
||||
.response
|
||||
}
|
||||
|
||||
fn nested_menus(ui: &mut egui::Ui) {
|
||||
if ui.button("Open...").clicked() {
|
||||
ui.close_menu();
|
||||
}
|
||||
ui.menu_button("SubMenu", |ui| {
|
||||
ui.menu_button("SubMenu", |ui| {
|
||||
if ui.button("Open...").clicked() {
|
||||
ui.close_menu();
|
||||
}
|
||||
let _ = ui.button("Item");
|
||||
});
|
||||
ui.menu_button("SubMenu", |ui| {
|
||||
if ui.button("Open...").clicked() {
|
||||
ui.close_menu();
|
||||
}
|
||||
let _ = ui.button("Item");
|
||||
});
|
||||
let _ = ui.button("Item");
|
||||
if ui.button("Open...").clicked() {
|
||||
ui.close_menu();
|
||||
}
|
||||
});
|
||||
ui.menu_button("SubMenu", |ui| {
|
||||
let _ = ui.button("Item1");
|
||||
let _ = ui.button("Item2");
|
||||
let _ = ui.button("Item3");
|
||||
let _ = ui.button("Item4");
|
||||
if ui.button("Open...").clicked() {
|
||||
ui.close_menu();
|
||||
}
|
||||
});
|
||||
let _ = ui.button("Very long text for this item");
|
||||
}
|
||||
}
|
||||
67
crates/egui_demo_lib/src/demo/dancing_strings.rs
Normal file
67
crates/egui_demo_lib/src/demo/dancing_strings.rs
Normal file
@@ -0,0 +1,67 @@
|
||||
use egui::{containers::*, *};
|
||||
|
||||
#[derive(Default)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
pub struct DancingStrings {}
|
||||
|
||||
impl super::Demo for DancingStrings {
|
||||
fn name(&self) -> &'static str {
|
||||
"♫ Dancing Strings"
|
||||
}
|
||||
|
||||
fn show(&mut self, ctx: &Context, open: &mut bool) {
|
||||
use super::View as _;
|
||||
Window::new(self.name())
|
||||
.open(open)
|
||||
.default_size(vec2(512.0, 256.0))
|
||||
.vscroll(false)
|
||||
.show(ctx, |ui| self.ui(ui));
|
||||
}
|
||||
}
|
||||
|
||||
impl super::View for DancingStrings {
|
||||
fn ui(&mut self, ui: &mut Ui) {
|
||||
let color = if ui.visuals().dark_mode {
|
||||
Color32::from_additive_luminance(196)
|
||||
} else {
|
||||
Color32::from_black_alpha(240)
|
||||
};
|
||||
|
||||
Frame::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 to_screen =
|
||||
emath::RectTransform::from_to(Rect::from_x_y_ranges(0.0..=1.0, -1.0..=1.0), rect);
|
||||
|
||||
let mut shapes = vec![];
|
||||
|
||||
for &mode in &[2, 3, 5] {
|
||||
let mode = mode as f64;
|
||||
let n = 120;
|
||||
let speed = 1.5;
|
||||
|
||||
let points: Vec<Pos2> = (0..=n)
|
||||
.map(|i| {
|
||||
let t = i as f64 / (n as f64);
|
||||
let amp = (time * speed * mode).sin() / mode;
|
||||
let y = amp * (t * std::f64::consts::TAU / 2.0 * mode).sin();
|
||||
to_screen * pos2(t as f32, y as f32)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let thickness = 10.0 / mode as f32;
|
||||
shapes.push(epaint::Shape::line(points, Stroke::new(thickness, color)));
|
||||
}
|
||||
|
||||
ui.painter().extend(shapes);
|
||||
});
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.add(crate::egui_github_link_file!());
|
||||
});
|
||||
}
|
||||
}
|
||||
318
crates/egui_demo_lib/src/demo/demo_app_windows.rs
Normal file
318
crates/egui_demo_lib/src/demo/demo_app_windows.rs
Normal file
@@ -0,0 +1,318 @@
|
||||
use egui::{Context, ScrollArea, Ui};
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use super::About;
|
||||
use super::Demo;
|
||||
use super::View;
|
||||
use crate::is_mobile;
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
struct Demos {
|
||||
#[cfg_attr(feature = "serde", serde(skip))]
|
||||
demos: Vec<Box<dyn Demo>>,
|
||||
|
||||
open: BTreeSet<String>,
|
||||
}
|
||||
|
||||
impl Default for Demos {
|
||||
fn default() -> Self {
|
||||
Self::from_demos(vec![
|
||||
Box::new(super::paint_bezier::PaintBezier::default()),
|
||||
Box::new(super::code_editor::CodeEditor::default()),
|
||||
Box::new(super::code_example::CodeExample::default()),
|
||||
Box::new(super::context_menu::ContextMenus::default()),
|
||||
Box::new(super::dancing_strings::DancingStrings::default()),
|
||||
Box::new(super::drag_and_drop::DragAndDropDemo::default()),
|
||||
Box::new(super::font_book::FontBook::default()),
|
||||
Box::new(super::MiscDemoWindow::default()),
|
||||
Box::new(super::multi_touch::MultiTouch::default()),
|
||||
Box::new(super::painting::Painting::default()),
|
||||
Box::new(super::plot_demo::PlotDemo::default()),
|
||||
Box::new(super::scrolling::Scrolling::default()),
|
||||
Box::new(super::sliders::Sliders::default()),
|
||||
Box::new(super::strip_demo::StripDemo::default()),
|
||||
Box::new(super::table_demo::TableDemo::default()),
|
||||
Box::new(super::text_edit::TextEdit::default()),
|
||||
Box::new(super::widget_gallery::WidgetGallery::default()),
|
||||
Box::new(super::window_options::WindowOptions::default()),
|
||||
Box::new(super::tests::WindowResizeTest::default()),
|
||||
Box::new(super::window_with_panels::WindowWithPanels::default()),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
impl Demos {
|
||||
pub fn from_demos(demos: Vec<Box<dyn Demo>>) -> Self {
|
||||
let mut open = BTreeSet::new();
|
||||
open.insert(
|
||||
super::widget_gallery::WidgetGallery::default()
|
||||
.name()
|
||||
.to_owned(),
|
||||
);
|
||||
|
||||
Self { demos, open }
|
||||
}
|
||||
|
||||
pub fn checkboxes(&mut self, ui: &mut Ui) {
|
||||
let Self { demos, open } = self;
|
||||
for demo in demos {
|
||||
let mut is_open = open.contains(demo.name());
|
||||
ui.toggle_value(&mut is_open, demo.name());
|
||||
set_open(open, demo.name(), is_open);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn windows(&mut self, ctx: &Context) {
|
||||
let Self { demos, open } = self;
|
||||
for demo in demos {
|
||||
let mut is_open = open.contains(demo.name());
|
||||
demo.show(ctx, &mut is_open);
|
||||
set_open(open, demo.name(), is_open);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
struct Tests {
|
||||
#[cfg_attr(feature = "serde", serde(skip))]
|
||||
demos: Vec<Box<dyn Demo>>,
|
||||
|
||||
open: BTreeSet<String>,
|
||||
}
|
||||
|
||||
impl Default for Tests {
|
||||
fn default() -> Self {
|
||||
Self::from_demos(vec![
|
||||
Box::new(super::tests::CursorTest::default()),
|
||||
Box::new(super::tests::IdTest::default()),
|
||||
Box::new(super::tests::InputTest::default()),
|
||||
Box::new(super::layout_test::LayoutTest::default()),
|
||||
Box::new(super::tests::ManualLayoutTest::default()),
|
||||
Box::new(super::tests::TableTest::default()),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
impl Tests {
|
||||
pub fn from_demos(demos: Vec<Box<dyn Demo>>) -> Self {
|
||||
let mut open = BTreeSet::new();
|
||||
open.insert(
|
||||
super::widget_gallery::WidgetGallery::default()
|
||||
.name()
|
||||
.to_owned(),
|
||||
);
|
||||
|
||||
Self { demos, open }
|
||||
}
|
||||
|
||||
pub fn checkboxes(&mut self, ui: &mut Ui) {
|
||||
let Self { demos, open } = self;
|
||||
for demo in demos {
|
||||
let mut is_open = open.contains(demo.name());
|
||||
ui.toggle_value(&mut is_open, demo.name());
|
||||
set_open(open, demo.name(), is_open);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn windows(&mut self, ctx: &Context) {
|
||||
let Self { demos, open } = self;
|
||||
for demo in demos {
|
||||
let mut is_open = open.contains(demo.name());
|
||||
demo.show(ctx, &mut is_open);
|
||||
set_open(open, demo.name(), is_open);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
fn set_open(open: &mut BTreeSet<String>, key: &'static str, is_open: bool) {
|
||||
if is_open {
|
||||
if !open.contains(key) {
|
||||
open.insert(key.to_owned());
|
||||
}
|
||||
} else {
|
||||
open.remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// A menu bar in which you can select different demo windows to show.
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
pub struct DemoWindows {
|
||||
about_is_open: bool,
|
||||
about: About,
|
||||
demos: Demos,
|
||||
tests: Tests,
|
||||
}
|
||||
|
||||
impl Default for DemoWindows {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
about_is_open: true,
|
||||
about: Default::default(),
|
||||
demos: Default::default(),
|
||||
tests: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DemoWindows {
|
||||
/// Show the app ui (menu bar and windows).
|
||||
pub fn ui(&mut self, ctx: &Context) {
|
||||
if is_mobile(ctx) {
|
||||
self.mobile_ui(ctx);
|
||||
} else {
|
||||
self.desktop_ui(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
fn mobile_ui(&mut self, ctx: &Context) {
|
||||
if self.about_is_open {
|
||||
let screen_size = ctx.input().screen_rect.size();
|
||||
let default_width = (screen_size.x - 20.0).min(400.0);
|
||||
|
||||
let mut close = false;
|
||||
egui::Window::new(self.about.name())
|
||||
.anchor(egui::Align2::CENTER_CENTER, [0.0, 0.0])
|
||||
.default_width(default_width)
|
||||
.default_height(ctx.available_rect().height() - 46.0)
|
||||
.vscroll(true)
|
||||
.open(&mut self.about_is_open)
|
||||
.resizable(false)
|
||||
.collapsible(false)
|
||||
.show(ctx, |ui| {
|
||||
self.about.ui(ui);
|
||||
ui.add_space(12.0);
|
||||
ui.vertical_centered_justified(|ui| {
|
||||
if ui
|
||||
.button(egui::RichText::new("Continue to the demo!").size(24.0))
|
||||
.clicked()
|
||||
{
|
||||
close = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
self.about_is_open &= !close;
|
||||
} else {
|
||||
self.mobile_top_bar(ctx);
|
||||
self.show_windows(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
fn mobile_top_bar(&mut self, ctx: &Context) {
|
||||
egui::TopBottomPanel::top("menu_bar").show(ctx, |ui| {
|
||||
egui::menu::bar(ui, |ui| {
|
||||
let font_size = 20.0;
|
||||
|
||||
ui.menu_button(egui::RichText::new("⏷ demos").size(font_size), |ui| {
|
||||
ui.set_style(ui.ctx().style()); // ignore the "menu" style set by `menu_button`.
|
||||
self.demo_list_ui(ui);
|
||||
if ui.ui_contains_pointer() && ui.input().pointer.any_click() {
|
||||
ui.close_menu();
|
||||
}
|
||||
});
|
||||
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
use egui::special_emojis::{GITHUB, TWITTER};
|
||||
ui.hyperlink_to(
|
||||
egui::RichText::new(TWITTER).size(font_size),
|
||||
"https://twitter.com/ernerfeldt",
|
||||
);
|
||||
ui.hyperlink_to(
|
||||
egui::RichText::new(GITHUB).size(font_size),
|
||||
"https://github.com/emilk/egui",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn desktop_ui(&mut self, ctx: &Context) {
|
||||
egui::SidePanel::right("egui_demo_panel")
|
||||
.resizable(false)
|
||||
.default_width(145.0)
|
||||
.show(ctx, |ui| {
|
||||
egui::trace!(ui);
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.heading("✒ egui demos");
|
||||
});
|
||||
|
||||
ui.separator();
|
||||
|
||||
use egui::special_emojis::{GITHUB, TWITTER};
|
||||
ui.hyperlink_to(
|
||||
format!("{} egui on GitHub", GITHUB),
|
||||
"https://github.com/emilk/egui",
|
||||
);
|
||||
ui.hyperlink_to(
|
||||
format!("{} @ernerfeldt", TWITTER),
|
||||
"https://twitter.com/ernerfeldt",
|
||||
);
|
||||
|
||||
ui.separator();
|
||||
|
||||
self.demo_list_ui(ui);
|
||||
});
|
||||
|
||||
egui::TopBottomPanel::top("menu_bar").show(ctx, |ui| {
|
||||
egui::menu::bar(ui, |ui| {
|
||||
file_menu_button(ui);
|
||||
});
|
||||
});
|
||||
|
||||
self.show_windows(ctx);
|
||||
}
|
||||
|
||||
/// Show the open windows.
|
||||
fn show_windows(&mut self, ctx: &Context) {
|
||||
self.about.show(ctx, &mut self.about_is_open);
|
||||
self.demos.windows(ctx);
|
||||
self.tests.windows(ctx);
|
||||
}
|
||||
|
||||
fn demo_list_ui(&mut self, ui: &mut egui::Ui) {
|
||||
ScrollArea::vertical().show(ui, |ui| {
|
||||
ui.with_layout(egui::Layout::top_down_justified(egui::Align::LEFT), |ui| {
|
||||
ui.toggle_value(&mut self.about_is_open, self.about.name());
|
||||
|
||||
ui.separator();
|
||||
self.demos.checkboxes(ui);
|
||||
ui.separator();
|
||||
self.tests.checkboxes(ui);
|
||||
ui.separator();
|
||||
|
||||
if ui.button("Organize windows").clicked() {
|
||||
ui.ctx().memory().reset_areas();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
fn file_menu_button(ui: &mut Ui) {
|
||||
ui.menu_button("File", |ui| {
|
||||
if ui.button("Organize windows").clicked() {
|
||||
ui.ctx().memory().reset_areas();
|
||||
ui.close_menu();
|
||||
}
|
||||
if ui
|
||||
.button("Reset egui memory")
|
||||
.on_hover_text("Forget scroll, positions, sizes etc")
|
||||
.clicked()
|
||||
{
|
||||
*ui.ctx().memory() = Default::default();
|
||||
ui.close_menu();
|
||||
}
|
||||
});
|
||||
}
|
||||
178
crates/egui_demo_lib/src/demo/drag_and_drop.rs
Normal file
178
crates/egui_demo_lib/src/demo/drag_and_drop.rs
Normal file
@@ -0,0 +1,178 @@
|
||||
use egui::*;
|
||||
|
||||
pub fn drag_source(ui: &mut Ui, id: Id, body: impl FnOnce(&mut Ui)) {
|
||||
let is_being_dragged = ui.memory().is_being_dragged(id);
|
||||
|
||||
if !is_being_dragged {
|
||||
let response = ui.scope(body).response;
|
||||
|
||||
// Check for drags:
|
||||
let response = ui.interact(response.rect, id, Sense::drag());
|
||||
if response.hovered() {
|
||||
ui.output().cursor_icon = CursorIcon::Grab;
|
||||
}
|
||||
} else {
|
||||
ui.output().cursor_icon = CursorIcon::Grabbing;
|
||||
|
||||
// Paint the body to a new layer:
|
||||
let layer_id = LayerId::new(Order::Tooltip, id);
|
||||
let response = ui.with_layer_id(layer_id, body).response;
|
||||
|
||||
// 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(pointer_pos) = ui.ctx().pointer_interact_pos() {
|
||||
let delta = pointer_pos - response.rect.center();
|
||||
ui.ctx().translate_layer(layer_id, delta);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn drop_target<R>(
|
||||
ui: &mut Ui,
|
||||
can_accept_what_is_being_dragged: bool,
|
||||
body: impl FnOnce(&mut Ui) -> R,
|
||||
) -> InnerResponse<R> {
|
||||
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(Shape::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 (rect, response) = ui.allocate_at_least(outer_rect.size(), Sense::hover());
|
||||
|
||||
let style = if is_being_dragged && can_accept_what_is_being_dragged && response.hovered() {
|
||||
ui.visuals().widgets.active
|
||||
} else {
|
||||
ui.visuals().widgets.inactive
|
||||
};
|
||||
|
||||
let mut fill = style.bg_fill;
|
||||
let mut stroke = style.bg_stroke;
|
||||
if is_being_dragged && !can_accept_what_is_being_dragged {
|
||||
// gray out:
|
||||
fill = color::tint_color_towards(fill, ui.visuals().window_fill());
|
||||
stroke.color = color::tint_color_towards(stroke.color, ui.visuals().window_fill());
|
||||
}
|
||||
|
||||
ui.painter().set(
|
||||
where_to_put_background,
|
||||
epaint::RectShape {
|
||||
rounding: style.rounding,
|
||||
fill,
|
||||
stroke,
|
||||
rect,
|
||||
},
|
||||
);
|
||||
|
||||
InnerResponse::new(ret, response)
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct DragAndDropDemo {
|
||||
/// columns with items
|
||||
columns: Vec<Vec<String>>,
|
||||
}
|
||||
|
||||
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"],
|
||||
]
|
||||
.into_iter()
|
||||
.map(|v| v.into_iter().map(ToString::to_string).collect())
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl super::Demo for DragAndDropDemo {
|
||||
fn name(&self) -> &'static str {
|
||||
"✋ Drag and Drop"
|
||||
}
|
||||
|
||||
fn show(&mut self, ctx: &Context, open: &mut bool) {
|
||||
use super::View as _;
|
||||
Window::new(self.name())
|
||||
.open(open)
|
||||
.default_size(vec2(256.0, 256.0))
|
||||
.vscroll(false)
|
||||
.resizable(false)
|
||||
.show(ctx, |ui| self.ui(ui));
|
||||
}
|
||||
}
|
||||
|
||||
impl super::View for DragAndDropDemo {
|
||||
fn ui(&mut self, ui: &mut Ui) {
|
||||
ui.label("This is a proof-of-concept of drag-and-drop in egui.");
|
||||
ui.label("Drag items between columns.");
|
||||
|
||||
let id_source = "my_drag_and_drop_demo";
|
||||
let mut source_col_row = None;
|
||||
let mut drop_col = None;
|
||||
ui.columns(self.columns.len(), |uis| {
|
||||
for (col_idx, column) in self.columns.clone().into_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(id_source).with(col_idx).with(row_idx);
|
||||
drag_source(ui, item_id, |ui| {
|
||||
let response = ui.add(Label::new(item).sense(Sense::click()));
|
||||
response.context_menu(|ui| {
|
||||
if ui.button("Remove").clicked() {
|
||||
self.columns[col_idx].remove(row_idx);
|
||||
ui.close_menu();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if ui.memory().is_being_dragged(item_id) {
|
||||
source_col_row = Some((col_idx, row_idx));
|
||||
}
|
||||
}
|
||||
})
|
||||
.response;
|
||||
|
||||
let response = response.context_menu(|ui| {
|
||||
if ui.button("New Item").clicked() {
|
||||
self.columns[col_idx].push("New Item".to_owned());
|
||||
ui.close_menu();
|
||||
}
|
||||
});
|
||||
|
||||
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().pointer.any_released() {
|
||||
// do the drop:
|
||||
let item = self.columns[source_col].remove(source_row);
|
||||
self.columns[drop_col].push(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.add(crate::egui_github_link_file!());
|
||||
});
|
||||
}
|
||||
}
|
||||
261
crates/egui_demo_lib/src/demo/font_book.rs
Normal file
261
crates/egui_demo_lib/src/demo/font_book.rs
Normal file
@@ -0,0 +1,261 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
pub struct FontBook {
|
||||
filter: String,
|
||||
font_id: egui::FontId,
|
||||
named_chars: BTreeMap<egui::FontFamily, BTreeMap<char, String>>,
|
||||
}
|
||||
|
||||
impl Default for FontBook {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
filter: Default::default(),
|
||||
font_id: egui::FontId::proportional(20.0),
|
||||
named_chars: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl super::Demo for FontBook {
|
||||
fn name(&self) -> &'static str {
|
||||
"🔤 Font Book"
|
||||
}
|
||||
|
||||
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
|
||||
egui::Window::new(self.name()).open(open).show(ctx, |ui| {
|
||||
use super::View as _;
|
||||
self.ui(ui);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl super::View for FontBook {
|
||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.add(crate::egui_github_link_file!());
|
||||
});
|
||||
|
||||
ui.label(format!(
|
||||
"The selected font supports {} characters.",
|
||||
self.named_chars
|
||||
.get(&self.font_id.family)
|
||||
.map(|map| map.len())
|
||||
.unwrap_or_default()
|
||||
));
|
||||
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = 0.0;
|
||||
ui.label("You can add more characters by installing additional fonts with ");
|
||||
ui.add(egui::Hyperlink::from_label_and_url(
|
||||
egui::RichText::new("Context::set_fonts").text_style(egui::TextStyle::Monospace),
|
||||
"https://docs.rs/egui/latest/egui/struct.Context.html#method.set_fonts",
|
||||
));
|
||||
ui.label(".");
|
||||
});
|
||||
|
||||
ui.separator();
|
||||
|
||||
egui::introspection::font_id_ui(ui, &mut self.font_id);
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Filter:");
|
||||
ui.add(egui::TextEdit::singleline(&mut self.filter).desired_width(120.0));
|
||||
self.filter = self.filter.to_lowercase();
|
||||
if ui.button("x").clicked() {
|
||||
self.filter.clear();
|
||||
}
|
||||
});
|
||||
|
||||
let filter = &self.filter;
|
||||
let named_chars = self
|
||||
.named_chars
|
||||
.entry(self.font_id.family.clone())
|
||||
.or_insert_with(|| available_characters(ui, self.font_id.family.clone()));
|
||||
|
||||
ui.separator();
|
||||
|
||||
egui::ScrollArea::vertical().show(ui, |ui| {
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
ui.spacing_mut().item_spacing = egui::Vec2::splat(2.0);
|
||||
|
||||
for (&chr, name) in named_chars {
|
||||
if filter.is_empty() || name.contains(filter) || *filter == chr.to_string() {
|
||||
let button = egui::Button::new(
|
||||
egui::RichText::new(chr.to_string()).font(self.font_id.clone()),
|
||||
)
|
||||
.frame(false);
|
||||
|
||||
let tooltip_ui = |ui: &mut egui::Ui| {
|
||||
ui.label(
|
||||
egui::RichText::new(chr.to_string()).font(self.font_id.clone()),
|
||||
);
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn available_characters(ui: &egui::Ui, family: egui::FontFamily) -> BTreeMap<char, String> {
|
||||
ui.fonts()
|
||||
.lock()
|
||||
.fonts
|
||||
.font(&egui::FontId::new(10.0, family)) // size is arbitrary for getting the characters
|
||||
.characters()
|
||||
.iter()
|
||||
.filter(|chr| !chr.is_whitespace() && !chr.is_ascii_control())
|
||||
.map(|&chr| (chr, char_name(chr)))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn char_name(chr: char) -> String {
|
||||
special_char_name(chr)
|
||||
.map(|s| s.to_owned())
|
||||
.or_else(|| unicode_names2::name(chr).map(|name| name.to_string().to_lowercase()))
|
||||
.unwrap_or_else(|| "unknown".to_owned())
|
||||
}
|
||||
|
||||
fn special_char_name(chr: char) -> Option<&'static str> {
|
||||
#[allow(clippy::match_same_arms)] // many "flag"
|
||||
match chr {
|
||||
// Special private-use-area extensions found in `emoji-icon-font.ttf`:
|
||||
// Private use area extensions:
|
||||
'\u{FE4E5}' => Some("flag japan"),
|
||||
'\u{FE4E6}' => Some("flag usa"),
|
||||
'\u{FE4E7}' => Some("flag"),
|
||||
'\u{FE4E8}' => Some("flag"),
|
||||
'\u{FE4E9}' => Some("flag"),
|
||||
'\u{FE4EA}' => Some("flag great britain"),
|
||||
'\u{FE4EB}' => Some("flag"),
|
||||
'\u{FE4EC}' => Some("flag"),
|
||||
'\u{FE4ED}' => Some("flag"),
|
||||
'\u{FE4EE}' => Some("flag south korea"),
|
||||
'\u{FE82C}' => Some("number sign in square"),
|
||||
'\u{FE82E}' => Some("digit one in square"),
|
||||
'\u{FE82F}' => Some("digit two in square"),
|
||||
'\u{FE830}' => Some("digit three in square"),
|
||||
'\u{FE831}' => Some("digit four in square"),
|
||||
'\u{FE832}' => Some("digit five in square"),
|
||||
'\u{FE833}' => Some("digit six in square"),
|
||||
'\u{FE834}' => Some("digit seven in square"),
|
||||
'\u{FE835}' => Some("digit eight in square"),
|
||||
'\u{FE836}' => Some("digit nine in square"),
|
||||
'\u{FE837}' => Some("digit zero in square"),
|
||||
|
||||
// Special private-use-area extensions found in `emoji-icon-font.ttf`:
|
||||
// Web services / operating systems / browsers
|
||||
'\u{E600}' => Some("web-dribbble"),
|
||||
'\u{E601}' => Some("web-stackoverflow"),
|
||||
'\u{E602}' => Some("web-vimeo"),
|
||||
'\u{E603}' => Some("web-twitter"),
|
||||
'\u{E604}' => Some("web-facebook"),
|
||||
'\u{E605}' => Some("web-googleplus"),
|
||||
'\u{E606}' => Some("web-pinterest"),
|
||||
'\u{E607}' => Some("web-tumblr"),
|
||||
'\u{E608}' => Some("web-linkedin"),
|
||||
'\u{E60A}' => Some("web-stumbleupon"),
|
||||
'\u{E60B}' => Some("web-lastfm"),
|
||||
'\u{E60C}' => Some("web-rdio"),
|
||||
'\u{E60D}' => Some("web-spotify"),
|
||||
'\u{E60E}' => Some("web-qq"),
|
||||
'\u{E60F}' => Some("web-instagram"),
|
||||
'\u{E610}' => Some("web-dropbox"),
|
||||
'\u{E611}' => Some("web-evernote"),
|
||||
'\u{E612}' => Some("web-flattr"),
|
||||
'\u{E613}' => Some("web-skype"),
|
||||
'\u{E614}' => Some("web-renren"),
|
||||
'\u{E615}' => Some("web-sina-weibo"),
|
||||
'\u{E616}' => Some("web-paypal"),
|
||||
'\u{E617}' => Some("web-picasa"),
|
||||
'\u{E618}' => Some("os-android"),
|
||||
'\u{E619}' => Some("web-mixi"),
|
||||
'\u{E61A}' => Some("web-behance"),
|
||||
'\u{E61B}' => Some("web-circles"),
|
||||
'\u{E61C}' => Some("web-vk"),
|
||||
'\u{E61D}' => Some("web-smashing"),
|
||||
'\u{E61E}' => Some("web-forrst"),
|
||||
'\u{E61F}' => Some("os-windows"),
|
||||
'\u{E620}' => Some("web-flickr"),
|
||||
'\u{E621}' => Some("web-picassa"),
|
||||
'\u{E622}' => Some("web-deviantart"),
|
||||
'\u{E623}' => Some("web-steam"),
|
||||
'\u{E624}' => Some("web-github"),
|
||||
'\u{E625}' => Some("web-git"),
|
||||
'\u{E626}' => Some("web-blogger"),
|
||||
'\u{E627}' => Some("web-soundcloud"),
|
||||
'\u{E628}' => Some("web-reddit"),
|
||||
'\u{E629}' => Some("web-delicious"),
|
||||
'\u{E62A}' => Some("browser-chrome"),
|
||||
'\u{E62B}' => Some("browser-firefox"),
|
||||
'\u{E62C}' => Some("browser-ie"),
|
||||
'\u{E62D}' => Some("browser-opera"),
|
||||
'\u{E62E}' => Some("browser-safari"),
|
||||
'\u{E62F}' => Some("web-google-drive"),
|
||||
'\u{E630}' => Some("web-wordpress"),
|
||||
'\u{E631}' => Some("web-joomla"),
|
||||
'\u{E632}' => Some("lastfm"),
|
||||
'\u{E633}' => Some("web-foursquare"),
|
||||
'\u{E634}' => Some("web-yelp"),
|
||||
'\u{E635}' => Some("web-drupal"),
|
||||
'\u{E636}' => Some("youtube"),
|
||||
'\u{F189}' => Some("vk"),
|
||||
'\u{F1A6}' => Some("digg"),
|
||||
'\u{F1CA}' => Some("web-vine"),
|
||||
'\u{F8FF}' => Some("os-apple"),
|
||||
|
||||
// Special private-use-area extensions found in `Ubuntu-Light.ttf`
|
||||
'\u{F000}' => Some("uniF000"),
|
||||
'\u{F001}' => Some("fi"),
|
||||
'\u{F002}' => Some("fl"),
|
||||
'\u{F506}' => Some("one seventh"),
|
||||
'\u{F507}' => Some("two sevenths"),
|
||||
'\u{F508}' => Some("three sevenths"),
|
||||
'\u{F509}' => Some("four sevenths"),
|
||||
'\u{F50A}' => Some("five sevenths"),
|
||||
'\u{F50B}' => Some("six sevenths"),
|
||||
'\u{F50C}' => Some("one ninth"),
|
||||
'\u{F50D}' => Some("two ninths"),
|
||||
'\u{F50E}' => Some("four ninths"),
|
||||
'\u{F50F}' => Some("five ninths"),
|
||||
'\u{F510}' => Some("seven ninths"),
|
||||
'\u{F511}' => Some("eight ninths"),
|
||||
'\u{F800}' => Some("zero.alt"),
|
||||
'\u{F801}' => Some("one.alt"),
|
||||
'\u{F802}' => Some("two.alt"),
|
||||
'\u{F803}' => Some("three.alt"),
|
||||
'\u{F804}' => Some("four.alt"),
|
||||
'\u{F805}' => Some("five.alt"),
|
||||
'\u{F806}' => Some("six.alt"),
|
||||
'\u{F807}' => Some("seven.alt"),
|
||||
'\u{F808}' => Some("eight.alt"),
|
||||
'\u{F809}' => Some("nine.alt"),
|
||||
'\u{F80A}' => Some("zero.sups"),
|
||||
'\u{F80B}' => Some("one.sups"),
|
||||
'\u{F80C}' => Some("two.sups"),
|
||||
'\u{F80D}' => Some("three.sups"),
|
||||
'\u{F80E}' => Some("four.sups"),
|
||||
'\u{F80F}' => Some("five.sups"),
|
||||
'\u{F810}' => Some("six.sups"),
|
||||
'\u{F811}' => Some("seven.sups"),
|
||||
'\u{F812}' => Some("eight.sups"),
|
||||
'\u{F813}' => Some("nine.sups"),
|
||||
'\u{F814}' => Some("zero.sinf"),
|
||||
'\u{F815}' => Some("one.sinf"),
|
||||
'\u{F816}' => Some("two.sinf"),
|
||||
'\u{F817}' => Some("three.sinf"),
|
||||
'\u{F818}' => Some("four.sinf"),
|
||||
'\u{F819}' => Some("five.sinf"),
|
||||
'\u{F81A}' => Some("six.sinf"),
|
||||
'\u{F81B}' => Some("seven.sinf"),
|
||||
'\u{F81C}' => Some("eight.sinf"),
|
||||
'\u{F81D}' => Some("nine.sinf"),
|
||||
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
180
crates/egui_demo_lib/src/demo/layout_test.rs
Normal file
180
crates/egui_demo_lib/src/demo/layout_test.rs
Normal file
@@ -0,0 +1,180 @@
|
||||
use egui::*;
|
||||
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
pub struct LayoutTest {
|
||||
// Identical to contents of `egui::Layout`
|
||||
layout: LayoutSettings,
|
||||
|
||||
// Extra for testing wrapping:
|
||||
wrap_column_width: f32,
|
||||
wrap_row_height: f32,
|
||||
}
|
||||
|
||||
impl Default for LayoutTest {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
layout: LayoutSettings::top_down(),
|
||||
wrap_column_width: 150.0,
|
||||
wrap_row_height: 20.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
pub struct LayoutSettings {
|
||||
// Similar to the contents of `egui::Layout`
|
||||
main_dir: Direction,
|
||||
main_wrap: bool,
|
||||
cross_align: Align,
|
||||
cross_justify: bool,
|
||||
}
|
||||
|
||||
impl Default for LayoutSettings {
|
||||
fn default() -> Self {
|
||||
Self::top_down()
|
||||
}
|
||||
}
|
||||
|
||||
impl LayoutSettings {
|
||||
fn top_down() -> Self {
|
||||
Self {
|
||||
main_dir: Direction::TopDown,
|
||||
main_wrap: false,
|
||||
cross_align: Align::Min,
|
||||
cross_justify: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn top_down_justified_centered() -> Self {
|
||||
Self {
|
||||
main_dir: Direction::TopDown,
|
||||
main_wrap: false,
|
||||
cross_align: Align::Center,
|
||||
cross_justify: true,
|
||||
}
|
||||
}
|
||||
|
||||
fn horizontal_wrapped() -> Self {
|
||||
Self {
|
||||
main_dir: Direction::LeftToRight,
|
||||
main_wrap: true,
|
||||
cross_align: Align::Center,
|
||||
cross_justify: false,
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
impl super::Demo for LayoutTest {
|
||||
fn name(&self) -> &'static str {
|
||||
"Layout Test"
|
||||
}
|
||||
|
||||
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
|
||||
egui::Window::new(self.name())
|
||||
.open(open)
|
||||
.resizable(false)
|
||||
.show(ctx, |ui| {
|
||||
use super::View as _;
|
||||
self.ui(ui);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl super::View for LayoutTest {
|
||||
fn ui(&mut self, ui: &mut Ui) {
|
||||
ui.label("Tests and demonstrates the limits of the egui layouts");
|
||||
self.content_ui(ui);
|
||||
Resize::default()
|
||||
.default_size([150.0, 200.0])
|
||||
.show(ui, |ui| {
|
||||
if self.layout.main_wrap {
|
||||
if self.layout.main_dir.is_horizontal() {
|
||||
ui.allocate_ui(
|
||||
vec2(ui.available_size_before_wrap().x, self.wrap_row_height),
|
||||
|ui| ui.with_layout(self.layout.layout(), demo_ui),
|
||||
);
|
||||
} else {
|
||||
ui.allocate_ui(
|
||||
vec2(self.wrap_column_width, ui.available_size_before_wrap().y),
|
||||
|ui| ui.with_layout(self.layout.layout(), demo_ui),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
ui.with_layout(self.layout.layout(), demo_ui);
|
||||
}
|
||||
});
|
||||
ui.label("Resize to see effect");
|
||||
}
|
||||
}
|
||||
|
||||
impl LayoutTest {
|
||||
pub fn content_ui(&mut self, ui: &mut Ui) {
|
||||
ui.horizontal(|ui| {
|
||||
ui.selectable_value(&mut self.layout, LayoutSettings::top_down(), "Top-down");
|
||||
ui.selectable_value(
|
||||
&mut self.layout,
|
||||
LayoutSettings::top_down_justified_centered(),
|
||||
"Top-down, centered and justified",
|
||||
);
|
||||
ui.selectable_value(
|
||||
&mut self.layout,
|
||||
LayoutSettings::horizontal_wrapped(),
|
||||
"Horizontal wrapped",
|
||||
);
|
||||
});
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Main Direction:");
|
||||
for &dir in &[
|
||||
Direction::LeftToRight,
|
||||
Direction::RightToLeft,
|
||||
Direction::TopDown,
|
||||
Direction::BottomUp,
|
||||
] {
|
||||
ui.radio_value(&mut self.layout.main_dir, dir, format!("{:?}", dir));
|
||||
}
|
||||
});
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.checkbox(&mut self.layout.main_wrap, "Main wrap")
|
||||
.on_hover_text("Wrap when next widget doesn't fit the current row/column");
|
||||
|
||||
if self.layout.main_wrap {
|
||||
if self.layout.main_dir.is_horizontal() {
|
||||
ui.add(Slider::new(&mut self.wrap_row_height, 0.0..=200.0).text("Row height"));
|
||||
} else {
|
||||
ui.add(
|
||||
Slider::new(&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.layout.cross_align, align, format!("{:?}", align));
|
||||
}
|
||||
});
|
||||
|
||||
ui.checkbox(&mut self.layout.cross_justify, "Cross Justified")
|
||||
.on_hover_text("Try to fill full width/height (e.g. buttons)");
|
||||
}
|
||||
}
|
||||
|
||||
fn demo_ui(ui: &mut Ui) {
|
||||
ui.add(egui::Label::new("Wrapping text followed by example widgets:").wrap(true));
|
||||
let mut dummy = false;
|
||||
ui.checkbox(&mut dummy, "checkbox");
|
||||
ui.radio_value(&mut dummy, false, "radio");
|
||||
let _ = ui.button("button");
|
||||
}
|
||||
663
crates/egui_demo_lib/src/demo/misc_demo_window.rs
Normal file
663
crates/egui_demo_lib/src/demo/misc_demo_window.rs
Normal file
@@ -0,0 +1,663 @@
|
||||
use super::*;
|
||||
use crate::LOREM_IPSUM;
|
||||
use egui::{color::*, epaint::text::TextWrapping, *};
|
||||
|
||||
/// Showcase some ui code
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
pub struct MiscDemoWindow {
|
||||
num_columns: usize,
|
||||
|
||||
break_anywhere: bool,
|
||||
max_rows: usize,
|
||||
overflow_character: Option<char>,
|
||||
|
||||
widgets: Widgets,
|
||||
colors: ColorWidgets,
|
||||
custom_collapsing_header: CustomCollapsingHeader,
|
||||
tree: Tree,
|
||||
box_painting: BoxPainting,
|
||||
|
||||
dummy_bool: bool,
|
||||
dummy_usize: usize,
|
||||
}
|
||||
|
||||
impl Default for MiscDemoWindow {
|
||||
fn default() -> MiscDemoWindow {
|
||||
MiscDemoWindow {
|
||||
num_columns: 2,
|
||||
|
||||
max_rows: 2,
|
||||
break_anywhere: false,
|
||||
overflow_character: Some('…'),
|
||||
|
||||
widgets: Default::default(),
|
||||
colors: Default::default(),
|
||||
custom_collapsing_header: Default::default(),
|
||||
tree: Tree::demo(),
|
||||
box_painting: Default::default(),
|
||||
|
||||
dummy_bool: false,
|
||||
dummy_usize: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Demo for MiscDemoWindow {
|
||||
fn name(&self) -> &'static str {
|
||||
"✨ Misc Demos"
|
||||
}
|
||||
|
||||
fn show(&mut self, ctx: &Context, open: &mut bool) {
|
||||
Window::new(self.name())
|
||||
.open(open)
|
||||
.vscroll(true)
|
||||
.hscroll(true)
|
||||
.show(ctx, |ui| self.ui(ui));
|
||||
}
|
||||
}
|
||||
|
||||
impl View for MiscDemoWindow {
|
||||
fn ui(&mut self, ui: &mut Ui) {
|
||||
ui.set_min_width(250.0);
|
||||
|
||||
CollapsingHeader::new("Widgets")
|
||||
.default_open(true)
|
||||
.show(ui, |ui| {
|
||||
self.widgets.ui(ui);
|
||||
});
|
||||
|
||||
CollapsingHeader::new("Text layout")
|
||||
.default_open(false)
|
||||
.show(ui, |ui| {
|
||||
text_layout_ui(
|
||||
ui,
|
||||
&mut self.max_rows,
|
||||
&mut self.break_anywhere,
|
||||
&mut self.overflow_character,
|
||||
);
|
||||
});
|
||||
|
||||
CollapsingHeader::new("Colors")
|
||||
.default_open(false)
|
||||
.show(ui, |ui| {
|
||||
self.colors.ui(ui);
|
||||
});
|
||||
|
||||
CollapsingHeader::new("Custom Collapsing Header")
|
||||
.default_open(false)
|
||||
.show(ui, |ui| self.custom_collapsing_header.ui(ui));
|
||||
|
||||
CollapsingHeader::new("Tree")
|
||||
.default_open(false)
|
||||
.show(ui, |ui| self.tree.ui(ui));
|
||||
|
||||
CollapsingHeader::new("Checkboxes")
|
||||
.default_open(false)
|
||||
.show(ui, |ui| {
|
||||
ui.label("Checkboxes with empty labels take up very little space:");
|
||||
ui.spacing_mut().item_spacing = Vec2::ZERO;
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
for _ in 0..64 {
|
||||
ui.checkbox(&mut self.dummy_bool, "");
|
||||
}
|
||||
});
|
||||
ui.checkbox(&mut self.dummy_bool, "checkbox");
|
||||
|
||||
ui.label("Radiobuttons are similar:");
|
||||
ui.spacing_mut().item_spacing = Vec2::ZERO;
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
for i in 0..64 {
|
||||
ui.radio_value(&mut self.dummy_usize, i, "");
|
||||
}
|
||||
});
|
||||
ui.radio_value(&mut self.dummy_usize, 64, "radio_value");
|
||||
});
|
||||
|
||||
ui.collapsing("Columns", |ui| {
|
||||
ui.add(Slider::new(&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("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 size = Vec2::splat(16.0);
|
||||
let (response, painter) = ui.allocate_painter(size, Sense::hover());
|
||||
let rect = response.rect;
|
||||
let c = rect.center();
|
||||
let r = rect.width() / 2.0 - 1.0;
|
||||
let color = Color32::from_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);
|
||||
});
|
||||
});
|
||||
|
||||
CollapsingHeader::new("Many circles of different sizes")
|
||||
.default_open(false)
|
||||
.show(ui, |ui| {
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
for i in 0..100 {
|
||||
let r = i as f32 * 0.5;
|
||||
let size = Vec2::splat(2.0 * r + 5.0);
|
||||
let (rect, _response) = ui.allocate_at_least(size, Sense::hover());
|
||||
ui.painter()
|
||||
.circle_filled(rect.center(), r, ui.visuals().text_color());
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
pub struct Widgets {
|
||||
angle: f32,
|
||||
password: String,
|
||||
}
|
||||
|
||||
impl Default for Widgets {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
angle: std::f32::consts::TAU / 3.0,
|
||||
password: "hunter2".to_owned(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Widgets {
|
||||
pub fn ui(&mut self, ui: &mut Ui) {
|
||||
let Self { angle, password } = self;
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.add(crate::egui_github_link_file_line!());
|
||||
});
|
||||
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
// Trick so we don't have to add spaces in the text below:
|
||||
let width = ui.fonts().glyph_width(&TextStyle::Body.resolve(ui.style()), ' ');
|
||||
ui.spacing_mut().item_spacing.x = width;
|
||||
|
||||
ui.label(RichText::new("Text can have").color(Color32::from_rgb(110, 255, 110)));
|
||||
ui.colored_label(Color32::from_rgb(128, 140, 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.separator();
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("An angle:");
|
||||
ui.drag_angle(angle);
|
||||
ui.label(format!("≈ {:.3}τ", *angle / std::f32::consts::TAU))
|
||||
.on_hover_text("Each τ represents one turn (τ = 2π)");
|
||||
})
|
||||
.response
|
||||
.on_hover_text("The angle is stored in radians, but presented in degrees");
|
||||
|
||||
ui.separator();
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.hyperlink_to("Password:", super::password::url_to_file_source_code())
|
||||
.on_hover_text("See the example code for how to use egui to store UI state");
|
||||
ui.add(super::password::password(password));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[derive(PartialEq)]
|
||||
#[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) {
|
||||
egui::reset_button(ui, self);
|
||||
|
||||
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,
|
||||
rounding: f32,
|
||||
stroke_width: f32,
|
||||
num_boxes: usize,
|
||||
}
|
||||
|
||||
impl Default for BoxPainting {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
size: vec2(64.0, 32.0),
|
||||
rounding: 5.0,
|
||||
stroke_width: 2.0,
|
||||
num_boxes: 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BoxPainting {
|
||||
pub fn ui(&mut self, ui: &mut Ui) {
|
||||
ui.add(Slider::new(&mut self.size.x, 0.0..=500.0).text("width"));
|
||||
ui.add(Slider::new(&mut self.size.y, 0.0..=500.0).text("height"));
|
||||
ui.add(Slider::new(&mut self.rounding, 0.0..=50.0).text("rounding"));
|
||||
ui.add(Slider::new(&mut self.stroke_width, 0.0..=10.0).text("stroke_width"));
|
||||
ui.add(Slider::new(&mut self.num_boxes, 0..=8).text("num_boxes"));
|
||||
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
for _ in 0..self.num_boxes {
|
||||
let (rect, _response) = ui.allocate_at_least(self.size, Sense::hover());
|
||||
ui.painter().rect(
|
||||
rect,
|
||||
self.rounding,
|
||||
Color32::from_gray(64),
|
||||
Stroke::new(self.stroke_width, Color32::WHITE),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
struct CustomCollapsingHeader {
|
||||
selected: bool,
|
||||
radio_value: bool,
|
||||
}
|
||||
|
||||
impl Default for CustomCollapsingHeader {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
selected: true,
|
||||
radio_value: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CustomCollapsingHeader {
|
||||
pub fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
ui.label("Example of a collapsing header with custom header:");
|
||||
|
||||
let id = ui.make_persistent_id("my_collapsing_header");
|
||||
egui::collapsing_header::CollapsingState::load_with_default_open(ui.ctx(), id, true)
|
||||
.show_header(ui, |ui| {
|
||||
ui.toggle_value(&mut self.selected, "Click to select/unselect");
|
||||
ui.radio_value(&mut self.radio_value, false, "");
|
||||
ui.radio_value(&mut self.radio_value, true, "");
|
||||
})
|
||||
.body(|ui| {
|
||||
ui.label("The body is always custom");
|
||||
});
|
||||
|
||||
CollapsingHeader::new("Normal collapsing header for comparison").show(ui, |ui| {
|
||||
ui.label("Nothing exciting here");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[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")
|
||||
}
|
||||
}
|
||||
|
||||
impl Tree {
|
||||
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
|
||||
.button(RichText::new("delete").color(ui.visuals().warn_fg_color))
|
||||
.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
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
fn text_layout_ui(
|
||||
ui: &mut egui::Ui,
|
||||
max_rows: &mut usize,
|
||||
break_anywhere: &mut bool,
|
||||
overflow_character: &mut Option<char>,
|
||||
) {
|
||||
use egui::text::LayoutJob;
|
||||
|
||||
let mut job = LayoutJob::default();
|
||||
|
||||
let first_row_indentation = 10.0;
|
||||
|
||||
let (default_color, strong_color) = if ui.visuals().dark_mode {
|
||||
(Color32::LIGHT_GRAY, Color32::WHITE)
|
||||
} else {
|
||||
(Color32::DARK_GRAY, Color32::BLACK)
|
||||
};
|
||||
|
||||
job.append(
|
||||
"This is a demonstration of ",
|
||||
first_row_indentation,
|
||||
TextFormat {
|
||||
color: default_color,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
job.append(
|
||||
"the egui text layout engine. ",
|
||||
0.0,
|
||||
TextFormat {
|
||||
color: strong_color,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
job.append(
|
||||
"It supports ",
|
||||
0.0,
|
||||
TextFormat {
|
||||
color: default_color,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
job.append(
|
||||
"different ",
|
||||
0.0,
|
||||
TextFormat {
|
||||
color: Color32::from_rgb(110, 255, 110),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
job.append(
|
||||
"colors, ",
|
||||
0.0,
|
||||
TextFormat {
|
||||
color: Color32::from_rgb(128, 140, 255),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
job.append(
|
||||
"backgrounds, ",
|
||||
0.0,
|
||||
TextFormat {
|
||||
color: default_color,
|
||||
background: Color32::from_rgb(128, 32, 32),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
job.append(
|
||||
"mixing ",
|
||||
0.0,
|
||||
TextFormat {
|
||||
font_id: FontId::proportional(20.0),
|
||||
color: default_color,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
job.append(
|
||||
"fonts, ",
|
||||
0.0,
|
||||
TextFormat {
|
||||
font_id: FontId::monospace(14.0),
|
||||
color: default_color,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
job.append(
|
||||
"raised text, ",
|
||||
0.0,
|
||||
TextFormat {
|
||||
font_id: FontId::proportional(8.0),
|
||||
color: default_color,
|
||||
valign: Align::TOP,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
job.append(
|
||||
"with ",
|
||||
0.0,
|
||||
TextFormat {
|
||||
color: default_color,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
job.append(
|
||||
"underlining",
|
||||
0.0,
|
||||
TextFormat {
|
||||
color: default_color,
|
||||
underline: Stroke::new(1.0, Color32::LIGHT_BLUE),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
job.append(
|
||||
" and ",
|
||||
0.0,
|
||||
TextFormat {
|
||||
color: default_color,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
job.append(
|
||||
"strikethrough",
|
||||
0.0,
|
||||
TextFormat {
|
||||
color: default_color,
|
||||
strikethrough: Stroke::new(2.0, Color32::RED.linear_multiply(0.5)),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
job.append(
|
||||
". Of course, ",
|
||||
0.0,
|
||||
TextFormat {
|
||||
color: default_color,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
job.append(
|
||||
"you can",
|
||||
0.0,
|
||||
TextFormat {
|
||||
color: default_color,
|
||||
strikethrough: Stroke::new(1.0, strong_color),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
job.append(
|
||||
" mix these!",
|
||||
0.0,
|
||||
TextFormat {
|
||||
font_id: FontId::proportional(8.0),
|
||||
color: Color32::LIGHT_BLUE,
|
||||
background: Color32::from_rgb(128, 0, 0),
|
||||
underline: Stroke::new(1.0, strong_color),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
ui.label(job);
|
||||
|
||||
ui.separator();
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.add(DragValue::new(max_rows));
|
||||
ui.label("Max rows");
|
||||
});
|
||||
ui.checkbox(break_anywhere, "Break anywhere");
|
||||
ui.horizontal(|ui| {
|
||||
ui.selectable_value(overflow_character, None, "None");
|
||||
ui.selectable_value(overflow_character, Some('…'), "…");
|
||||
ui.selectable_value(overflow_character, Some('—'), "—");
|
||||
ui.selectable_value(overflow_character, Some('-'), " - ");
|
||||
ui.label("Overflow character");
|
||||
});
|
||||
|
||||
let mut job = LayoutJob::single_section(LOREM_IPSUM.to_owned(), TextFormat::default());
|
||||
job.wrap = TextWrapping {
|
||||
max_rows: *max_rows,
|
||||
break_anywhere: *break_anywhere,
|
||||
overflow_character: *overflow_character,
|
||||
..Default::default()
|
||||
};
|
||||
ui.label(job);
|
||||
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.add(crate::egui_github_link_file_line!());
|
||||
});
|
||||
}
|
||||
52
crates/egui_demo_lib/src/demo/mod.rs
Normal file
52
crates/egui_demo_lib/src/demo/mod.rs
Normal file
@@ -0,0 +1,52 @@
|
||||
//! Demo-code for showing how egui is used.
|
||||
//!
|
||||
//! The demo-code is also used in benchmarks and tests.
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
pub mod about;
|
||||
pub mod code_editor;
|
||||
pub mod code_example;
|
||||
pub mod context_menu;
|
||||
pub mod dancing_strings;
|
||||
pub mod demo_app_windows;
|
||||
pub mod drag_and_drop;
|
||||
pub mod font_book;
|
||||
pub mod layout_test;
|
||||
pub mod misc_demo_window;
|
||||
pub mod multi_touch;
|
||||
pub mod paint_bezier;
|
||||
pub mod painting;
|
||||
pub mod password;
|
||||
pub mod plot_demo;
|
||||
pub mod scrolling;
|
||||
pub mod sliders;
|
||||
pub mod strip_demo;
|
||||
pub mod table_demo;
|
||||
pub mod tests;
|
||||
pub mod text_edit;
|
||||
pub mod toggle_switch;
|
||||
pub mod widget_gallery;
|
||||
pub mod window_options;
|
||||
pub mod window_with_panels;
|
||||
|
||||
pub use {
|
||||
about::About, demo_app_windows::DemoWindows, misc_demo_window::MiscDemoWindow,
|
||||
widget_gallery::WidgetGallery,
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// Something to view in the demo windows
|
||||
pub trait View {
|
||||
fn ui(&mut self, ui: &mut egui::Ui);
|
||||
}
|
||||
|
||||
/// Something to view
|
||||
pub trait Demo {
|
||||
/// `&'static` so we can also use it as a key to store open/close state.
|
||||
fn name(&self) -> &'static str;
|
||||
|
||||
/// Show windows, etc
|
||||
fn show(&mut self, ctx: &egui::Context, open: &mut bool);
|
||||
}
|
||||
145
crates/egui_demo_lib/src/demo/multi_touch.rs
Normal file
145
crates/egui_demo_lib/src/demo/multi_touch.rs
Normal file
@@ -0,0 +1,145 @@
|
||||
use egui::{
|
||||
emath::{RectTransform, Rot2},
|
||||
vec2, Color32, Frame, Pos2, Rect, Sense, Stroke, Vec2,
|
||||
};
|
||||
|
||||
pub struct MultiTouch {
|
||||
rotation: f32,
|
||||
translation: Vec2,
|
||||
zoom: f32,
|
||||
last_touch_time: f64,
|
||||
}
|
||||
|
||||
impl Default for MultiTouch {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
rotation: 0.,
|
||||
translation: Vec2::ZERO,
|
||||
zoom: 1.,
|
||||
last_touch_time: 0.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl super::Demo for MultiTouch {
|
||||
fn name(&self) -> &'static str {
|
||||
"👌 Multi Touch"
|
||||
}
|
||||
|
||||
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
|
||||
egui::Window::new(self.name())
|
||||
.open(open)
|
||||
.default_size(vec2(512.0, 512.0))
|
||||
.resizable(true)
|
||||
.show(ctx, |ui| {
|
||||
use super::View as _;
|
||||
self.ui(ui);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl super::View for MultiTouch {
|
||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.add(crate::egui_github_link_file!());
|
||||
});
|
||||
ui.strong(
|
||||
"This demo only works on devices with multitouch support (e.g. mobiles and tablets).",
|
||||
);
|
||||
ui.separator();
|
||||
ui.label("Try touch gestures Pinch/Stretch, Rotation, and Pressure with 2+ fingers.");
|
||||
|
||||
let num_touches = ui.input().multi_touch().map_or(0, |mt| mt.num_touches);
|
||||
ui.label(format!("Current touches: {}", num_touches));
|
||||
|
||||
let color = if ui.visuals().dark_mode {
|
||||
Color32::WHITE
|
||||
} else {
|
||||
Color32::BLACK
|
||||
};
|
||||
|
||||
Frame::canvas(ui.style()).show(ui, |ui| {
|
||||
// Note that we use `Sense::drag()` although we do not use any pointer events. With
|
||||
// the current implementation, the fact that a touch event of two or more fingers is
|
||||
// recognized, does not mean that the pointer events are suppressed, which are always
|
||||
// generated for the first finger. Therefore, if we do not explicitly consume pointer
|
||||
// events, the window will move around, not only when dragged with a single finger, but
|
||||
// also when a two-finger touch is active. I guess this problem can only be cleanly
|
||||
// solved when the synthetic pointer events are created by egui, and not by the
|
||||
// backend.
|
||||
|
||||
// set up the drawing canvas with normalized coordinates:
|
||||
let (response, painter) =
|
||||
ui.allocate_painter(ui.available_size_before_wrap(), Sense::drag());
|
||||
|
||||
// normalize painter coordinates to ±1 units in each direction with [0,0] in the center:
|
||||
let painter_proportions = response.rect.square_proportions();
|
||||
let to_screen = RectTransform::from_to(
|
||||
Rect::from_min_size(Pos2::ZERO - painter_proportions, 2. * painter_proportions),
|
||||
response.rect,
|
||||
);
|
||||
|
||||
// check for touch input (or the lack thereof) and update zoom and scale factors, plus
|
||||
// color and width:
|
||||
let mut stroke_width = 1.;
|
||||
if let Some(multi_touch) = ui.ctx().multi_touch() {
|
||||
// This adjusts the current zoom factor and rotation angle according to the dynamic
|
||||
// change (for the current frame) of the touch gesture:
|
||||
self.zoom *= multi_touch.zoom_delta;
|
||||
self.rotation += multi_touch.rotation_delta;
|
||||
// the translation we get from `multi_touch` needs to be scaled down to the
|
||||
// normalized coordinates we use as the basis for painting:
|
||||
self.translation += to_screen.inverse().scale() * multi_touch.translation_delta;
|
||||
// touch pressure will make the arrow thicker (not all touch devices support this):
|
||||
stroke_width += 10. * multi_touch.force;
|
||||
|
||||
self.last_touch_time = ui.input().time;
|
||||
} else {
|
||||
self.slowly_reset(ui);
|
||||
}
|
||||
let zoom_and_rotate = self.zoom * Rot2::from_angle(self.rotation);
|
||||
let arrow_start_offset = self.translation + zoom_and_rotate * vec2(-0.5, 0.5);
|
||||
|
||||
// Paints an arrow pointing from bottom-left (-0.5, 0.5) to top-right (0.5, -0.5), but
|
||||
// scaled, rotated, and translated according to the current touch gesture:
|
||||
let arrow_start = Pos2::ZERO + arrow_start_offset;
|
||||
let arrow_direction = zoom_and_rotate * vec2(1., -1.);
|
||||
painter.arrow(
|
||||
to_screen * arrow_start,
|
||||
to_screen.scale() * arrow_direction,
|
||||
Stroke::new(stroke_width, color),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl MultiTouch {
|
||||
fn slowly_reset(&mut self, ui: &egui::Ui) {
|
||||
// This has nothing to do with the touch gesture. It just smoothly brings the
|
||||
// painted arrow back into its original position, for a nice visual effect:
|
||||
|
||||
let time_since_last_touch = (ui.input().time - self.last_touch_time) as f32;
|
||||
|
||||
let delay = 0.5;
|
||||
if time_since_last_touch < delay {
|
||||
ui.ctx().request_repaint();
|
||||
} else {
|
||||
// seconds after which half the amount of zoom/rotation will be reverted:
|
||||
let half_life =
|
||||
egui::remap_clamp(time_since_last_touch, delay..=1.0, 1.0..=0.0).powf(4.0);
|
||||
|
||||
if half_life <= 1e-3 {
|
||||
self.zoom = 1.0;
|
||||
self.rotation = 0.0;
|
||||
self.translation = Vec2::ZERO;
|
||||
} else {
|
||||
let dt = ui.input().unstable_dt;
|
||||
let half_life_factor = (-(2_f32.ln()) / half_life * dt).exp();
|
||||
self.zoom = 1. + ((self.zoom - 1.) * half_life_factor);
|
||||
self.rotation *= half_life_factor;
|
||||
self.translation *= half_life_factor;
|
||||
ui.ctx().request_repaint();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
172
crates/egui_demo_lib/src/demo/paint_bezier.rs
Normal file
172
crates/egui_demo_lib/src/demo/paint_bezier.rs
Normal file
@@ -0,0 +1,172 @@
|
||||
use egui::epaint::{CubicBezierShape, PathShape, QuadraticBezierShape};
|
||||
use egui::*;
|
||||
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
pub struct PaintBezier {
|
||||
/// Bézier curve degree, it can be 3, 4.
|
||||
degree: usize,
|
||||
|
||||
/// The control points. The [`Self::degree`] first of them are used.
|
||||
control_points: [Pos2; 4],
|
||||
|
||||
/// Stroke for Bézier curve.
|
||||
stroke: Stroke,
|
||||
|
||||
/// Fill for Bézier curve.
|
||||
fill: Color32,
|
||||
|
||||
/// Stroke for auxiliary lines.
|
||||
aux_stroke: Stroke,
|
||||
|
||||
bounding_box_stroke: Stroke,
|
||||
}
|
||||
|
||||
impl Default for PaintBezier {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
degree: 4,
|
||||
control_points: [
|
||||
pos2(50.0, 50.0),
|
||||
pos2(60.0, 250.0),
|
||||
pos2(200.0, 200.0),
|
||||
pos2(250.0, 50.0),
|
||||
],
|
||||
stroke: Stroke::new(1.0, Color32::from_rgb(25, 200, 100)),
|
||||
fill: Color32::from_rgb(50, 100, 150).linear_multiply(0.25),
|
||||
aux_stroke: Stroke::new(1.0, Color32::RED.linear_multiply(0.25)),
|
||||
bounding_box_stroke: Stroke::new(0.0, Color32::LIGHT_GREEN.linear_multiply(0.25)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PaintBezier {
|
||||
pub fn ui_control(&mut self, ui: &mut egui::Ui) {
|
||||
ui.collapsing("Colors", |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Fill color:");
|
||||
ui.color_edit_button_srgba(&mut self.fill);
|
||||
});
|
||||
egui::stroke_ui(ui, &mut self.stroke, "Curve Stroke");
|
||||
egui::stroke_ui(ui, &mut self.aux_stroke, "Auxiliary Stroke");
|
||||
egui::stroke_ui(ui, &mut self.bounding_box_stroke, "Bounding Box Stroke");
|
||||
});
|
||||
|
||||
ui.collapsing("Global tessellation options", |ui| {
|
||||
let mut tessellation_options = *(ui.ctx().tessellation_options());
|
||||
let tessellation_options = &mut tessellation_options;
|
||||
tessellation_options.ui(ui);
|
||||
let mut new_tessellation_options = ui.ctx().tessellation_options();
|
||||
*new_tessellation_options = *tessellation_options;
|
||||
});
|
||||
|
||||
ui.radio_value(&mut self.degree, 3, "Quadratic Bézier");
|
||||
ui.radio_value(&mut self.degree, 4, "Cubic Bézier");
|
||||
ui.label("Move the points by dragging them.");
|
||||
ui.small("Only convex curves can be accurately filled.");
|
||||
}
|
||||
|
||||
pub fn ui_content(&mut self, ui: &mut Ui) -> egui::Response {
|
||||
let (response, painter) =
|
||||
ui.allocate_painter(Vec2::new(ui.available_width(), 300.0), Sense::hover());
|
||||
|
||||
let to_screen = emath::RectTransform::from_to(
|
||||
Rect::from_min_size(Pos2::ZERO, response.rect.size()),
|
||||
response.rect,
|
||||
);
|
||||
|
||||
let control_point_radius = 8.0;
|
||||
|
||||
let mut control_point_shapes = vec![];
|
||||
|
||||
for (i, point) in self.control_points.iter_mut().enumerate().take(self.degree) {
|
||||
let size = Vec2::splat(2.0 * control_point_radius);
|
||||
|
||||
let point_in_screen = to_screen.transform_pos(*point);
|
||||
let point_rect = Rect::from_center_size(point_in_screen, size);
|
||||
let point_id = response.id.with(i);
|
||||
let point_response = ui.interact(point_rect, point_id, Sense::drag());
|
||||
|
||||
*point += point_response.drag_delta();
|
||||
*point = to_screen.from().clamp(*point);
|
||||
|
||||
let point_in_screen = to_screen.transform_pos(*point);
|
||||
let stroke = ui.style().interact(&point_response).fg_stroke;
|
||||
|
||||
control_point_shapes.push(Shape::circle_stroke(
|
||||
point_in_screen,
|
||||
control_point_radius,
|
||||
stroke,
|
||||
));
|
||||
}
|
||||
|
||||
let points_in_screen: Vec<Pos2> = self
|
||||
.control_points
|
||||
.iter()
|
||||
.take(self.degree)
|
||||
.map(|p| to_screen * *p)
|
||||
.collect();
|
||||
|
||||
match self.degree {
|
||||
3 => {
|
||||
let points = points_in_screen.clone().try_into().unwrap();
|
||||
let shape =
|
||||
QuadraticBezierShape::from_points_stroke(points, true, self.fill, self.stroke);
|
||||
painter.add(epaint::RectShape::stroke(
|
||||
shape.visual_bounding_rect(),
|
||||
0.0,
|
||||
self.bounding_box_stroke,
|
||||
));
|
||||
painter.add(shape);
|
||||
}
|
||||
4 => {
|
||||
let points = points_in_screen.clone().try_into().unwrap();
|
||||
let shape =
|
||||
CubicBezierShape::from_points_stroke(points, true, self.fill, self.stroke);
|
||||
painter.add(epaint::RectShape::stroke(
|
||||
shape.visual_bounding_rect(),
|
||||
0.0,
|
||||
self.bounding_box_stroke,
|
||||
));
|
||||
painter.add(shape);
|
||||
}
|
||||
_ => {
|
||||
unreachable!();
|
||||
}
|
||||
};
|
||||
|
||||
painter.add(PathShape::line(points_in_screen, self.aux_stroke));
|
||||
painter.extend(control_point_shapes);
|
||||
|
||||
response
|
||||
}
|
||||
}
|
||||
|
||||
impl super::Demo for PaintBezier {
|
||||
fn name(&self) -> &'static str {
|
||||
") Bézier Curve"
|
||||
}
|
||||
|
||||
fn show(&mut self, ctx: &Context, open: &mut bool) {
|
||||
use super::View as _;
|
||||
Window::new(self.name())
|
||||
.open(open)
|
||||
.vscroll(false)
|
||||
.resizable(false)
|
||||
.default_size([300.0, 350.0])
|
||||
.show(ctx, |ui| self.ui(ui));
|
||||
}
|
||||
}
|
||||
|
||||
impl super::View for PaintBezier {
|
||||
fn ui(&mut self, ui: &mut Ui) {
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.add(crate::egui_github_link_file!());
|
||||
});
|
||||
self.ui_control(ui);
|
||||
|
||||
Frame::canvas(ui.style()).show(ui, |ui| {
|
||||
self.ui_content(ui);
|
||||
});
|
||||
}
|
||||
}
|
||||
98
crates/egui_demo_lib/src/demo/painting.rs
Normal file
98
crates/egui_demo_lib/src/demo/painting.rs
Normal file
@@ -0,0 +1,98 @@
|
||||
use egui::*;
|
||||
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
pub struct Painting {
|
||||
/// in 0-1 normalized coordinates
|
||||
lines: Vec<Vec<Pos2>>,
|
||||
stroke: Stroke,
|
||||
}
|
||||
|
||||
impl Default for Painting {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
lines: Default::default(),
|
||||
stroke: Stroke::new(1.0, Color32::from_rgb(25, 200, 100)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Painting {
|
||||
pub fn ui_control(&mut self, ui: &mut egui::Ui) -> egui::Response {
|
||||
ui.horizontal(|ui| {
|
||||
egui::stroke_ui(ui, &mut self.stroke, "Stroke");
|
||||
ui.separator();
|
||||
if ui.button("Clear Painting").clicked() {
|
||||
self.lines.clear();
|
||||
}
|
||||
})
|
||||
.response
|
||||
}
|
||||
|
||||
pub fn ui_content(&mut self, ui: &mut Ui) -> egui::Response {
|
||||
let (mut response, painter) =
|
||||
ui.allocate_painter(ui.available_size_before_wrap(), Sense::drag());
|
||||
|
||||
let to_screen = emath::RectTransform::from_to(
|
||||
Rect::from_min_size(Pos2::ZERO, response.rect.square_proportions()),
|
||||
response.rect,
|
||||
);
|
||||
let from_screen = to_screen.inverse();
|
||||
|
||||
if self.lines.is_empty() {
|
||||
self.lines.push(vec![]);
|
||||
}
|
||||
|
||||
let current_line = self.lines.last_mut().unwrap();
|
||||
|
||||
if let Some(pointer_pos) = response.interact_pointer_pos() {
|
||||
let canvas_pos = from_screen * pointer_pos;
|
||||
if current_line.last() != Some(&canvas_pos) {
|
||||
current_line.push(canvas_pos);
|
||||
response.mark_changed();
|
||||
}
|
||||
} else if !current_line.is_empty() {
|
||||
self.lines.push(vec![]);
|
||||
response.mark_changed();
|
||||
}
|
||||
|
||||
let mut shapes = vec![];
|
||||
for line in &self.lines {
|
||||
if line.len() >= 2 {
|
||||
let points: Vec<Pos2> = line.iter().map(|p| to_screen * *p).collect();
|
||||
shapes.push(egui::Shape::line(points, self.stroke));
|
||||
}
|
||||
}
|
||||
painter.extend(shapes);
|
||||
|
||||
response
|
||||
}
|
||||
}
|
||||
|
||||
impl super::Demo for Painting {
|
||||
fn name(&self) -> &'static str {
|
||||
"🖊 Painting"
|
||||
}
|
||||
|
||||
fn show(&mut self, ctx: &Context, open: &mut bool) {
|
||||
use super::View as _;
|
||||
Window::new(self.name())
|
||||
.open(open)
|
||||
.default_size(vec2(512.0, 512.0))
|
||||
.vscroll(false)
|
||||
.show(ctx, |ui| self.ui(ui));
|
||||
}
|
||||
}
|
||||
|
||||
impl super::View for Painting {
|
||||
fn ui(&mut self, ui: &mut Ui) {
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.add(crate::egui_github_link_file!());
|
||||
});
|
||||
self.ui_control(ui);
|
||||
ui.label("Paint with your mouse/touch!");
|
||||
Frame::canvas(ui.style()).show(ui, |ui| {
|
||||
self.ui_content(ui);
|
||||
});
|
||||
}
|
||||
}
|
||||
66
crates/egui_demo_lib/src/demo/password.rs
Normal file
66
crates/egui_demo_lib/src/demo/password.rs
Normal file
@@ -0,0 +1,66 @@
|
||||
//! Source code example about creating a widget which uses `egui::Memory` to store UI state.
|
||||
//!
|
||||
//! This is meant to be read as a tutorial, hence the plethora of comments.
|
||||
|
||||
/// Password entry field with ability to toggle character hiding.
|
||||
///
|
||||
/// ## Example:
|
||||
/// ``` ignore
|
||||
/// password_ui(ui, &mut my_password);
|
||||
/// ```
|
||||
#[allow(clippy::ptr_arg)] // false positive
|
||||
pub fn password_ui(ui: &mut egui::Ui, password: &mut String) -> egui::Response {
|
||||
// This widget has its own state — show or hide password characters (`show_plaintext`).
|
||||
// In this case we use a simple `bool`, but you can also declare your own type.
|
||||
// It must implement at least `Clone` and be `'static`.
|
||||
// If you use the `persistence` feature, it also must implement `serde::{Deserialize, Serialize}`.
|
||||
|
||||
// Generate an id for the state
|
||||
let state_id = ui.id().with("show_plaintext");
|
||||
|
||||
// Get state for this widget.
|
||||
// You should get state by value, not by reference to avoid borrowing of [`Memory`].
|
||||
let mut show_plaintext = ui.data().get_temp::<bool>(state_id).unwrap_or(false);
|
||||
|
||||
// Process ui, change a local copy of the state
|
||||
// We want TextEdit to fill entire space, and have button after that, so in that case we can
|
||||
// change direction to right_to_left.
|
||||
let result = ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
// Toggle the `show_plaintext` bool with a button:
|
||||
let response = ui
|
||||
.add(egui::SelectableLabel::new(show_plaintext, "👁"))
|
||||
.on_hover_text("Show/hide password");
|
||||
|
||||
if response.clicked() {
|
||||
show_plaintext = !show_plaintext;
|
||||
}
|
||||
|
||||
// Show the password field:
|
||||
ui.add_sized(
|
||||
ui.available_size(),
|
||||
egui::TextEdit::singleline(password).password(!show_plaintext),
|
||||
);
|
||||
});
|
||||
|
||||
// Store the (possibly changed) state:
|
||||
ui.data().insert_temp(state_id, show_plaintext);
|
||||
|
||||
// All done! Return the interaction response so the user can check what happened
|
||||
// (hovered, clicked, …) and maybe show a tooltip:
|
||||
result.response
|
||||
}
|
||||
|
||||
// A wrapper that allows the more idiomatic usage pattern: `ui.add(…)`
|
||||
/// Password entry field with ability to toggle character hiding.
|
||||
///
|
||||
/// ## Example:
|
||||
/// ``` ignore
|
||||
/// ui.add(password(&mut my_password));
|
||||
/// ```
|
||||
pub fn password(password: &mut String) -> impl egui::Widget + '_ {
|
||||
move |ui: &mut egui::Ui| password_ui(ui, password)
|
||||
}
|
||||
|
||||
pub fn url_to_file_source_code() -> String {
|
||||
format!("https://github.com/emilk/egui/blob/master/{}", file!())
|
||||
}
|
||||
969
crates/egui_demo_lib/src/demo/plot_demo.rs
Normal file
969
crates/egui_demo_lib/src/demo/plot_demo.rs
Normal file
@@ -0,0 +1,969 @@
|
||||
use std::f64::consts::TAU;
|
||||
use std::ops::RangeInclusive;
|
||||
|
||||
use egui::plot::{GridInput, GridMark};
|
||||
use egui::*;
|
||||
use plot::{
|
||||
Arrows, Bar, BarChart, BoxElem, BoxPlot, BoxSpread, CoordinatesFormatter, Corner, HLine,
|
||||
Legend, Line, LineStyle, MarkerShape, Plot, PlotImage, PlotPoint, PlotPoints, Points, Polygon,
|
||||
Text, VLine,
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[derive(PartialEq)]
|
||||
struct LineDemo {
|
||||
animate: bool,
|
||||
time: f64,
|
||||
circle_radius: f64,
|
||||
circle_center: Pos2,
|
||||
square: bool,
|
||||
proportional: bool,
|
||||
coordinates: bool,
|
||||
line_style: LineStyle,
|
||||
}
|
||||
|
||||
impl Default for LineDemo {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
animate: !cfg!(debug_assertions),
|
||||
time: 0.0,
|
||||
circle_radius: 1.5,
|
||||
circle_center: Pos2::new(0.0, 0.0),
|
||||
square: false,
|
||||
proportional: true,
|
||||
coordinates: true,
|
||||
line_style: LineStyle::Solid,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LineDemo {
|
||||
fn options_ui(&mut self, ui: &mut Ui) {
|
||||
let Self {
|
||||
animate,
|
||||
time: _,
|
||||
circle_radius,
|
||||
circle_center,
|
||||
square,
|
||||
proportional,
|
||||
line_style,
|
||||
coordinates,
|
||||
..
|
||||
} = self;
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.group(|ui| {
|
||||
ui.vertical(|ui| {
|
||||
ui.label("Circle:");
|
||||
ui.add(
|
||||
egui::DragValue::new(circle_radius)
|
||||
.speed(0.1)
|
||||
.clamp_range(0.0..=f64::INFINITY)
|
||||
.prefix("r: "),
|
||||
);
|
||||
ui.horizontal(|ui| {
|
||||
ui.add(
|
||||
egui::DragValue::new(&mut circle_center.x)
|
||||
.speed(0.1)
|
||||
.prefix("x: "),
|
||||
);
|
||||
ui.add(
|
||||
egui::DragValue::new(&mut circle_center.y)
|
||||
.speed(1.0)
|
||||
.prefix("y: "),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
ui.vertical(|ui| {
|
||||
ui.style_mut().wrap = Some(false);
|
||||
ui.checkbox(animate, "Animate");
|
||||
ui.checkbox(square, "Square view")
|
||||
.on_hover_text("Always keep the viewport square.");
|
||||
ui.checkbox(proportional, "Proportional data axes")
|
||||
.on_hover_text("Tick are the same size on both axes.");
|
||||
ui.checkbox(coordinates, "Show coordinates")
|
||||
.on_hover_text("Can take a custom formatting function.");
|
||||
|
||||
ComboBox::from_label("Line style")
|
||||
.selected_text(line_style.to_string())
|
||||
.show_ui(ui, |ui| {
|
||||
for style in &[
|
||||
LineStyle::Solid,
|
||||
LineStyle::dashed_dense(),
|
||||
LineStyle::dashed_loose(),
|
||||
LineStyle::dotted_dense(),
|
||||
LineStyle::dotted_loose(),
|
||||
] {
|
||||
ui.selectable_value(line_style, *style, style.to_string());
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn circle(&self) -> Line {
|
||||
let n = 512;
|
||||
let circle_points: PlotPoints = (0..=n)
|
||||
.map(|i| {
|
||||
let t = remap(i as f64, 0.0..=(n as f64), 0.0..=TAU);
|
||||
let r = self.circle_radius;
|
||||
[
|
||||
r * t.cos() + self.circle_center.x as f64,
|
||||
r * t.sin() + self.circle_center.y as f64,
|
||||
]
|
||||
})
|
||||
.collect();
|
||||
Line::new(circle_points)
|
||||
.color(Color32::from_rgb(100, 200, 100))
|
||||
.style(self.line_style)
|
||||
.name("circle")
|
||||
}
|
||||
|
||||
fn sin(&self) -> Line {
|
||||
let time = self.time;
|
||||
Line::new(PlotPoints::from_explicit_callback(
|
||||
move |x| 0.5 * (2.0 * x).sin() * time.sin(),
|
||||
..,
|
||||
512,
|
||||
))
|
||||
.color(Color32::from_rgb(200, 100, 100))
|
||||
.style(self.line_style)
|
||||
.name("wave")
|
||||
}
|
||||
|
||||
fn thingy(&self) -> Line {
|
||||
let time = self.time;
|
||||
Line::new(PlotPoints::from_parametric_callback(
|
||||
move |t| ((2.0 * t + time).sin(), (3.0 * t).sin()),
|
||||
0.0..=TAU,
|
||||
256,
|
||||
))
|
||||
.color(Color32::from_rgb(100, 150, 250))
|
||||
.style(self.line_style)
|
||||
.name("x = sin(2t), y = sin(3t)")
|
||||
}
|
||||
}
|
||||
|
||||
impl LineDemo {
|
||||
fn ui(&mut self, ui: &mut Ui) -> Response {
|
||||
self.options_ui(ui);
|
||||
if self.animate {
|
||||
ui.ctx().request_repaint();
|
||||
self.time += ui.input().unstable_dt.at_most(1.0 / 30.0) as f64;
|
||||
};
|
||||
let mut plot = Plot::new("lines_demo").legend(Legend::default());
|
||||
if self.square {
|
||||
plot = plot.view_aspect(1.0);
|
||||
}
|
||||
if self.proportional {
|
||||
plot = plot.data_aspect(1.0);
|
||||
}
|
||||
if self.coordinates {
|
||||
plot = plot.coordinates_formatter(Corner::LeftBottom, CoordinatesFormatter::default());
|
||||
}
|
||||
plot.show(ui, |plot_ui| {
|
||||
plot_ui.line(self.circle());
|
||||
plot_ui.line(self.sin());
|
||||
plot_ui.line(self.thingy());
|
||||
})
|
||||
.response
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[derive(PartialEq)]
|
||||
struct MarkerDemo {
|
||||
fill_markers: bool,
|
||||
marker_radius: f32,
|
||||
automatic_colors: bool,
|
||||
marker_color: Color32,
|
||||
}
|
||||
|
||||
impl Default for MarkerDemo {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
fill_markers: true,
|
||||
marker_radius: 5.0,
|
||||
automatic_colors: true,
|
||||
marker_color: Color32::GREEN,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MarkerDemo {
|
||||
fn markers(&self) -> Vec<Points> {
|
||||
MarkerShape::all()
|
||||
.enumerate()
|
||||
.map(|(i, marker)| {
|
||||
let y_offset = i as f64 * 0.5 + 1.0;
|
||||
let mut points = Points::new(vec![
|
||||
[1.0, 0.0 + y_offset],
|
||||
[2.0, 0.5 + y_offset],
|
||||
[3.0, 0.0 + y_offset],
|
||||
[4.0, 0.5 + y_offset],
|
||||
[5.0, 0.0 + y_offset],
|
||||
[6.0, 0.5 + y_offset],
|
||||
])
|
||||
.name(format!("{:?}", marker))
|
||||
.filled(self.fill_markers)
|
||||
.radius(self.marker_radius)
|
||||
.shape(marker);
|
||||
|
||||
if !self.automatic_colors {
|
||||
points = points.color(self.marker_color);
|
||||
}
|
||||
|
||||
points
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn ui(&mut self, ui: &mut Ui) -> Response {
|
||||
ui.horizontal(|ui| {
|
||||
ui.checkbox(&mut self.fill_markers, "Fill");
|
||||
ui.add(
|
||||
egui::DragValue::new(&mut self.marker_radius)
|
||||
.speed(0.1)
|
||||
.clamp_range(0.0..=f64::INFINITY)
|
||||
.prefix("Radius: "),
|
||||
);
|
||||
ui.checkbox(&mut self.automatic_colors, "Automatic colors");
|
||||
if !self.automatic_colors {
|
||||
ui.color_edit_button_srgba(&mut self.marker_color);
|
||||
}
|
||||
});
|
||||
|
||||
let markers_plot = Plot::new("markers_demo")
|
||||
.data_aspect(1.0)
|
||||
.legend(Legend::default());
|
||||
markers_plot
|
||||
.show(ui, |plot_ui| {
|
||||
for marker in self.markers() {
|
||||
plot_ui.points(marker);
|
||||
}
|
||||
})
|
||||
.response
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[derive(Default, PartialEq)]
|
||||
struct LegendDemo {
|
||||
config: Legend,
|
||||
}
|
||||
|
||||
impl LegendDemo {
|
||||
fn line_with_slope(slope: f64) -> Line {
|
||||
Line::new(PlotPoints::from_explicit_callback(
|
||||
move |x| slope * x,
|
||||
..,
|
||||
100,
|
||||
))
|
||||
}
|
||||
|
||||
fn sin() -> Line {
|
||||
Line::new(PlotPoints::from_explicit_callback(
|
||||
move |x| x.sin(),
|
||||
..,
|
||||
100,
|
||||
))
|
||||
}
|
||||
|
||||
fn cos() -> Line {
|
||||
Line::new(PlotPoints::from_explicit_callback(
|
||||
move |x| x.cos(),
|
||||
..,
|
||||
100,
|
||||
))
|
||||
}
|
||||
|
||||
fn ui(&mut self, ui: &mut Ui) -> Response {
|
||||
let LegendDemo { config } = self;
|
||||
|
||||
egui::Grid::new("settings").show(ui, |ui| {
|
||||
ui.label("Text style:");
|
||||
ui.horizontal(|ui| {
|
||||
let all_text_styles = ui.style().text_styles();
|
||||
for style in all_text_styles {
|
||||
ui.selectable_value(&mut config.text_style, style.clone(), style.to_string());
|
||||
}
|
||||
});
|
||||
ui.end_row();
|
||||
|
||||
ui.label("Position:");
|
||||
ui.horizontal(|ui| {
|
||||
Corner::all().for_each(|position| {
|
||||
ui.selectable_value(&mut config.position, position, format!("{:?}", position));
|
||||
});
|
||||
});
|
||||
ui.end_row();
|
||||
|
||||
ui.label("Opacity:");
|
||||
ui.add(
|
||||
egui::DragValue::new(&mut config.background_alpha)
|
||||
.speed(0.02)
|
||||
.clamp_range(0.0..=1.0),
|
||||
);
|
||||
ui.end_row();
|
||||
});
|
||||
|
||||
let legend_plot = Plot::new("legend_demo")
|
||||
.legend(config.clone())
|
||||
.data_aspect(1.0);
|
||||
legend_plot
|
||||
.show(ui, |plot_ui| {
|
||||
plot_ui.line(LegendDemo::line_with_slope(0.5).name("lines"));
|
||||
plot_ui.line(LegendDemo::line_with_slope(1.0).name("lines"));
|
||||
plot_ui.line(LegendDemo::line_with_slope(2.0).name("lines"));
|
||||
plot_ui.line(LegendDemo::sin().name("sin(x)"));
|
||||
plot_ui.line(LegendDemo::cos().name("cos(x)"));
|
||||
})
|
||||
.response
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[derive(PartialEq, Default)]
|
||||
struct CustomAxisDemo {}
|
||||
|
||||
impl CustomAxisDemo {
|
||||
const MINS_PER_DAY: f64 = 24.0 * 60.0;
|
||||
const MINS_PER_H: f64 = 60.0;
|
||||
|
||||
fn logistic_fn() -> Line {
|
||||
fn days(min: f64) -> f64 {
|
||||
CustomAxisDemo::MINS_PER_DAY * min
|
||||
}
|
||||
|
||||
let values = PlotPoints::from_explicit_callback(
|
||||
move |x| 1.0 / (1.0 + (-2.5 * (x / CustomAxisDemo::MINS_PER_DAY - 2.0)).exp()),
|
||||
days(0.0)..days(5.0),
|
||||
100,
|
||||
);
|
||||
Line::new(values)
|
||||
}
|
||||
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
fn x_grid(input: GridInput) -> Vec<GridMark> {
|
||||
// Note: this always fills all possible marks. For optimization, `input.bounds`
|
||||
// could be used to decide when the low-interval grids (minutes) should be added.
|
||||
|
||||
let mut marks = vec![];
|
||||
|
||||
let (min, max) = input.bounds;
|
||||
let min = min.floor() as i32;
|
||||
let max = max.ceil() as i32;
|
||||
|
||||
for i in min..=max {
|
||||
let step_size = if i % Self::MINS_PER_DAY as i32 == 0 {
|
||||
// 1 day
|
||||
Self::MINS_PER_DAY
|
||||
} else if i % Self::MINS_PER_H as i32 == 0 {
|
||||
// 1 hour
|
||||
Self::MINS_PER_H
|
||||
} else if i % 5 == 0 {
|
||||
// 5min
|
||||
5.0
|
||||
} else {
|
||||
// skip grids below 5min
|
||||
continue;
|
||||
};
|
||||
|
||||
marks.push(GridMark {
|
||||
value: i as f64,
|
||||
step_size,
|
||||
});
|
||||
}
|
||||
|
||||
marks
|
||||
}
|
||||
|
||||
#[allow(clippy::unused_self)]
|
||||
fn ui(&mut self, ui: &mut Ui) -> Response {
|
||||
const MINS_PER_DAY: f64 = CustomAxisDemo::MINS_PER_DAY;
|
||||
const MINS_PER_H: f64 = CustomAxisDemo::MINS_PER_H;
|
||||
|
||||
fn day(x: f64) -> f64 {
|
||||
(x / MINS_PER_DAY).floor()
|
||||
}
|
||||
|
||||
fn hour(x: f64) -> f64 {
|
||||
(x.rem_euclid(MINS_PER_DAY) / MINS_PER_H).floor()
|
||||
}
|
||||
|
||||
fn minute(x: f64) -> f64 {
|
||||
x.rem_euclid(MINS_PER_H).floor()
|
||||
}
|
||||
|
||||
fn percent(y: f64) -> f64 {
|
||||
100.0 * y
|
||||
}
|
||||
|
||||
let x_fmt = |x, _range: &RangeInclusive<f64>| {
|
||||
if x < 0.0 * MINS_PER_DAY || x >= 5.0 * MINS_PER_DAY {
|
||||
// No labels outside value bounds
|
||||
String::new()
|
||||
} else if is_approx_integer(x / MINS_PER_DAY) {
|
||||
// Days
|
||||
format!("Day {}", day(x))
|
||||
} else {
|
||||
// Hours and minutes
|
||||
format!("{h}:{m:02}", h = hour(x), m = minute(x))
|
||||
}
|
||||
};
|
||||
|
||||
let y_fmt = |y, _range: &RangeInclusive<f64>| {
|
||||
// Display only integer percentages
|
||||
if !is_approx_zero(y) && is_approx_integer(100.0 * y) {
|
||||
format!("{:.0}%", percent(y))
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
};
|
||||
|
||||
let label_fmt = |_s: &str, val: &PlotPoint| {
|
||||
format!(
|
||||
"Day {d}, {h}:{m:02}\n{p:.2}%",
|
||||
d = day(val.x),
|
||||
h = hour(val.x),
|
||||
m = minute(val.x),
|
||||
p = percent(val.y)
|
||||
)
|
||||
};
|
||||
|
||||
ui.label("Zoom in on the X-axis to see hours and minutes");
|
||||
|
||||
Plot::new("custom_axes")
|
||||
.data_aspect(2.0 * MINS_PER_DAY as f32)
|
||||
.x_axis_formatter(x_fmt)
|
||||
.y_axis_formatter(y_fmt)
|
||||
.x_grid_spacer(CustomAxisDemo::x_grid)
|
||||
.label_formatter(label_fmt)
|
||||
.show(ui, |plot_ui| {
|
||||
plot_ui.line(CustomAxisDemo::logistic_fn());
|
||||
})
|
||||
.response
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[derive(PartialEq)]
|
||||
struct LinkedAxisDemo {
|
||||
link_x: bool,
|
||||
link_y: bool,
|
||||
group: plot::LinkedAxisGroup,
|
||||
}
|
||||
|
||||
impl Default for LinkedAxisDemo {
|
||||
fn default() -> Self {
|
||||
let link_x = true;
|
||||
let link_y = false;
|
||||
Self {
|
||||
link_x,
|
||||
link_y,
|
||||
group: plot::LinkedAxisGroup::new(link_x, link_y),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LinkedAxisDemo {
|
||||
fn line_with_slope(slope: f64) -> Line {
|
||||
Line::new(PlotPoints::from_explicit_callback(
|
||||
move |x| slope * x,
|
||||
..,
|
||||
100,
|
||||
))
|
||||
}
|
||||
|
||||
fn sin() -> Line {
|
||||
Line::new(PlotPoints::from_explicit_callback(
|
||||
move |x| x.sin(),
|
||||
..,
|
||||
100,
|
||||
))
|
||||
}
|
||||
|
||||
fn cos() -> Line {
|
||||
Line::new(PlotPoints::from_explicit_callback(
|
||||
move |x| x.cos(),
|
||||
..,
|
||||
100,
|
||||
))
|
||||
}
|
||||
|
||||
fn configure_plot(plot_ui: &mut plot::PlotUi) {
|
||||
plot_ui.line(LinkedAxisDemo::line_with_slope(0.5));
|
||||
plot_ui.line(LinkedAxisDemo::line_with_slope(1.0));
|
||||
plot_ui.line(LinkedAxisDemo::line_with_slope(2.0));
|
||||
plot_ui.line(LinkedAxisDemo::sin());
|
||||
plot_ui.line(LinkedAxisDemo::cos());
|
||||
}
|
||||
|
||||
fn ui(&mut self, ui: &mut Ui) -> Response {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Linked axes:");
|
||||
ui.checkbox(&mut self.link_x, "X");
|
||||
ui.checkbox(&mut self.link_y, "Y");
|
||||
});
|
||||
self.group.set_link_x(self.link_x);
|
||||
self.group.set_link_y(self.link_y);
|
||||
ui.horizontal(|ui| {
|
||||
Plot::new("linked_axis_1")
|
||||
.data_aspect(1.0)
|
||||
.width(250.0)
|
||||
.height(250.0)
|
||||
.link_axis(self.group.clone())
|
||||
.show(ui, LinkedAxisDemo::configure_plot);
|
||||
Plot::new("linked_axis_2")
|
||||
.data_aspect(2.0)
|
||||
.width(150.0)
|
||||
.height(250.0)
|
||||
.link_axis(self.group.clone())
|
||||
.show(ui, LinkedAxisDemo::configure_plot);
|
||||
});
|
||||
Plot::new("linked_axis_3")
|
||||
.data_aspect(0.5)
|
||||
.width(250.0)
|
||||
.height(150.0)
|
||||
.link_axis(self.group.clone())
|
||||
.show(ui, LinkedAxisDemo::configure_plot)
|
||||
.response
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[derive(PartialEq, Default)]
|
||||
struct ItemsDemo {
|
||||
texture: Option<egui::TextureHandle>,
|
||||
}
|
||||
|
||||
impl ItemsDemo {
|
||||
fn ui(&mut self, ui: &mut Ui) -> Response {
|
||||
let n = 100;
|
||||
let mut sin_values: Vec<_> = (0..=n)
|
||||
.map(|i| remap(i as f64, 0.0..=n as f64, -TAU..=TAU))
|
||||
.map(|i| [i, i.sin()])
|
||||
.collect();
|
||||
|
||||
let line = Line::new(sin_values.split_off(n / 2)).fill(-1.5);
|
||||
let polygon = Polygon::new(PlotPoints::from_parametric_callback(
|
||||
|t| (4.0 * t.sin() + 2.0 * t.cos(), 4.0 * t.cos() + 2.0 * t.sin()),
|
||||
0.0..TAU,
|
||||
100,
|
||||
));
|
||||
let points = Points::new(sin_values).stems(-1.5).radius(1.0);
|
||||
|
||||
let arrows = {
|
||||
let pos_radius = 8.0;
|
||||
let tip_radius = 7.0;
|
||||
let arrow_origins = PlotPoints::from_parametric_callback(
|
||||
|t| (pos_radius * t.sin(), pos_radius * t.cos()),
|
||||
0.0..TAU,
|
||||
36,
|
||||
);
|
||||
let arrow_tips = PlotPoints::from_parametric_callback(
|
||||
|t| (tip_radius * t.sin(), tip_radius * t.cos()),
|
||||
0.0..TAU,
|
||||
36,
|
||||
);
|
||||
Arrows::new(arrow_origins, arrow_tips)
|
||||
};
|
||||
|
||||
let texture: &egui::TextureHandle = self.texture.get_or_insert_with(|| {
|
||||
ui.ctx().load_texture(
|
||||
"plot_demo",
|
||||
egui::ColorImage::example(),
|
||||
egui::TextureFilter::Linear,
|
||||
)
|
||||
});
|
||||
let image = PlotImage::new(
|
||||
texture,
|
||||
PlotPoint::new(0.0, 10.0),
|
||||
5.0 * vec2(texture.aspect_ratio(), 1.0),
|
||||
);
|
||||
|
||||
let plot = Plot::new("items_demo")
|
||||
.legend(Legend::default().position(Corner::RightBottom))
|
||||
.show_x(false)
|
||||
.show_y(false)
|
||||
.data_aspect(1.0);
|
||||
plot.show(ui, |plot_ui| {
|
||||
plot_ui.hline(HLine::new(9.0).name("Lines horizontal"));
|
||||
plot_ui.hline(HLine::new(-9.0).name("Lines horizontal"));
|
||||
plot_ui.vline(VLine::new(9.0).name("Lines vertical"));
|
||||
plot_ui.vline(VLine::new(-9.0).name("Lines vertical"));
|
||||
plot_ui.line(line.name("Line with fill"));
|
||||
plot_ui.polygon(polygon.name("Convex polygon"));
|
||||
plot_ui.points(points.name("Points with stems"));
|
||||
plot_ui.text(Text::new(PlotPoint::new(-3.0, -3.0), "wow").name("Text"));
|
||||
plot_ui.text(Text::new(PlotPoint::new(-2.0, 2.5), "so graph").name("Text"));
|
||||
plot_ui.text(Text::new(PlotPoint::new(3.0, 3.0), "much color").name("Text"));
|
||||
plot_ui.text(Text::new(PlotPoint::new(2.5, -2.0), "such plot").name("Text"));
|
||||
plot_ui.image(image.name("Image"));
|
||||
plot_ui.arrows(arrows.name("Arrows"));
|
||||
})
|
||||
.response
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[derive(Default, PartialEq)]
|
||||
struct InteractionDemo {}
|
||||
|
||||
impl InteractionDemo {
|
||||
#[allow(clippy::unused_self)]
|
||||
fn ui(&mut self, ui: &mut Ui) -> Response {
|
||||
let plot = Plot::new("interaction_demo").height(300.0);
|
||||
|
||||
let InnerResponse {
|
||||
response,
|
||||
inner: (screen_pos, pointer_coordinate, pointer_coordinate_drag_delta, bounds, hovered),
|
||||
} = plot.show(ui, |plot_ui| {
|
||||
(
|
||||
plot_ui.screen_from_plot(PlotPoint::new(0.0, 0.0)),
|
||||
plot_ui.pointer_coordinate(),
|
||||
plot_ui.pointer_coordinate_drag_delta(),
|
||||
plot_ui.plot_bounds(),
|
||||
plot_ui.plot_hovered(),
|
||||
)
|
||||
});
|
||||
|
||||
ui.label(format!(
|
||||
"plot bounds: min: {:.02?}, max: {:.02?}",
|
||||
bounds.min(),
|
||||
bounds.max()
|
||||
));
|
||||
ui.label(format!(
|
||||
"origin in screen coordinates: x: {:.02}, y: {:.02}",
|
||||
screen_pos.x, screen_pos.y
|
||||
));
|
||||
ui.label(format!("plot hovered: {}", hovered));
|
||||
let coordinate_text = if let Some(coordinate) = pointer_coordinate {
|
||||
format!("x: {:.02}, y: {:.02}", coordinate.x, coordinate.y)
|
||||
} else {
|
||||
"None".to_owned()
|
||||
};
|
||||
ui.label(format!("pointer coordinate: {}", coordinate_text));
|
||||
let coordinate_text = format!(
|
||||
"x: {:.02}, y: {:.02}",
|
||||
pointer_coordinate_drag_delta.x, pointer_coordinate_drag_delta.y
|
||||
);
|
||||
ui.label(format!(
|
||||
"pointer coordinate drag delta: {}",
|
||||
coordinate_text
|
||||
));
|
||||
|
||||
response
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[derive(PartialEq, Eq)]
|
||||
enum Chart {
|
||||
GaussBars,
|
||||
StackedBars,
|
||||
BoxPlot,
|
||||
}
|
||||
|
||||
impl Default for Chart {
|
||||
fn default() -> Self {
|
||||
Self::GaussBars
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
struct ChartsDemo {
|
||||
chart: Chart,
|
||||
vertical: bool,
|
||||
}
|
||||
|
||||
impl Default for ChartsDemo {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
vertical: true,
|
||||
chart: Chart::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ChartsDemo {
|
||||
fn ui(&mut self, ui: &mut Ui) -> Response {
|
||||
ui.label("Type:");
|
||||
ui.horizontal(|ui| {
|
||||
ui.selectable_value(&mut self.chart, Chart::GaussBars, "Histogram");
|
||||
ui.selectable_value(&mut self.chart, Chart::StackedBars, "Stacked Bar Chart");
|
||||
ui.selectable_value(&mut self.chart, Chart::BoxPlot, "Box Plot");
|
||||
});
|
||||
ui.label("Orientation:");
|
||||
ui.horizontal(|ui| {
|
||||
ui.selectable_value(&mut self.vertical, true, "Vertical");
|
||||
ui.selectable_value(&mut self.vertical, false, "Horizontal");
|
||||
});
|
||||
match self.chart {
|
||||
Chart::GaussBars => self.bar_gauss(ui),
|
||||
Chart::StackedBars => self.bar_stacked(ui),
|
||||
Chart::BoxPlot => self.box_plot(ui),
|
||||
}
|
||||
}
|
||||
|
||||
fn bar_gauss(&self, ui: &mut Ui) -> Response {
|
||||
let mut chart = BarChart::new(
|
||||
(-395..=395)
|
||||
.step_by(10)
|
||||
.map(|x| x as f64 * 0.01)
|
||||
.map(|x| {
|
||||
(
|
||||
x,
|
||||
(-x * x / 2.0).exp() / (2.0 * std::f64::consts::PI).sqrt(),
|
||||
)
|
||||
})
|
||||
// The 10 factor here is purely for a nice 1:1 aspect ratio
|
||||
.map(|(x, f)| Bar::new(x, f * 10.0).width(0.095))
|
||||
.collect(),
|
||||
)
|
||||
.color(Color32::LIGHT_BLUE)
|
||||
.name("Normal Distribution");
|
||||
if !self.vertical {
|
||||
chart = chart.horizontal();
|
||||
}
|
||||
|
||||
Plot::new("Normal Distribution Demo")
|
||||
.legend(Legend::default())
|
||||
.data_aspect(1.0)
|
||||
.show(ui, |plot_ui| plot_ui.bar_chart(chart))
|
||||
.response
|
||||
}
|
||||
|
||||
fn bar_stacked(&self, ui: &mut Ui) -> Response {
|
||||
let mut chart1 = BarChart::new(vec![
|
||||
Bar::new(0.5, 1.0).name("Day 1"),
|
||||
Bar::new(1.5, 3.0).name("Day 2"),
|
||||
Bar::new(2.5, 1.0).name("Day 3"),
|
||||
Bar::new(3.5, 2.0).name("Day 4"),
|
||||
Bar::new(4.5, 4.0).name("Day 5"),
|
||||
])
|
||||
.width(0.7)
|
||||
.name("Set 1");
|
||||
|
||||
let mut chart2 = BarChart::new(vec![
|
||||
Bar::new(0.5, 1.0),
|
||||
Bar::new(1.5, 1.5),
|
||||
Bar::new(2.5, 0.1),
|
||||
Bar::new(3.5, 0.7),
|
||||
Bar::new(4.5, 0.8),
|
||||
])
|
||||
.width(0.7)
|
||||
.name("Set 2")
|
||||
.stack_on(&[&chart1]);
|
||||
|
||||
let mut chart3 = BarChart::new(vec![
|
||||
Bar::new(0.5, -0.5),
|
||||
Bar::new(1.5, 1.0),
|
||||
Bar::new(2.5, 0.5),
|
||||
Bar::new(3.5, -1.0),
|
||||
Bar::new(4.5, 0.3),
|
||||
])
|
||||
.width(0.7)
|
||||
.name("Set 3")
|
||||
.stack_on(&[&chart1, &chart2]);
|
||||
|
||||
let mut chart4 = BarChart::new(vec![
|
||||
Bar::new(0.5, 0.5),
|
||||
Bar::new(1.5, 1.0),
|
||||
Bar::new(2.5, 0.5),
|
||||
Bar::new(3.5, -0.5),
|
||||
Bar::new(4.5, -0.5),
|
||||
])
|
||||
.width(0.7)
|
||||
.name("Set 4")
|
||||
.stack_on(&[&chart1, &chart2, &chart3]);
|
||||
|
||||
if !self.vertical {
|
||||
chart1 = chart1.horizontal();
|
||||
chart2 = chart2.horizontal();
|
||||
chart3 = chart3.horizontal();
|
||||
chart4 = chart4.horizontal();
|
||||
}
|
||||
|
||||
Plot::new("Stacked Bar Chart Demo")
|
||||
.legend(Legend::default())
|
||||
.data_aspect(1.0)
|
||||
.show(ui, |plot_ui| {
|
||||
plot_ui.bar_chart(chart1);
|
||||
plot_ui.bar_chart(chart2);
|
||||
plot_ui.bar_chart(chart3);
|
||||
plot_ui.bar_chart(chart4);
|
||||
})
|
||||
.response
|
||||
}
|
||||
|
||||
fn box_plot(&self, ui: &mut Ui) -> Response {
|
||||
let yellow = Color32::from_rgb(248, 252, 168);
|
||||
let mut box1 = BoxPlot::new(vec![
|
||||
BoxElem::new(0.5, BoxSpread::new(1.5, 2.2, 2.5, 2.6, 3.1)).name("Day 1"),
|
||||
BoxElem::new(2.5, BoxSpread::new(0.4, 1.0, 1.1, 1.4, 2.1)).name("Day 2"),
|
||||
BoxElem::new(4.5, BoxSpread::new(1.7, 2.0, 2.2, 2.5, 2.9)).name("Day 3"),
|
||||
])
|
||||
.name("Experiment A");
|
||||
|
||||
let mut box2 = BoxPlot::new(vec![
|
||||
BoxElem::new(1.0, BoxSpread::new(0.2, 0.5, 1.0, 2.0, 2.7)).name("Day 1"),
|
||||
BoxElem::new(3.0, BoxSpread::new(1.5, 1.7, 2.1, 2.9, 3.3))
|
||||
.name("Day 2: interesting")
|
||||
.stroke(Stroke::new(1.5, yellow))
|
||||
.fill(yellow.linear_multiply(0.2)),
|
||||
BoxElem::new(5.0, BoxSpread::new(1.3, 2.0, 2.3, 2.9, 4.0)).name("Day 3"),
|
||||
])
|
||||
.name("Experiment B");
|
||||
|
||||
let mut box3 = BoxPlot::new(vec![
|
||||
BoxElem::new(1.5, BoxSpread::new(2.1, 2.2, 2.6, 2.8, 3.0)).name("Day 1"),
|
||||
BoxElem::new(3.5, BoxSpread::new(1.3, 1.5, 1.9, 2.2, 2.4)).name("Day 2"),
|
||||
BoxElem::new(5.5, BoxSpread::new(0.2, 0.4, 1.0, 1.3, 1.5)).name("Day 3"),
|
||||
])
|
||||
.name("Experiment C");
|
||||
|
||||
if !self.vertical {
|
||||
box1 = box1.horizontal();
|
||||
box2 = box2.horizontal();
|
||||
box3 = box3.horizontal();
|
||||
}
|
||||
|
||||
Plot::new("Box Plot Demo")
|
||||
.legend(Legend::default())
|
||||
.show(ui, |plot_ui| {
|
||||
plot_ui.box_plot(box1);
|
||||
plot_ui.box_plot(box2);
|
||||
plot_ui.box_plot(box3);
|
||||
})
|
||||
.response
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[derive(PartialEq, Eq)]
|
||||
enum Panel {
|
||||
Lines,
|
||||
Markers,
|
||||
Legend,
|
||||
Charts,
|
||||
Items,
|
||||
Interaction,
|
||||
CustomAxes,
|
||||
LinkedAxes,
|
||||
}
|
||||
|
||||
impl Default for Panel {
|
||||
fn default() -> Self {
|
||||
Self::Lines
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[derive(PartialEq, Default)]
|
||||
pub struct PlotDemo {
|
||||
line_demo: LineDemo,
|
||||
marker_demo: MarkerDemo,
|
||||
legend_demo: LegendDemo,
|
||||
charts_demo: ChartsDemo,
|
||||
items_demo: ItemsDemo,
|
||||
interaction_demo: InteractionDemo,
|
||||
custom_axes_demo: CustomAxisDemo,
|
||||
linked_axes_demo: LinkedAxisDemo,
|
||||
open_panel: Panel,
|
||||
}
|
||||
|
||||
impl super::Demo for PlotDemo {
|
||||
fn name(&self) -> &'static str {
|
||||
"🗠 Plot"
|
||||
}
|
||||
|
||||
fn show(&mut self, ctx: &Context, open: &mut bool) {
|
||||
use super::View as _;
|
||||
Window::new(self.name())
|
||||
.open(open)
|
||||
.default_size(vec2(400.0, 400.0))
|
||||
.vscroll(false)
|
||||
.show(ctx, |ui| self.ui(ui));
|
||||
}
|
||||
}
|
||||
|
||||
impl super::View for PlotDemo {
|
||||
fn ui(&mut self, ui: &mut Ui) {
|
||||
ui.horizontal(|ui| {
|
||||
egui::reset_button(ui, self);
|
||||
ui.collapsing("Instructions", |ui| {
|
||||
ui.label("Pan by dragging, or scroll (+ shift = horizontal).");
|
||||
ui.label("Box zooming: Right click to zoom in and zoom out using a selection.");
|
||||
if cfg!(target_arch = "wasm32") {
|
||||
ui.label("Zoom with ctrl / ⌘ + pointer wheel, or with pinch gesture.");
|
||||
} else if cfg!(target_os = "macos") {
|
||||
ui.label("Zoom with ctrl / ⌘ + scroll.");
|
||||
} else {
|
||||
ui.label("Zoom with ctrl + scroll.");
|
||||
}
|
||||
ui.label("Reset view with double-click.");
|
||||
ui.add(crate::egui_github_link_file!());
|
||||
});
|
||||
});
|
||||
ui.separator();
|
||||
ui.horizontal(|ui| {
|
||||
ui.selectable_value(&mut self.open_panel, Panel::Lines, "Lines");
|
||||
ui.selectable_value(&mut self.open_panel, Panel::Markers, "Markers");
|
||||
ui.selectable_value(&mut self.open_panel, Panel::Legend, "Legend");
|
||||
ui.selectable_value(&mut self.open_panel, Panel::Charts, "Charts");
|
||||
ui.selectable_value(&mut self.open_panel, Panel::Items, "Items");
|
||||
ui.selectable_value(&mut self.open_panel, Panel::Interaction, "Interaction");
|
||||
ui.selectable_value(&mut self.open_panel, Panel::CustomAxes, "Custom Axes");
|
||||
ui.selectable_value(&mut self.open_panel, Panel::LinkedAxes, "Linked Axes");
|
||||
});
|
||||
ui.separator();
|
||||
|
||||
match self.open_panel {
|
||||
Panel::Lines => {
|
||||
self.line_demo.ui(ui);
|
||||
}
|
||||
Panel::Markers => {
|
||||
self.marker_demo.ui(ui);
|
||||
}
|
||||
Panel::Legend => {
|
||||
self.legend_demo.ui(ui);
|
||||
}
|
||||
Panel::Charts => {
|
||||
self.charts_demo.ui(ui);
|
||||
}
|
||||
Panel::Items => {
|
||||
self.items_demo.ui(ui);
|
||||
}
|
||||
Panel::Interaction => {
|
||||
self.interaction_demo.ui(ui);
|
||||
}
|
||||
Panel::CustomAxes => {
|
||||
self.custom_axes_demo.ui(ui);
|
||||
}
|
||||
Panel::LinkedAxes => {
|
||||
self.linked_axes_demo.ui(ui);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn is_approx_zero(val: f64) -> bool {
|
||||
val.abs() < 1e-6
|
||||
}
|
||||
|
||||
fn is_approx_integer(val: f64) -> bool {
|
||||
val.fract().abs() < 1e-6
|
||||
}
|
||||
289
crates/egui_demo_lib/src/demo/scrolling.rs
Normal file
289
crates/egui_demo_lib/src/demo/scrolling.rs
Normal file
@@ -0,0 +1,289 @@
|
||||
use egui::{color::*, *};
|
||||
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
enum ScrollDemo {
|
||||
ScrollTo,
|
||||
ManyLines,
|
||||
LargeCanvas,
|
||||
StickToEnd,
|
||||
}
|
||||
|
||||
impl Default for ScrollDemo {
|
||||
fn default() -> Self {
|
||||
Self::ScrollTo
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
#[derive(Default, PartialEq)]
|
||||
pub struct Scrolling {
|
||||
demo: ScrollDemo,
|
||||
scroll_to: ScrollTo,
|
||||
scroll_stick_to: ScrollStickTo,
|
||||
}
|
||||
|
||||
impl super::Demo for Scrolling {
|
||||
fn name(&self) -> &'static str {
|
||||
"↕ Scrolling"
|
||||
}
|
||||
|
||||
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
|
||||
egui::Window::new(self.name())
|
||||
.open(open)
|
||||
.resizable(false)
|
||||
.show(ctx, |ui| {
|
||||
use super::View as _;
|
||||
self.ui(ui);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl super::View for Scrolling {
|
||||
fn ui(&mut self, ui: &mut Ui) {
|
||||
ui.horizontal(|ui| {
|
||||
ui.selectable_value(&mut self.demo, ScrollDemo::ScrollTo, "Scroll to");
|
||||
ui.selectable_value(
|
||||
&mut self.demo,
|
||||
ScrollDemo::ManyLines,
|
||||
"Scroll a lot of lines",
|
||||
);
|
||||
ui.selectable_value(
|
||||
&mut self.demo,
|
||||
ScrollDemo::LargeCanvas,
|
||||
"Scroll a large canvas",
|
||||
);
|
||||
ui.selectable_value(&mut self.demo, ScrollDemo::StickToEnd, "Stick to end");
|
||||
});
|
||||
ui.separator();
|
||||
match self.demo {
|
||||
ScrollDemo::ScrollTo => {
|
||||
self.scroll_to.ui(ui);
|
||||
}
|
||||
ScrollDemo::ManyLines => {
|
||||
huge_content_lines(ui);
|
||||
}
|
||||
ScrollDemo::LargeCanvas => {
|
||||
huge_content_painter(ui);
|
||||
}
|
||||
ScrollDemo::StickToEnd => {
|
||||
self.scroll_stick_to.ui(ui);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn huge_content_lines(ui: &mut egui::Ui) {
|
||||
ui.label(
|
||||
"A lot of rows, but only the visible ones are layed out, so performance is still good:",
|
||||
);
|
||||
ui.add_space(4.0);
|
||||
|
||||
let text_style = TextStyle::Body;
|
||||
let row_height = ui.text_style_height(&text_style);
|
||||
let num_rows = 10_000;
|
||||
ScrollArea::vertical().auto_shrink([false; 2]).show_rows(
|
||||
ui,
|
||||
row_height,
|
||||
num_rows,
|
||||
|ui, row_range| {
|
||||
for row in row_range {
|
||||
let text = format!("This is row {}/{}", row + 1, num_rows);
|
||||
ui.label(text);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn huge_content_painter(ui: &mut egui::Ui) {
|
||||
// This is similar to the other demo, but is fully manual, for when you want to do custom painting.
|
||||
ui.label("A lot of rows, but only the visible ones are painted, so performance is still good:");
|
||||
ui.add_space(4.0);
|
||||
|
||||
let font_id = TextStyle::Body.resolve(ui.style());
|
||||
let row_height = ui.fonts().row_height(&font_id) + ui.spacing().item_spacing.y;
|
||||
let num_rows = 10_000;
|
||||
|
||||
ScrollArea::vertical()
|
||||
.auto_shrink([false; 2])
|
||||
.show_viewport(ui, |ui, viewport| {
|
||||
ui.set_height(row_height * num_rows as f32);
|
||||
|
||||
let first_item = (viewport.min.y / row_height).floor().at_least(0.0) as usize;
|
||||
let last_item = (viewport.max.y / row_height).ceil() as usize + 1;
|
||||
let last_item = last_item.at_most(num_rows);
|
||||
|
||||
let mut used_rect = Rect::NOTHING;
|
||||
|
||||
for i in first_item..last_item {
|
||||
let indentation = (i % 100) as f32;
|
||||
let x = ui.min_rect().left() + indentation;
|
||||
let y = ui.min_rect().top() + i as f32 * row_height;
|
||||
let text = format!(
|
||||
"This is row {}/{}, indented by {} pixels",
|
||||
i + 1,
|
||||
num_rows,
|
||||
indentation
|
||||
);
|
||||
let text_rect = ui.painter().text(
|
||||
pos2(x, y),
|
||||
Align2::LEFT_TOP,
|
||||
text,
|
||||
font_id.clone(),
|
||||
ui.visuals().text_color(),
|
||||
);
|
||||
used_rect = used_rect.union(text_rect);
|
||||
}
|
||||
|
||||
ui.allocate_rect(used_rect, Sense::hover()); // make sure it is visible!
|
||||
});
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
#[derive(PartialEq)]
|
||||
struct ScrollTo {
|
||||
track_item: usize,
|
||||
tack_item_align: Option<Align>,
|
||||
offset: f32,
|
||||
}
|
||||
|
||||
impl Default for ScrollTo {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
track_item: 25,
|
||||
tack_item_align: Some(Align::Center),
|
||||
offset: 0.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl super::View for ScrollTo {
|
||||
fn ui(&mut self, ui: &mut Ui) {
|
||||
ui.label("This shows how you can scroll to a specific item or pixel offset");
|
||||
|
||||
let mut track_item = false;
|
||||
let mut go_to_scroll_offset = false;
|
||||
let mut scroll_top = false;
|
||||
let mut scroll_bottom = false;
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Scroll to a specific item index:");
|
||||
track_item |= ui
|
||||
.add(Slider::new(&mut self.track_item, 1..=50).text("Track Item"))
|
||||
.dragged();
|
||||
});
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Item align:");
|
||||
track_item |= ui
|
||||
.radio_value(&mut self.tack_item_align, Some(Align::Min), "Top")
|
||||
.clicked();
|
||||
track_item |= ui
|
||||
.radio_value(&mut self.tack_item_align, Some(Align::Center), "Center")
|
||||
.clicked();
|
||||
track_item |= ui
|
||||
.radio_value(&mut self.tack_item_align, Some(Align::Max), "Bottom")
|
||||
.clicked();
|
||||
track_item |= ui
|
||||
.radio_value(&mut self.tack_item_align, None, "None (Bring into view)")
|
||||
.clicked();
|
||||
});
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Scroll to a specific offset:");
|
||||
go_to_scroll_offset |= ui
|
||||
.add(DragValue::new(&mut self.offset).speed(1.0).suffix("px"))
|
||||
.dragged();
|
||||
});
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
scroll_top |= ui.button("Scroll to top").clicked();
|
||||
scroll_bottom |= ui.button("Scroll to bottom").clicked();
|
||||
});
|
||||
|
||||
let mut scroll_area = ScrollArea::vertical()
|
||||
.max_height(200.0)
|
||||
.auto_shrink([false; 2]);
|
||||
if go_to_scroll_offset {
|
||||
scroll_area = scroll_area.vertical_scroll_offset(self.offset);
|
||||
}
|
||||
|
||||
ui.separator();
|
||||
let (current_scroll, max_scroll) = scroll_area
|
||||
.show(ui, |ui| {
|
||||
if scroll_top {
|
||||
ui.scroll_to_cursor(Some(Align::TOP));
|
||||
}
|
||||
ui.vertical(|ui| {
|
||||
for item in 1..=50 {
|
||||
if track_item && item == self.track_item {
|
||||
let response =
|
||||
ui.colored_label(Color32::YELLOW, format!("This is item {}", item));
|
||||
response.scroll_to_me(self.tack_item_align);
|
||||
} else {
|
||||
ui.label(format!("This is item {}", item));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if scroll_bottom {
|
||||
ui.scroll_to_cursor(Some(Align::BOTTOM));
|
||||
}
|
||||
|
||||
let margin = ui.visuals().clip_rect_margin;
|
||||
|
||||
let current_scroll = ui.clip_rect().top() - ui.min_rect().top() + margin;
|
||||
let max_scroll = ui.min_rect().height() - ui.clip_rect().height() + 2.0 * margin;
|
||||
(current_scroll, max_scroll)
|
||||
})
|
||||
.inner;
|
||||
ui.separator();
|
||||
|
||||
ui.label(format!(
|
||||
"Scroll offset: {:.0}/{:.0} px",
|
||||
current_scroll, max_scroll
|
||||
));
|
||||
|
||||
ui.separator();
|
||||
ui.vertical_centered(|ui| {
|
||||
egui::reset_button(ui, self);
|
||||
ui.add(crate::egui_github_link_file!());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
#[derive(Default, PartialEq)]
|
||||
struct ScrollStickTo {
|
||||
n_items: usize,
|
||||
}
|
||||
|
||||
impl super::View for ScrollStickTo {
|
||||
fn ui(&mut self, ui: &mut Ui) {
|
||||
ui.label("Rows enter from the bottom, we want the scroll handle to start and stay at bottom unless moved");
|
||||
|
||||
ui.add_space(4.0);
|
||||
|
||||
let text_style = TextStyle::Body;
|
||||
let row_height = ui.text_style_height(&text_style);
|
||||
ScrollArea::vertical().stick_to_bottom(true).show_rows(
|
||||
ui,
|
||||
row_height,
|
||||
self.n_items,
|
||||
|ui, row_range| {
|
||||
for row in row_range {
|
||||
let text = format!("This is row {}", row + 1);
|
||||
ui.label(text);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
self.n_items += 1;
|
||||
ui.ctx().request_repaint();
|
||||
}
|
||||
}
|
||||
182
crates/egui_demo_lib/src/demo/sliders.rs
Normal file
182
crates/egui_demo_lib/src/demo/sliders.rs
Normal file
@@ -0,0 +1,182 @@
|
||||
use egui::*;
|
||||
use std::f64::INFINITY;
|
||||
|
||||
/// Showcase sliders
|
||||
#[derive(PartialEq)]
|
||||
#[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 clamp_to_range: bool,
|
||||
pub smart_aim: bool,
|
||||
pub step: f64,
|
||||
pub use_steps: bool,
|
||||
pub integer: bool,
|
||||
pub vertical: bool,
|
||||
pub value: f64,
|
||||
}
|
||||
|
||||
impl Default for Sliders {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
min: 0.0,
|
||||
max: 10000.0,
|
||||
logarithmic: true,
|
||||
clamp_to_range: false,
|
||||
smart_aim: true,
|
||||
step: 10.0,
|
||||
use_steps: false,
|
||||
integer: false,
|
||||
vertical: false,
|
||||
value: 10.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl super::Demo for Sliders {
|
||||
fn name(&self) -> &'static str {
|
||||
"⬌ Sliders"
|
||||
}
|
||||
|
||||
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
|
||||
egui::Window::new(self.name())
|
||||
.open(open)
|
||||
.resizable(false)
|
||||
.show(ctx, |ui| {
|
||||
use super::View as _;
|
||||
self.ui(ui);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl super::View for Sliders {
|
||||
fn ui(&mut self, ui: &mut Ui) {
|
||||
let Self {
|
||||
min,
|
||||
max,
|
||||
logarithmic,
|
||||
clamp_to_range,
|
||||
smart_aim,
|
||||
step,
|
||||
use_steps,
|
||||
integer,
|
||||
vertical,
|
||||
value,
|
||||
} = self;
|
||||
|
||||
ui.label("You can click a slider value to edit it with the keyboard.");
|
||||
|
||||
let (type_min, type_max) = 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 = min.clamp(type_min, type_max);
|
||||
*max = max.clamp(type_min, type_max);
|
||||
|
||||
let orientation = if *vertical {
|
||||
SliderOrientation::Vertical
|
||||
} else {
|
||||
SliderOrientation::Horizontal
|
||||
};
|
||||
|
||||
let istep = if *use_steps { *step } else { 0.0 };
|
||||
if *integer {
|
||||
let mut value_i32 = *value as i32;
|
||||
ui.add(
|
||||
Slider::new(&mut value_i32, (*min as i32)..=(*max as i32))
|
||||
.logarithmic(*logarithmic)
|
||||
.clamp_to_range(*clamp_to_range)
|
||||
.smart_aim(*smart_aim)
|
||||
.orientation(orientation)
|
||||
.text("i32 demo slider")
|
||||
.step_by(istep),
|
||||
);
|
||||
*value = value_i32 as f64;
|
||||
} else {
|
||||
ui.add(
|
||||
Slider::new(value, (*min)..=(*max))
|
||||
.logarithmic(*logarithmic)
|
||||
.clamp_to_range(*clamp_to_range)
|
||||
.smart_aim(*smart_aim)
|
||||
.orientation(orientation)
|
||||
.text("f64 demo slider")
|
||||
.step_by(istep),
|
||||
);
|
||||
|
||||
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("Slider range:");
|
||||
ui.add(
|
||||
Slider::new(min, type_min..=type_max)
|
||||
.logarithmic(true)
|
||||
.smart_aim(*smart_aim)
|
||||
.text("left"),
|
||||
);
|
||||
ui.add(
|
||||
Slider::new(max, type_min..=type_max)
|
||||
.logarithmic(true)
|
||||
.smart_aim(*smart_aim)
|
||||
.text("right"),
|
||||
);
|
||||
|
||||
ui.separator();
|
||||
|
||||
ui.checkbox(use_steps, "Use steps");
|
||||
ui.label("When enabled, the minimal value change would be restricted to a given step.");
|
||||
if *use_steps {
|
||||
ui.add(egui::DragValue::new(step).speed(1.0));
|
||||
}
|
||||
|
||||
ui.separator();
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Slider type:");
|
||||
ui.radio_value(integer, true, "i32");
|
||||
ui.radio_value(integer, false, "f64");
|
||||
})
|
||||
.response
|
||||
.on_hover_text("All numeric types (f32, usize, …) are supported.");
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Slider orientation:");
|
||||
ui.radio_value(vertical, false, "Horizontal");
|
||||
ui.radio_value(vertical, true, "Vertical");
|
||||
});
|
||||
ui.add_space(8.0);
|
||||
|
||||
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.add_space(8.0);
|
||||
|
||||
ui.checkbox(clamp_to_range, "Clamp to range");
|
||||
ui.label("If true, the slider will clamp incoming and outgoing values to the given range.");
|
||||
ui.label("If false, the slider can shows values outside its range, and you can manually enter values outside the range.");
|
||||
ui.add_space(8.0);
|
||||
|
||||
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");
|
||||
ui.add_space(8.0);
|
||||
|
||||
ui.vertical_centered(|ui| {
|
||||
egui::reset_button(ui, self);
|
||||
ui.add(crate::egui_github_link_file!());
|
||||
});
|
||||
}
|
||||
}
|
||||
119
crates/egui_demo_lib/src/demo/strip_demo.rs
Normal file
119
crates/egui_demo_lib/src/demo/strip_demo.rs
Normal file
@@ -0,0 +1,119 @@
|
||||
use egui::Color32;
|
||||
use egui_extras::{Size, StripBuilder};
|
||||
|
||||
/// Shows off a table with dynamic layout
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[derive(Default)]
|
||||
pub struct StripDemo {}
|
||||
|
||||
impl super::Demo for StripDemo {
|
||||
fn name(&self) -> &'static str {
|
||||
"▣ Strip Demo"
|
||||
}
|
||||
|
||||
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
|
||||
egui::Window::new(self.name())
|
||||
.open(open)
|
||||
.resizable(true)
|
||||
.default_width(400.0)
|
||||
.show(ctx, |ui| {
|
||||
use super::View as _;
|
||||
self.ui(ui);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl super::View for StripDemo {
|
||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
let dark_mode = ui.visuals().dark_mode;
|
||||
let faded_color = ui.visuals().window_fill();
|
||||
let faded_color = |color: Color32| -> Color32 {
|
||||
use egui::Rgba;
|
||||
let t = if dark_mode { 0.95 } else { 0.8 };
|
||||
egui::lerp(Rgba::from(color)..=Rgba::from(faded_color), t).into()
|
||||
};
|
||||
|
||||
StripBuilder::new(ui)
|
||||
.size(Size::exact(50.0))
|
||||
.size(Size::remainder())
|
||||
.size(Size::relative(0.5).at_least(60.0))
|
||||
.size(Size::exact(10.0))
|
||||
.vertical(|mut strip| {
|
||||
strip.cell(|ui| {
|
||||
ui.painter().rect_filled(
|
||||
ui.available_rect_before_wrap(),
|
||||
0.0,
|
||||
faded_color(Color32::BLUE),
|
||||
);
|
||||
ui.label("width: 100%\nheight: 50px");
|
||||
});
|
||||
strip.strip(|builder| {
|
||||
builder.sizes(Size::remainder(), 2).horizontal(|mut strip| {
|
||||
strip.cell(|ui| {
|
||||
ui.painter().rect_filled(
|
||||
ui.available_rect_before_wrap(),
|
||||
0.0,
|
||||
faded_color(Color32::RED),
|
||||
);
|
||||
ui.label("width: 50%\nheight: remaining");
|
||||
});
|
||||
strip.strip(|builder| {
|
||||
builder.sizes(Size::remainder(), 3).vertical(|mut strip| {
|
||||
strip.empty();
|
||||
strip.cell(|ui| {
|
||||
ui.painter().rect_filled(
|
||||
ui.available_rect_before_wrap(),
|
||||
0.0,
|
||||
faded_color(Color32::YELLOW),
|
||||
);
|
||||
ui.label("width: 50%\nheight: 1/3 of the red region");
|
||||
});
|
||||
strip.empty();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
strip.strip(|builder| {
|
||||
builder
|
||||
.size(Size::remainder())
|
||||
.size(Size::exact(120.0))
|
||||
.size(Size::remainder())
|
||||
.size(Size::exact(70.0))
|
||||
.horizontal(|mut strip| {
|
||||
strip.empty();
|
||||
strip.strip(|builder| {
|
||||
builder
|
||||
.size(Size::remainder())
|
||||
.size(Size::exact(60.0))
|
||||
.size(Size::remainder())
|
||||
.vertical(|mut strip| {
|
||||
strip.empty();
|
||||
strip.cell(|ui| {
|
||||
ui.painter().rect_filled(
|
||||
ui.available_rect_before_wrap(),
|
||||
0.0,
|
||||
faded_color(Color32::GOLD),
|
||||
);
|
||||
ui.label("width: 120px\nheight: 60px");
|
||||
});
|
||||
});
|
||||
});
|
||||
strip.empty();
|
||||
strip.cell(|ui| {
|
||||
ui.painter().rect_filled(
|
||||
ui.available_rect_before_wrap(),
|
||||
0.0,
|
||||
faded_color(Color32::GREEN),
|
||||
);
|
||||
ui.label("width: 70px\n\nheight: 50%, but at least 60px.");
|
||||
});
|
||||
});
|
||||
});
|
||||
strip.cell(|ui| {
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.add(crate::egui_github_link_file!());
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
199
crates/egui_demo_lib/src/demo/table_demo.rs
Normal file
199
crates/egui_demo_lib/src/demo/table_demo.rs
Normal file
@@ -0,0 +1,199 @@
|
||||
#[derive(PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
enum DemoType {
|
||||
Manual,
|
||||
ManyHomogenous,
|
||||
ManyHeterogenous,
|
||||
}
|
||||
|
||||
/// Shows off a table with dynamic layout
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct TableDemo {
|
||||
demo: DemoType,
|
||||
resizable: bool,
|
||||
num_rows: usize,
|
||||
}
|
||||
|
||||
impl Default for TableDemo {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
demo: DemoType::Manual,
|
||||
resizable: true,
|
||||
num_rows: 10_000,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl super::Demo for TableDemo {
|
||||
fn name(&self) -> &'static str {
|
||||
"☰ Table Demo"
|
||||
}
|
||||
|
||||
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
|
||||
egui::Window::new(self.name())
|
||||
.open(open)
|
||||
.resizable(true)
|
||||
.default_width(400.0)
|
||||
.show(ctx, |ui| {
|
||||
use super::View as _;
|
||||
self.ui(ui);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl super::View for TableDemo {
|
||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
ui.vertical(|ui| {
|
||||
ui.checkbox(&mut self.resizable, "Resizable columns");
|
||||
|
||||
ui.label("Table type:");
|
||||
ui.radio_value(&mut self.demo, DemoType::Manual, "Few, manual rows");
|
||||
ui.radio_value(
|
||||
&mut self.demo,
|
||||
DemoType::ManyHomogenous,
|
||||
"Thousands of rows of same height",
|
||||
);
|
||||
ui.radio_value(
|
||||
&mut self.demo,
|
||||
DemoType::ManyHeterogenous,
|
||||
"Thousands of rows of differing heights",
|
||||
);
|
||||
|
||||
if self.demo != DemoType::Manual {
|
||||
ui.add(
|
||||
egui::Slider::new(&mut self.num_rows, 0..=100_000)
|
||||
.logarithmic(true)
|
||||
.text("Num rows"),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
ui.separator();
|
||||
|
||||
// Leave room for the source code link after the table demo:
|
||||
use egui_extras::{Size, StripBuilder};
|
||||
StripBuilder::new(ui)
|
||||
.size(Size::remainder()) // for the table
|
||||
.size(Size::exact(10.0)) // for the source code link
|
||||
.vertical(|mut strip| {
|
||||
strip.cell(|ui| {
|
||||
self.table_ui(ui);
|
||||
});
|
||||
strip.cell(|ui| {
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.add(crate::egui_github_link_file!());
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl TableDemo {
|
||||
fn table_ui(&mut self, ui: &mut egui::Ui) {
|
||||
use egui_extras::{Size, TableBuilder};
|
||||
|
||||
let text_height = egui::TextStyle::Body.resolve(ui.style()).size;
|
||||
|
||||
TableBuilder::new(ui)
|
||||
.striped(true)
|
||||
.cell_layout(egui::Layout::left_to_right(egui::Align::Center))
|
||||
.column(Size::initial(60.0).at_least(40.0))
|
||||
.column(Size::initial(60.0).at_least(40.0))
|
||||
.column(Size::remainder().at_least(60.0))
|
||||
.resizable(self.resizable)
|
||||
.header(20.0, |mut header| {
|
||||
header.col(|ui| {
|
||||
ui.heading("Row");
|
||||
});
|
||||
header.col(|ui| {
|
||||
ui.heading("Clock");
|
||||
});
|
||||
header.col(|ui| {
|
||||
ui.heading("Content");
|
||||
});
|
||||
})
|
||||
.body(|mut body| match self.demo {
|
||||
DemoType::Manual => {
|
||||
for row_index in 0..20 {
|
||||
let is_thick = thick_row(row_index);
|
||||
let row_height = if is_thick { 30.0 } else { 18.0 };
|
||||
body.row(row_height, |mut row| {
|
||||
row.col(|ui| {
|
||||
ui.label(row_index.to_string());
|
||||
});
|
||||
row.col(|ui| {
|
||||
ui.label(clock_emoji(row_index));
|
||||
});
|
||||
row.col(|ui| {
|
||||
ui.style_mut().wrap = Some(false);
|
||||
if is_thick {
|
||||
ui.heading("Extra thick row");
|
||||
} else {
|
||||
ui.label("Normal row");
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
DemoType::ManyHomogenous => {
|
||||
body.rows(text_height, self.num_rows, |row_index, mut row| {
|
||||
row.col(|ui| {
|
||||
ui.label(row_index.to_string());
|
||||
});
|
||||
row.col(|ui| {
|
||||
ui.label(clock_emoji(row_index));
|
||||
});
|
||||
row.col(|ui| {
|
||||
ui.add(
|
||||
egui::Label::new("Thousands of rows of even height").wrap(false),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
DemoType::ManyHeterogenous => {
|
||||
fn row_thickness(row_index: usize) -> f32 {
|
||||
if thick_row(row_index) {
|
||||
30.0
|
||||
} else {
|
||||
18.0
|
||||
}
|
||||
}
|
||||
body.heterogeneous_rows(
|
||||
(0..self.num_rows).into_iter().map(row_thickness),
|
||||
|row_index, mut row| {
|
||||
row.col(|ui| {
|
||||
ui.centered_and_justified(|ui| {
|
||||
ui.label(row_index.to_string());
|
||||
});
|
||||
});
|
||||
row.col(|ui| {
|
||||
ui.centered_and_justified(|ui| {
|
||||
ui.label(clock_emoji(row_index));
|
||||
});
|
||||
});
|
||||
row.col(|ui| {
|
||||
ui.centered_and_justified(|ui| {
|
||||
ui.style_mut().wrap = Some(false);
|
||||
if thick_row(row_index) {
|
||||
ui.heading("Extra thick row");
|
||||
} else {
|
||||
ui.label("Normal row");
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn clock_emoji(row_index: usize) -> String {
|
||||
char::from_u32(0x1f550 + row_index as u32 % 24)
|
||||
.unwrap()
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn thick_row(row_index: usize) -> bool {
|
||||
row_index % 6 == 0
|
||||
}
|
||||
480
crates/egui_demo_lib/src/demo/tests.rs
Normal file
480
crates/egui_demo_lib/src/demo/tests.rs
Normal file
@@ -0,0 +1,480 @@
|
||||
#[derive(Default)]
|
||||
pub struct CursorTest {}
|
||||
|
||||
impl super::Demo for CursorTest {
|
||||
fn name(&self) -> &'static str {
|
||||
"Cursor Test"
|
||||
}
|
||||
|
||||
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
|
||||
egui::Window::new(self.name()).open(open).show(ctx, |ui| {
|
||||
use super::View as _;
|
||||
self.ui(ui);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl super::View for CursorTest {
|
||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
ui.vertical_centered_justified(|ui| {
|
||||
ui.heading("Hover to switch cursor icon:");
|
||||
for &cursor_icon in &egui::CursorIcon::ALL {
|
||||
let _ = ui
|
||||
.button(format!("{:?}", cursor_icon))
|
||||
.on_hover_cursor(cursor_icon);
|
||||
}
|
||||
ui.add(crate::egui_github_link_file!());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct IdTest {}
|
||||
|
||||
impl super::Demo for IdTest {
|
||||
fn name(&self) -> &'static str {
|
||||
"ID Test"
|
||||
}
|
||||
|
||||
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
|
||||
egui::Window::new(self.name()).open(open).show(ctx, |ui| {
|
||||
use super::View as _;
|
||||
self.ui(ui);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl super::View for IdTest {
|
||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
ui.heading("Name collision example");
|
||||
|
||||
ui.label("\
|
||||
Widgets that store state require unique and persisting identifiers so we can track their state between frames.\n\
|
||||
For instance, collapsable headers needs to store whether 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.vertical_centered(|ui| {
|
||||
ui.add(crate::egui_github_link_file!());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
enum WidgetType {
|
||||
Label,
|
||||
Button,
|
||||
TextEdit,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct ManualLayoutTest {
|
||||
widget_offset: egui::Vec2,
|
||||
widget_size: egui::Vec2,
|
||||
widget_type: WidgetType,
|
||||
text_edit_contents: String,
|
||||
}
|
||||
|
||||
impl Default for ManualLayoutTest {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
widget_offset: egui::Vec2::splat(150.0),
|
||||
widget_size: egui::vec2(200.0, 100.0),
|
||||
widget_type: WidgetType::Button,
|
||||
text_edit_contents: crate::LOREM_IPSUM.to_owned(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl super::Demo for ManualLayoutTest {
|
||||
fn name(&self) -> &'static str {
|
||||
"Manual Layout Test"
|
||||
}
|
||||
|
||||
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
|
||||
egui::Window::new(self.name())
|
||||
.resizable(false)
|
||||
.open(open)
|
||||
.show(ctx, |ui| {
|
||||
use super::View as _;
|
||||
self.ui(ui);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl super::View for ManualLayoutTest {
|
||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
egui::reset_button(ui, self);
|
||||
|
||||
let Self {
|
||||
widget_offset,
|
||||
widget_size,
|
||||
widget_type,
|
||||
text_edit_contents,
|
||||
} = self;
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Test widget:");
|
||||
ui.radio_value(widget_type, WidgetType::Button, "Button");
|
||||
ui.radio_value(widget_type, WidgetType::Label, "Label");
|
||||
ui.radio_value(widget_type, WidgetType::TextEdit, "TextEdit");
|
||||
});
|
||||
egui::Grid::new("pos_size").show(ui, |ui| {
|
||||
ui.label("Widget position:");
|
||||
ui.add(egui::Slider::new(&mut widget_offset.x, 0.0..=400.0));
|
||||
ui.add(egui::Slider::new(&mut widget_offset.y, 0.0..=400.0));
|
||||
ui.end_row();
|
||||
|
||||
ui.label("Widget size:");
|
||||
ui.add(egui::Slider::new(&mut widget_size.x, 0.0..=400.0));
|
||||
ui.add(egui::Slider::new(&mut widget_size.y, 0.0..=400.0));
|
||||
ui.end_row();
|
||||
});
|
||||
|
||||
let widget_rect =
|
||||
egui::Rect::from_min_size(ui.min_rect().min + *widget_offset, *widget_size);
|
||||
|
||||
ui.add(crate::egui_github_link_file!());
|
||||
|
||||
// Showing how to place a widget anywhere in the [`Ui`]:
|
||||
match *widget_type {
|
||||
WidgetType::Button => {
|
||||
ui.put(widget_rect, egui::Button::new("Example button"));
|
||||
}
|
||||
WidgetType::Label => {
|
||||
ui.put(widget_rect, egui::Label::new("Example label"));
|
||||
}
|
||||
WidgetType::TextEdit => {
|
||||
ui.put(widget_rect, egui::TextEdit::multiline(text_edit_contents));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[derive(PartialEq)]
|
||||
pub struct TableTest {
|
||||
num_cols: usize,
|
||||
num_rows: usize,
|
||||
min_col_width: f32,
|
||||
max_col_width: f32,
|
||||
text_length: usize,
|
||||
}
|
||||
|
||||
impl Default for TableTest {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
num_cols: 4,
|
||||
num_rows: 4,
|
||||
min_col_width: 10.0,
|
||||
max_col_width: 200.0,
|
||||
text_length: 10,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl super::Demo for TableTest {
|
||||
fn name(&self) -> &'static str {
|
||||
"Table Test"
|
||||
}
|
||||
|
||||
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
|
||||
egui::Window::new(self.name()).open(open).show(ctx, |ui| {
|
||||
use super::View as _;
|
||||
self.ui(ui);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl super::View for TableTest {
|
||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
ui.add(
|
||||
egui::Slider::new(&mut self.min_col_width, 0.0..=400.0).text("Minimum column width"),
|
||||
);
|
||||
ui.add(
|
||||
egui::Slider::new(&mut self.max_col_width, 0.0..=400.0).text("Maximum column width"),
|
||||
);
|
||||
ui.add(egui::Slider::new(&mut self.num_cols, 0..=5).text("Columns"));
|
||||
ui.add(egui::Slider::new(&mut self.num_rows, 0..=20).text("Rows"));
|
||||
|
||||
ui.separator();
|
||||
|
||||
let words = [
|
||||
"random", "words", "in", "a", "random", "order", "that", "just", "keeps", "going",
|
||||
"with", "some", "more",
|
||||
];
|
||||
|
||||
egui::Grid::new("my_grid")
|
||||
.striped(true)
|
||||
.min_col_width(self.min_col_width)
|
||||
.max_col_width(self.max_col_width)
|
||||
.show(ui, |ui| {
|
||||
for row in 0..self.num_rows {
|
||||
for col in 0..self.num_cols {
|
||||
if col == 0 {
|
||||
ui.label(format!("row {}", row));
|
||||
} else {
|
||||
let word_idx = row * 3 + col * 5;
|
||||
let word_count = (row * 5 + col * 75) % 13;
|
||||
let mut string = String::new();
|
||||
for word in words.iter().cycle().skip(word_idx).take(word_count) {
|
||||
string += word;
|
||||
string += " ";
|
||||
}
|
||||
ui.label(string);
|
||||
}
|
||||
}
|
||||
ui.end_row();
|
||||
}
|
||||
});
|
||||
|
||||
ui.separator();
|
||||
ui.add(egui::Slider::new(&mut self.text_length, 1..=40).text("Text length"));
|
||||
egui::Grid::new("parent grid").striped(true).show(ui, |ui| {
|
||||
ui.vertical(|ui| {
|
||||
ui.label("Vertical nest1");
|
||||
ui.label("Vertical nest2");
|
||||
});
|
||||
ui.label("First row, second column");
|
||||
ui.end_row();
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Horizontal nest1");
|
||||
ui.label("Horizontal nest2");
|
||||
});
|
||||
ui.label("Second row, second column");
|
||||
ui.end_row();
|
||||
|
||||
ui.scope(|ui| {
|
||||
ui.label("Scope nest 1");
|
||||
ui.label("Scope nest 2");
|
||||
});
|
||||
ui.label("Third row, second column");
|
||||
ui.end_row();
|
||||
|
||||
egui::Grid::new("nested grid").show(ui, |ui| {
|
||||
ui.label("Grid nest11");
|
||||
ui.label("Grid nest12");
|
||||
ui.end_row();
|
||||
ui.label("Grid nest21");
|
||||
ui.label("Grid nest22");
|
||||
ui.end_row();
|
||||
});
|
||||
ui.label("Fourth row, second column");
|
||||
ui.end_row();
|
||||
|
||||
let mut dyn_text = String::from("O");
|
||||
dyn_text.extend(std::iter::repeat('h').take(self.text_length));
|
||||
ui.label(dyn_text);
|
||||
ui.label("Fifth row, second column");
|
||||
ui.end_row();
|
||||
});
|
||||
|
||||
ui.vertical_centered(|ui| {
|
||||
egui::reset_button(ui, self);
|
||||
ui.add(crate::egui_github_link_file!());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[derive(Default)]
|
||||
pub struct InputTest {
|
||||
info: String,
|
||||
}
|
||||
|
||||
impl super::Demo for InputTest {
|
||||
fn name(&self) -> &'static str {
|
||||
"Input Test"
|
||||
}
|
||||
|
||||
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
|
||||
egui::Window::new(self.name())
|
||||
.open(open)
|
||||
.resizable(false)
|
||||
.show(ctx, |ui| {
|
||||
use super::View as _;
|
||||
self.ui(ui);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl super::View for InputTest {
|
||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.add(crate::egui_github_link_file!());
|
||||
});
|
||||
|
||||
let response = ui.add(
|
||||
egui::Button::new("Click, double-click, triple-click or drag me with any mouse button")
|
||||
.sense(egui::Sense::click_and_drag()),
|
||||
);
|
||||
|
||||
let mut new_info = String::new();
|
||||
for &button in &[
|
||||
egui::PointerButton::Primary,
|
||||
egui::PointerButton::Secondary,
|
||||
egui::PointerButton::Middle,
|
||||
egui::PointerButton::Extra1,
|
||||
egui::PointerButton::Extra2,
|
||||
] {
|
||||
use std::fmt::Write as _;
|
||||
|
||||
if response.clicked_by(button) {
|
||||
writeln!(new_info, "Clicked by {:?} button", button).ok();
|
||||
}
|
||||
if response.double_clicked_by(button) {
|
||||
writeln!(new_info, "Double-clicked by {:?} button", button).ok();
|
||||
}
|
||||
if response.triple_clicked_by(button) {
|
||||
writeln!(new_info, "Triple-clicked by {:?} button", button).ok();
|
||||
}
|
||||
if response.dragged_by(button) {
|
||||
writeln!(
|
||||
new_info,
|
||||
"Dragged by {:?} button, delta: {:?}",
|
||||
button,
|
||||
response.drag_delta()
|
||||
)
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
if !new_info.is_empty() {
|
||||
self.info = new_info;
|
||||
}
|
||||
|
||||
ui.label(&self.info);
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
pub struct WindowResizeTest {
|
||||
text: String,
|
||||
}
|
||||
|
||||
impl Default for WindowResizeTest {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
text: crate::LOREM_IPSUM_LONG.to_owned(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl super::Demo for WindowResizeTest {
|
||||
fn name(&self) -> &'static str {
|
||||
"↔ Window Resize"
|
||||
}
|
||||
|
||||
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
|
||||
use egui::*;
|
||||
|
||||
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| {
|
||||
lorem_ipsum(ui, crate::LOREM_IPSUM);
|
||||
});
|
||||
ui.heading("Resize the above area!");
|
||||
});
|
||||
|
||||
Window::new("↔ resizable + scroll")
|
||||
.open(open)
|
||||
.vscroll(true)
|
||||
.resizable(true)
|
||||
.default_height(300.0)
|
||||
.show(ctx, |ui| {
|
||||
ui.label(
|
||||
"This window is resizable and has a scroll area. You can shrink it to any size.",
|
||||
);
|
||||
ui.separator();
|
||||
lorem_ipsum(ui, crate::LOREM_IPSUM_LONG);
|
||||
});
|
||||
|
||||
Window::new("↔ resizable + embedded scroll")
|
||||
.open(open)
|
||||
.vscroll(false)
|
||||
.resizable(true)
|
||||
.default_height(300.0)
|
||||
.show(ctx, |ui| {
|
||||
ui.label("This window is resizable but has no built-in scroll area.");
|
||||
ui.label("However, we have a sub-region with a scroll bar:");
|
||||
ui.separator();
|
||||
ScrollArea::vertical().show(ui, |ui| {
|
||||
let lorem_ipsum_extra_long =
|
||||
format!("{}\n\n{}", crate::LOREM_IPSUM_LONG, crate::LOREM_IPSUM_LONG);
|
||||
lorem_ipsum(ui, &lorem_ipsum_extra_long);
|
||||
});
|
||||
// ui.heading("Some additional text here, that should also be visible"); // this works, but messes with the resizing a bit
|
||||
});
|
||||
|
||||
Window::new("↔ resizable without scroll")
|
||||
.open(open)
|
||||
.vscroll(false)
|
||||
.resizable(true)
|
||||
.show(ctx, |ui| {
|
||||
ui.label("This window is resizable but has no scroll area. This means it can only be resized to a size where all the contents is visible.");
|
||||
ui.label("egui will not clip the contents of a window, nor add whitespace to it.");
|
||||
ui.separator();
|
||||
lorem_ipsum(ui, crate::LOREM_IPSUM);
|
||||
});
|
||||
|
||||
Window::new("↔ resizable with TextEdit")
|
||||
.open(open)
|
||||
.vscroll(false)
|
||||
.resizable(true)
|
||||
.default_height(300.0)
|
||||
.show(ctx, |ui| {
|
||||
ui.label("Shows how you can fill an area with a widget.");
|
||||
ui.add_sized(ui.available_size(), TextEdit::multiline(&mut self.text));
|
||||
});
|
||||
|
||||
Window::new("↔ freely resized")
|
||||
.open(open)
|
||||
.vscroll(false)
|
||||
.resizable(true)
|
||||
.default_size([250.0, 150.0])
|
||||
.show(ctx, |ui| {
|
||||
ui.label("This window has empty space that fills up the available space, preventing auto-shrink.");
|
||||
ui.allocate_space(ui.available_size());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn lorem_ipsum(ui: &mut egui::Ui, text: &str) {
|
||||
ui.with_layout(
|
||||
egui::Layout::top_down(egui::Align::LEFT).with_cross_justify(true),
|
||||
|ui| {
|
||||
ui.label(egui::RichText::new(text).weak());
|
||||
},
|
||||
);
|
||||
}
|
||||
111
crates/egui_demo_lib/src/demo/text_edit.rs
Normal file
111
crates/egui_demo_lib/src/demo/text_edit.rs
Normal file
@@ -0,0 +1,111 @@
|
||||
/// Showcase [`TextEdit`].
|
||||
#[derive(PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
pub struct TextEdit {
|
||||
pub text: String,
|
||||
}
|
||||
|
||||
impl Default for TextEdit {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
text: "Edit this text".to_owned(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl super::Demo for TextEdit {
|
||||
fn name(&self) -> &'static str {
|
||||
"🖹 TextEdit"
|
||||
}
|
||||
|
||||
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
|
||||
egui::Window::new(self.name())
|
||||
.open(open)
|
||||
.resizable(false)
|
||||
.show(ctx, |ui| {
|
||||
use super::View as _;
|
||||
self.ui(ui);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl super::View for TextEdit {
|
||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
let Self { text } = self;
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = 0.0;
|
||||
ui.label("Advanced usage of ");
|
||||
ui.code("TextEdit");
|
||||
ui.label(".");
|
||||
});
|
||||
|
||||
let output = egui::TextEdit::multiline(text)
|
||||
.hint_text("Type something!")
|
||||
.show(ui);
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = 0.0;
|
||||
ui.label("Selected text: ");
|
||||
if let Some(text_cursor_range) = output.cursor_range {
|
||||
use egui::TextBuffer as _;
|
||||
let selected_chars = text_cursor_range.as_sorted_char_range();
|
||||
let selected_text = text.char_range(selected_chars);
|
||||
ui.code(selected_text);
|
||||
}
|
||||
});
|
||||
|
||||
let anything_selected = output
|
||||
.cursor_range
|
||||
.map_or(false, |cursor| !cursor.is_empty());
|
||||
|
||||
ui.add_enabled(
|
||||
anything_selected,
|
||||
egui::Label::new("Press ctrl+Y to toggle the case of selected text (cmd+Y on Mac)"),
|
||||
);
|
||||
|
||||
if ui
|
||||
.input_mut()
|
||||
.consume_key(egui::Modifiers::COMMAND, egui::Key::Y)
|
||||
{
|
||||
if let Some(text_cursor_range) = output.cursor_range {
|
||||
use egui::TextBuffer as _;
|
||||
let selected_chars = text_cursor_range.as_sorted_char_range();
|
||||
let selected_text = text.char_range(selected_chars.clone());
|
||||
let upper_case = selected_text.to_uppercase();
|
||||
let new_text = if selected_text == upper_case {
|
||||
selected_text.to_lowercase()
|
||||
} else {
|
||||
upper_case
|
||||
};
|
||||
text.delete_char_range(selected_chars.clone());
|
||||
text.insert_text(&new_text, selected_chars.start);
|
||||
}
|
||||
}
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Move cursor to the:");
|
||||
|
||||
if ui.button("start").clicked() {
|
||||
let text_edit_id = output.response.id;
|
||||
if let Some(mut state) = egui::TextEdit::load_state(ui.ctx(), text_edit_id) {
|
||||
let ccursor = egui::text::CCursor::new(0);
|
||||
state.set_ccursor_range(Some(egui::text::CCursorRange::one(ccursor)));
|
||||
state.store(ui.ctx(), text_edit_id);
|
||||
ui.ctx().memory().request_focus(text_edit_id); // give focus back to the [`TextEdit`].
|
||||
}
|
||||
}
|
||||
|
||||
if ui.button("end").clicked() {
|
||||
let text_edit_id = output.response.id;
|
||||
if let Some(mut state) = egui::TextEdit::load_state(ui.ctx(), text_edit_id) {
|
||||
let ccursor = egui::text::CCursor::new(text.chars().count());
|
||||
state.set_ccursor_range(Some(egui::text::CCursorRange::one(ccursor)));
|
||||
state.store(ui.ctx(), text_edit_id);
|
||||
ui.ctx().memory().request_focus(text_edit_id); // give focus back to the [`TextEdit`].
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
111
crates/egui_demo_lib/src/demo/toggle_switch.rs
Normal file
111
crates/egui_demo_lib/src/demo/toggle_switch.rs
Normal file
@@ -0,0 +1,111 @@
|
||||
//! Source code example of how to create your own widget.
|
||||
//! This is meant to be read as a tutorial, hence the plethora of comments.
|
||||
|
||||
/// iOS-style toggle switch:
|
||||
///
|
||||
/// ``` text
|
||||
/// _____________
|
||||
/// / /.....\
|
||||
/// | |.......|
|
||||
/// \_______\_____/
|
||||
/// ```
|
||||
///
|
||||
/// ## Example:
|
||||
/// ``` ignore
|
||||
/// toggle_ui(ui, &mut my_bool);
|
||||
/// ```
|
||||
pub fn toggle_ui(ui: &mut egui::Ui, on: &mut bool) -> egui::Response {
|
||||
// Widget code can be broken up in four steps:
|
||||
// 1. Decide a size for the widget
|
||||
// 2. Allocate space for it
|
||||
// 3. Handle interactions with the widget (if any)
|
||||
// 4. Paint the widget
|
||||
|
||||
// 1. Deciding widget size:
|
||||
// You can query the `ui` how much space is available,
|
||||
// but in this example we have a fixed size widget based on the height of a standard button:
|
||||
let desired_size = ui.spacing().interact_size.y * egui::vec2(2.0, 1.0);
|
||||
|
||||
// 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 (rect, mut response) = ui.allocate_exact_size(desired_size, egui::Sense::click());
|
||||
|
||||
// 3. Interact: Time to check for clicks!
|
||||
if response.clicked() {
|
||||
*on = !*on;
|
||||
response.mark_changed(); // report back that the value changed
|
||||
}
|
||||
|
||||
// Attach some meta-data to the response which can be used by screen readers:
|
||||
response.widget_info(|| egui::WidgetInfo::selected(egui::WidgetType::Checkbox, *on, ""));
|
||||
|
||||
// 4. Paint!
|
||||
// Make sure we need to paint:
|
||||
if ui.is_rect_visible(rect) {
|
||||
// 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_selectable(&response, *on);
|
||||
// All coordinates are in absolute screen coordinates so we use `rect` to place the elements.
|
||||
let rect = rect.expand(visuals.expansion);
|
||||
let radius = 0.5 * rect.height();
|
||||
ui.painter()
|
||||
.rect(rect, radius, visuals.bg_fill, visuals.bg_stroke);
|
||||
// Paint the circle, animating it from left to right with `how_on`:
|
||||
let circle_x = egui::lerp((rect.left() + radius)..=(rect.right() - radius), how_on);
|
||||
let center = egui::pos2(circle_x, rect.center().y);
|
||||
ui.painter()
|
||||
.circle(center, 0.75 * radius, visuals.bg_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_ui_compact(ui: &mut egui::Ui, on: &mut bool) -> egui::Response {
|
||||
let desired_size = ui.spacing().interact_size.y * egui::vec2(2.0, 1.0);
|
||||
let (rect, mut response) = ui.allocate_exact_size(desired_size, egui::Sense::click());
|
||||
if response.clicked() {
|
||||
*on = !*on;
|
||||
response.mark_changed();
|
||||
}
|
||||
response.widget_info(|| egui::WidgetInfo::selected(egui::WidgetType::Checkbox, *on, ""));
|
||||
|
||||
if ui.is_rect_visible(rect) {
|
||||
let how_on = ui.ctx().animate_bool(response.id, *on);
|
||||
let visuals = ui.style().interact_selectable(&response, *on);
|
||||
let rect = rect.expand(visuals.expansion);
|
||||
let radius = 0.5 * rect.height();
|
||||
ui.painter()
|
||||
.rect(rect, radius, visuals.bg_fill, visuals.bg_stroke);
|
||||
let circle_x = egui::lerp((rect.left() + radius)..=(rect.right() - radius), how_on);
|
||||
let center = egui::pos2(circle_x, rect.center().y);
|
||||
ui.painter()
|
||||
.circle(center, 0.75 * radius, visuals.bg_fill, visuals.fg_stroke);
|
||||
}
|
||||
|
||||
response
|
||||
}
|
||||
|
||||
// A wrapper that allows the more idiomatic usage pattern: `ui.add(toggle(&mut my_bool))`
|
||||
/// iOS-style toggle switch.
|
||||
///
|
||||
/// ## Example:
|
||||
/// ``` ignore
|
||||
/// ui.add(toggle(&mut my_bool));
|
||||
/// ```
|
||||
pub fn toggle(on: &mut bool) -> impl egui::Widget + '_ {
|
||||
move |ui: &mut egui::Ui| toggle_ui(ui, on)
|
||||
}
|
||||
|
||||
pub fn url_to_file_source_code() -> String {
|
||||
format!("https://github.com/emilk/egui/blob/master/{}", file!())
|
||||
}
|
||||
291
crates/egui_demo_lib/src/demo/widget_gallery.rs
Normal file
291
crates/egui_demo_lib/src/demo/widget_gallery.rs
Normal file
@@ -0,0 +1,291 @@
|
||||
#[derive(Debug, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
enum Enum {
|
||||
First,
|
||||
Second,
|
||||
Third,
|
||||
}
|
||||
|
||||
/// Shows off one example of each major type of widget.
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct WidgetGallery {
|
||||
enabled: bool,
|
||||
visible: bool,
|
||||
boolean: bool,
|
||||
radio: Enum,
|
||||
scalar: f32,
|
||||
string: String,
|
||||
color: egui::Color32,
|
||||
animate_progress_bar: bool,
|
||||
|
||||
#[cfg(feature = "chrono")]
|
||||
#[cfg_attr(feature = "serde", serde(skip))]
|
||||
date: Option<chrono::Date<chrono::Utc>>,
|
||||
|
||||
#[cfg_attr(feature = "serde", serde(skip))]
|
||||
texture: Option<egui::TextureHandle>,
|
||||
}
|
||||
|
||||
impl Default for WidgetGallery {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: true,
|
||||
visible: true,
|
||||
boolean: false,
|
||||
radio: Enum::First,
|
||||
scalar: 42.0,
|
||||
string: Default::default(),
|
||||
color: egui::Color32::LIGHT_BLUE.linear_multiply(0.5),
|
||||
animate_progress_bar: false,
|
||||
#[cfg(feature = "chrono")]
|
||||
date: None,
|
||||
texture: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl super::Demo for WidgetGallery {
|
||||
fn name(&self) -> &'static str {
|
||||
"🗄 Widget Gallery"
|
||||
}
|
||||
|
||||
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
|
||||
egui::Window::new(self.name())
|
||||
.open(open)
|
||||
.resizable(true)
|
||||
.default_width(280.0)
|
||||
.show(ctx, |ui| {
|
||||
use super::View as _;
|
||||
self.ui(ui);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl super::View for WidgetGallery {
|
||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
ui.add_enabled_ui(self.enabled, |ui| {
|
||||
ui.set_visible(self.visible);
|
||||
|
||||
egui::Grid::new("my_grid")
|
||||
.num_columns(2)
|
||||
.spacing([40.0, 4.0])
|
||||
.striped(true)
|
||||
.show(ui, |ui| {
|
||||
self.gallery_grid_contents(ui);
|
||||
});
|
||||
});
|
||||
|
||||
ui.separator();
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.checkbox(&mut self.visible, "Visible")
|
||||
.on_hover_text("Uncheck to hide all the widgets.");
|
||||
if self.visible {
|
||||
ui.checkbox(&mut self.enabled, "Interactive")
|
||||
.on_hover_text("Uncheck to inspect how the widgets look when disabled.");
|
||||
}
|
||||
});
|
||||
|
||||
ui.separator();
|
||||
|
||||
ui.vertical_centered(|ui| {
|
||||
let tooltip_text = "The full egui documentation.\nYou can also click the different widgets names in the left column.";
|
||||
ui.hyperlink("https://docs.rs/egui/").on_hover_text(tooltip_text);
|
||||
ui.add(crate::egui_github_link_file!(
|
||||
"Source code of the widget gallery"
|
||||
));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl WidgetGallery {
|
||||
fn gallery_grid_contents(&mut self, ui: &mut egui::Ui) {
|
||||
let Self {
|
||||
enabled: _,
|
||||
visible: _,
|
||||
boolean,
|
||||
radio,
|
||||
scalar,
|
||||
string,
|
||||
color,
|
||||
animate_progress_bar,
|
||||
#[cfg(feature = "chrono")]
|
||||
date,
|
||||
texture,
|
||||
} = self;
|
||||
|
||||
let texture: &egui::TextureHandle = texture.get_or_insert_with(|| {
|
||||
ui.ctx().load_texture(
|
||||
"example",
|
||||
egui::ColorImage::example(),
|
||||
egui::TextureFilter::Linear,
|
||||
)
|
||||
});
|
||||
|
||||
ui.add(doc_link_label("Label", "label,heading"));
|
||||
ui.label("Welcome to the widget gallery!");
|
||||
ui.end_row();
|
||||
|
||||
ui.add(doc_link_label("Hyperlink", "Hyperlink"));
|
||||
use egui::special_emojis::GITHUB;
|
||||
ui.hyperlink_to(
|
||||
format!("{} egui on GitHub", GITHUB),
|
||||
"https://github.com/emilk/egui",
|
||||
);
|
||||
ui.end_row();
|
||||
|
||||
ui.add(doc_link_label("TextEdit", "TextEdit,text_edit"));
|
||||
ui.add(egui::TextEdit::singleline(string).hint_text("Write something here"));
|
||||
ui.end_row();
|
||||
|
||||
ui.add(doc_link_label("Button", "button"));
|
||||
if ui.button("Click me!").clicked() {
|
||||
*boolean = !*boolean;
|
||||
}
|
||||
ui.end_row();
|
||||
|
||||
ui.add(doc_link_label("Link", "link"));
|
||||
if ui.link("Click me!").clicked() {
|
||||
*boolean = !*boolean;
|
||||
}
|
||||
ui.end_row();
|
||||
|
||||
ui.add(doc_link_label("Checkbox", "checkbox"));
|
||||
ui.checkbox(boolean, "Checkbox");
|
||||
ui.end_row();
|
||||
|
||||
ui.add(doc_link_label("RadioButton", "radio"));
|
||||
ui.horizontal(|ui| {
|
||||
ui.radio_value(radio, Enum::First, "First");
|
||||
ui.radio_value(radio, Enum::Second, "Second");
|
||||
ui.radio_value(radio, Enum::Third, "Third");
|
||||
});
|
||||
ui.end_row();
|
||||
|
||||
ui.add(doc_link_label(
|
||||
"SelectableLabel",
|
||||
"selectable_value,SelectableLabel",
|
||||
));
|
||||
ui.horizontal(|ui| {
|
||||
ui.selectable_value(radio, Enum::First, "First");
|
||||
ui.selectable_value(radio, Enum::Second, "Second");
|
||||
ui.selectable_value(radio, Enum::Third, "Third");
|
||||
});
|
||||
ui.end_row();
|
||||
|
||||
ui.add(doc_link_label("ComboBox", "ComboBox"));
|
||||
|
||||
egui::ComboBox::from_label("Take your pick")
|
||||
.selected_text(format!("{:?}", radio))
|
||||
.show_ui(ui, |ui| {
|
||||
ui.selectable_value(radio, Enum::First, "First");
|
||||
ui.selectable_value(radio, Enum::Second, "Second");
|
||||
ui.selectable_value(radio, Enum::Third, "Third");
|
||||
});
|
||||
ui.end_row();
|
||||
|
||||
ui.add(doc_link_label("Slider", "Slider"));
|
||||
ui.add(egui::Slider::new(scalar, 0.0..=360.0).suffix("°"));
|
||||
ui.end_row();
|
||||
|
||||
ui.add(doc_link_label("DragValue", "DragValue"));
|
||||
ui.add(egui::DragValue::new(scalar).speed(1.0));
|
||||
ui.end_row();
|
||||
|
||||
ui.add(doc_link_label("ProgressBar", "ProgressBar"));
|
||||
let progress = *scalar / 360.0;
|
||||
let progress_bar = egui::ProgressBar::new(progress)
|
||||
.show_percentage()
|
||||
.animate(*animate_progress_bar);
|
||||
*animate_progress_bar = ui
|
||||
.add(progress_bar)
|
||||
.on_hover_text("The progress bar can be animated!")
|
||||
.hovered();
|
||||
ui.end_row();
|
||||
|
||||
ui.add(doc_link_label("Color picker", "color_edit"));
|
||||
ui.color_edit_button_srgba(color);
|
||||
ui.end_row();
|
||||
|
||||
let img_size = 16.0 * texture.size_vec2() / texture.size_vec2().y;
|
||||
|
||||
ui.add(doc_link_label("Image", "Image"));
|
||||
ui.image(texture, img_size);
|
||||
ui.end_row();
|
||||
|
||||
ui.add(doc_link_label("ImageButton", "ImageButton"));
|
||||
if ui.add(egui::ImageButton::new(texture, img_size)).clicked() {
|
||||
*boolean = !*boolean;
|
||||
}
|
||||
ui.end_row();
|
||||
|
||||
#[cfg(feature = "chrono")]
|
||||
{
|
||||
let date = date.get_or_insert_with(|| chrono::offset::Utc::now().date());
|
||||
ui.add(doc_link_label("DatePickerButton", "DatePickerButton"));
|
||||
ui.add(egui_extras::DatePickerButton::new(date));
|
||||
ui.end_row();
|
||||
}
|
||||
|
||||
ui.add(doc_link_label("Separator", "separator"));
|
||||
ui.separator();
|
||||
ui.end_row();
|
||||
|
||||
ui.add(doc_link_label("CollapsingHeader", "collapsing"));
|
||||
ui.collapsing("Click to see what is hidden!", |ui| {
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = 0.0;
|
||||
ui.label("It's a ");
|
||||
ui.add(doc_link_label("Spinner", "spinner"));
|
||||
ui.add_space(4.0);
|
||||
ui.add(egui::Spinner::new());
|
||||
});
|
||||
});
|
||||
ui.end_row();
|
||||
|
||||
ui.add(doc_link_label("Plot", "plot"));
|
||||
example_plot(ui);
|
||||
ui.end_row();
|
||||
|
||||
ui.hyperlink_to(
|
||||
"Custom widget:",
|
||||
super::toggle_switch::url_to_file_source_code(),
|
||||
);
|
||||
ui.add(super::toggle_switch::toggle(boolean)).on_hover_text(
|
||||
"It's easy to create your own widgets!\n\
|
||||
This toggle switch is just 15 lines of code.",
|
||||
);
|
||||
ui.end_row();
|
||||
}
|
||||
}
|
||||
|
||||
fn example_plot(ui: &mut egui::Ui) -> egui::Response {
|
||||
use egui::plot::{Line, PlotPoints};
|
||||
let n = 128;
|
||||
let line_points: PlotPoints = (0..=n)
|
||||
.map(|i| {
|
||||
use std::f64::consts::TAU;
|
||||
let x = egui::remap(i as f64, 0.0..=n as f64, -TAU..=TAU);
|
||||
[x, x.sin()]
|
||||
})
|
||||
.collect();
|
||||
let line = Line::new(line_points);
|
||||
egui::plot::Plot::new("example_plot")
|
||||
.height(32.0)
|
||||
.data_aspect(1.0)
|
||||
.show(ui, |plot_ui| plot_ui.line(line))
|
||||
.response
|
||||
}
|
||||
|
||||
fn doc_link_label<'a>(title: &'a str, search_term: &'a str) -> impl egui::Widget + 'a {
|
||||
let label = format!("{}:", title);
|
||||
let url = format!("https://docs.rs/egui?search={}", search_term);
|
||||
move |ui: &mut egui::Ui| {
|
||||
ui.hyperlink_to(label, url).on_hover_ui(|ui| {
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
ui.label("Search egui docs for");
|
||||
ui.code(search_term);
|
||||
});
|
||||
})
|
||||
}
|
||||
}
|
||||
141
crates/egui_demo_lib/src/demo/window_options.rs
Normal file
141
crates/egui_demo_lib/src/demo/window_options.rs
Normal file
@@ -0,0 +1,141 @@
|
||||
#[derive(Clone, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct WindowOptions {
|
||||
title: String,
|
||||
title_bar: bool,
|
||||
closable: bool,
|
||||
collapsible: bool,
|
||||
resizable: bool,
|
||||
scroll2: [bool; 2],
|
||||
disabled_time: f64,
|
||||
|
||||
anchored: bool,
|
||||
anchor: egui::Align2,
|
||||
anchor_offset: egui::Vec2,
|
||||
}
|
||||
|
||||
impl Default for WindowOptions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
title: "🗖 Window Options".to_owned(),
|
||||
title_bar: true,
|
||||
closable: true,
|
||||
collapsible: true,
|
||||
resizable: true,
|
||||
scroll2: [true; 2],
|
||||
disabled_time: f64::NEG_INFINITY,
|
||||
anchored: false,
|
||||
anchor: egui::Align2::RIGHT_TOP,
|
||||
anchor_offset: egui::Vec2::ZERO,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl super::Demo for WindowOptions {
|
||||
fn name(&self) -> &'static str {
|
||||
"🗖 Window Options"
|
||||
}
|
||||
|
||||
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
|
||||
let Self {
|
||||
title,
|
||||
title_bar,
|
||||
closable,
|
||||
collapsible,
|
||||
resizable,
|
||||
scroll2,
|
||||
disabled_time,
|
||||
anchored,
|
||||
anchor,
|
||||
anchor_offset,
|
||||
} = self.clone();
|
||||
|
||||
let enabled = ctx.input().time - disabled_time > 2.0;
|
||||
if !enabled {
|
||||
ctx.request_repaint();
|
||||
}
|
||||
|
||||
use super::View as _;
|
||||
let mut window = egui::Window::new(title)
|
||||
.id(egui::Id::new("demo_window_options")) // required since we change the title
|
||||
.resizable(resizable)
|
||||
.collapsible(collapsible)
|
||||
.title_bar(title_bar)
|
||||
.scroll2(scroll2)
|
||||
.enabled(enabled);
|
||||
if closable {
|
||||
window = window.open(open);
|
||||
}
|
||||
if anchored {
|
||||
window = window.anchor(anchor, anchor_offset);
|
||||
}
|
||||
window.show(ctx, |ui| self.ui(ui));
|
||||
}
|
||||
}
|
||||
|
||||
impl super::View for WindowOptions {
|
||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
let Self {
|
||||
title,
|
||||
title_bar,
|
||||
closable,
|
||||
collapsible,
|
||||
resizable,
|
||||
scroll2,
|
||||
disabled_time: _,
|
||||
anchored,
|
||||
anchor,
|
||||
anchor_offset,
|
||||
} = self;
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("title:");
|
||||
ui.text_edit_singleline(title);
|
||||
});
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.group(|ui| {
|
||||
ui.vertical(|ui| {
|
||||
ui.checkbox(title_bar, "title_bar");
|
||||
ui.checkbox(closable, "closable");
|
||||
ui.checkbox(collapsible, "collapsible");
|
||||
ui.checkbox(resizable, "resizable");
|
||||
ui.checkbox(&mut scroll2[0], "hscroll");
|
||||
ui.checkbox(&mut scroll2[1], "vscroll");
|
||||
});
|
||||
});
|
||||
ui.group(|ui| {
|
||||
ui.vertical(|ui| {
|
||||
ui.checkbox(anchored, "anchored");
|
||||
ui.set_enabled(*anchored);
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("x:");
|
||||
ui.selectable_value(&mut anchor[0], egui::Align::LEFT, "Left");
|
||||
ui.selectable_value(&mut anchor[0], egui::Align::Center, "Center");
|
||||
ui.selectable_value(&mut anchor[0], egui::Align::RIGHT, "Right");
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("y:");
|
||||
ui.selectable_value(&mut anchor[1], egui::Align::TOP, "Top");
|
||||
ui.selectable_value(&mut anchor[1], egui::Align::Center, "Center");
|
||||
ui.selectable_value(&mut anchor[1], egui::Align::BOTTOM, "Bottom");
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Offset:");
|
||||
ui.add(egui::DragValue::new(&mut anchor_offset.x));
|
||||
ui.add(egui::DragValue::new(&mut anchor_offset.y));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
ui.separator();
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
if ui.button("Disable for 2 seconds").clicked() {
|
||||
self.disabled_time = ui.input().time;
|
||||
}
|
||||
egui::reset_button(ui, self);
|
||||
ui.add(crate::egui_github_link_file!());
|
||||
});
|
||||
}
|
||||
}
|
||||
90
crates/egui_demo_lib/src/demo/window_with_panels.rs
Normal file
90
crates/egui_demo_lib/src/demo/window_with_panels.rs
Normal file
@@ -0,0 +1,90 @@
|
||||
#[derive(Clone, PartialEq, Default)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct WindowWithPanels {}
|
||||
|
||||
impl super::Demo for WindowWithPanels {
|
||||
fn name(&self) -> &'static str {
|
||||
"🗖 Window With Panels"
|
||||
}
|
||||
|
||||
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
|
||||
use super::View as _;
|
||||
let window = egui::Window::new("Window with Panels")
|
||||
.default_width(600.0)
|
||||
.default_height(400.0)
|
||||
.vscroll(false)
|
||||
.open(open);
|
||||
window.show(ctx, |ui| self.ui(ui));
|
||||
}
|
||||
}
|
||||
|
||||
impl super::View for WindowWithPanels {
|
||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
// Note that the order we add the panels is very important!
|
||||
|
||||
egui::TopBottomPanel::top("top_panel")
|
||||
.resizable(true)
|
||||
.min_height(32.0)
|
||||
.show_inside(ui, |ui| {
|
||||
egui::ScrollArea::vertical().show(ui, |ui| {
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.heading("Expandable Upper Panel");
|
||||
});
|
||||
lorem_ipsum(ui);
|
||||
});
|
||||
});
|
||||
|
||||
egui::SidePanel::left("left_panel")
|
||||
.resizable(true)
|
||||
.default_width(150.0)
|
||||
.width_range(80.0..=200.0)
|
||||
.show_inside(ui, |ui| {
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.heading("Left Panel");
|
||||
});
|
||||
egui::ScrollArea::vertical().show(ui, |ui| {
|
||||
lorem_ipsum(ui);
|
||||
});
|
||||
});
|
||||
|
||||
egui::SidePanel::right("right_panel")
|
||||
.resizable(true)
|
||||
.default_width(150.0)
|
||||
.width_range(80.0..=200.0)
|
||||
.show_inside(ui, |ui| {
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.heading("Right Panel");
|
||||
});
|
||||
egui::ScrollArea::vertical().show(ui, |ui| {
|
||||
lorem_ipsum(ui);
|
||||
});
|
||||
});
|
||||
|
||||
egui::TopBottomPanel::bottom("bottom_panel")
|
||||
.resizable(false)
|
||||
.min_height(0.0)
|
||||
.show_inside(ui, |ui| {
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.heading("Bottom Panel");
|
||||
});
|
||||
});
|
||||
|
||||
egui::CentralPanel::default().show_inside(ui, |ui| {
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.heading("Central Panel");
|
||||
});
|
||||
egui::ScrollArea::vertical().show(ui, |ui| {
|
||||
lorem_ipsum(ui);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn lorem_ipsum(ui: &mut egui::Ui) {
|
||||
ui.with_layout(
|
||||
egui::Layout::top_down(egui::Align::LEFT).with_cross_justify(true),
|
||||
|ui| {
|
||||
ui.label(egui::RichText::new(crate::LOREM_IPSUM_LONG).small().weak());
|
||||
},
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user