1
0
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:
Emil Ernerfeldt
2022-08-20 10:41:49 +02:00
committed by GitHub
parent 5c63648c02
commit 041f2e64ba
260 changed files with 59 additions and 57 deletions

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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