mirror of
https://github.com/Noratrieb/game-wip-dontplay.git
synced 2026-01-17 04:45:02 +01:00
vendor
This commit is contained in:
parent
12163d1338
commit
550b1644cb
363 changed files with 84081 additions and 16 deletions
559
egui/crates/egui_demo_lib/src/color_test.rs
Normal file
559
egui/crates/egui_demo_lib/src/color_test.rs
Normal file
|
|
@ -0,0 +1,559 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use egui::{widgets::color_picker::show_color, TextureOptions, *};
|
||||
|
||||
const GRADIENT_SIZE: Vec2 = vec2(256.0, 18.0);
|
||||
|
||||
const BLACK: Color32 = Color32::BLACK;
|
||||
const GREEN: Color32 = Color32::GREEN;
|
||||
const RED: Color32 = Color32::RED;
|
||||
const TRANSPARENT: Color32 = Color32::TRANSPARENT;
|
||||
const WHITE: Color32 = Color32::WHITE;
|
||||
|
||||
/// A test for sanity-checking and diagnosing egui rendering backends.
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct ColorTest {
|
||||
#[cfg_attr(feature = "serde", serde(skip))]
|
||||
tex_mngr: TextureManager,
|
||||
vertex_gradients: bool,
|
||||
texture_gradients: bool,
|
||||
}
|
||||
|
||||
impl Default for ColorTest {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
tex_mngr: Default::default(),
|
||||
vertex_gradients: true,
|
||||
texture_gradients: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ColorTest {
|
||||
pub fn ui(&mut self, ui: &mut Ui) {
|
||||
ui.set_max_width(680.0);
|
||||
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.add(crate::egui_github_link_file!());
|
||||
});
|
||||
|
||||
ui.horizontal_wrapped(|ui|{
|
||||
ui.label("This is made to test that the egui painter backend is set up correctly.");
|
||||
ui.add(egui::Label::new("❓").sense(egui::Sense::click()))
|
||||
.on_hover_text("The texture sampling should be sRGB-aware, and every other color operation should be done in gamma-space (sRGB). All colors should use pre-multiplied alpha");
|
||||
});
|
||||
ui.label("If the rendering is done right, all groups of gradients will look uniform.");
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.checkbox(&mut self.vertex_gradients, "Vertex gradients");
|
||||
ui.checkbox(&mut self.texture_gradients, "Texture gradients");
|
||||
});
|
||||
|
||||
ui.heading("sRGB color test");
|
||||
ui.label("Use a color picker to ensure this color is (255, 165, 0) / #ffa500");
|
||||
ui.scope(|ui| {
|
||||
ui.spacing_mut().item_spacing.y = 0.0; // No spacing between gradients
|
||||
let g = Gradient::one_color(Color32::from_rgb(255, 165, 0));
|
||||
self.vertex_gradient(ui, "orange rgb(255, 165, 0) - vertex", WHITE, &g);
|
||||
self.tex_gradient(ui, "orange rgb(255, 165, 0) - texture", WHITE, &g);
|
||||
});
|
||||
|
||||
ui.separator();
|
||||
|
||||
ui.label("Test that vertex color times texture color is done in gamma space:");
|
||||
ui.scope(|ui| {
|
||||
ui.spacing_mut().item_spacing.y = 0.0; // No spacing between gradients
|
||||
|
||||
let tex_color = Color32::from_rgb(64, 128, 255);
|
||||
let vertex_color = Color32::from_rgb(128, 196, 196);
|
||||
let ground_truth = mul_color_gamma(tex_color, vertex_color);
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
let color_size = ui.spacing().interact_size;
|
||||
ui.label("texture");
|
||||
show_color(ui, tex_color, color_size);
|
||||
ui.label(" * ");
|
||||
show_color(ui, vertex_color, color_size);
|
||||
ui.label(" vertex color =");
|
||||
});
|
||||
{
|
||||
let g = Gradient::one_color(ground_truth);
|
||||
self.vertex_gradient(ui, "Ground truth (vertices)", WHITE, &g);
|
||||
self.tex_gradient(ui, "Ground truth (texture)", WHITE, &g);
|
||||
}
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
let g = Gradient::one_color(tex_color);
|
||||
let tex = self.tex_mngr.get(ui.ctx(), &g);
|
||||
let texel_offset = 0.5 / (g.0.len() as f32);
|
||||
let uv = Rect::from_min_max(pos2(texel_offset, 0.0), pos2(1.0 - texel_offset, 1.0));
|
||||
ui.add(Image::new(tex, GRADIENT_SIZE).tint(vertex_color).uv(uv))
|
||||
.on_hover_text(format!("A texture that is {} texels wide", g.0.len()));
|
||||
ui.label("GPU result");
|
||||
});
|
||||
});
|
||||
|
||||
ui.separator();
|
||||
|
||||
// TODO(emilk): test color multiplication (image tint),
|
||||
// to make sure vertex and texture color multiplication is done in linear space.
|
||||
|
||||
ui.label("Gamma interpolation:");
|
||||
self.show_gradients(ui, WHITE, (RED, GREEN), Interpolation::Gamma);
|
||||
|
||||
ui.separator();
|
||||
|
||||
self.show_gradients(ui, RED, (TRANSPARENT, GREEN), Interpolation::Gamma);
|
||||
|
||||
ui.separator();
|
||||
|
||||
self.show_gradients(ui, WHITE, (TRANSPARENT, GREEN), Interpolation::Gamma);
|
||||
|
||||
ui.separator();
|
||||
|
||||
self.show_gradients(ui, BLACK, (BLACK, WHITE), Interpolation::Gamma);
|
||||
ui.separator();
|
||||
self.show_gradients(ui, WHITE, (BLACK, TRANSPARENT), Interpolation::Gamma);
|
||||
ui.separator();
|
||||
self.show_gradients(ui, BLACK, (TRANSPARENT, WHITE), Interpolation::Gamma);
|
||||
ui.separator();
|
||||
|
||||
ui.label("Additive blending: add more and more blue to the red background:");
|
||||
self.show_gradients(
|
||||
ui,
|
||||
RED,
|
||||
(TRANSPARENT, Color32::from_rgb_additive(0, 0, 255)),
|
||||
Interpolation::Gamma,
|
||||
);
|
||||
|
||||
ui.separator();
|
||||
|
||||
ui.label("Linear interpolation (texture sampling):");
|
||||
self.show_gradients(ui, WHITE, (RED, GREEN), Interpolation::Linear);
|
||||
|
||||
ui.separator();
|
||||
|
||||
pixel_test(ui);
|
||||
|
||||
ui.separator();
|
||||
ui.label("Testing text rendering:");
|
||||
|
||||
text_on_bg(ui, Color32::from_gray(200), Color32::from_gray(230)); // gray on gray
|
||||
text_on_bg(ui, Color32::from_gray(140), Color32::from_gray(28)); // dark mode normal text
|
||||
|
||||
// Matches Mac Font book (useful for testing):
|
||||
text_on_bg(ui, Color32::from_gray(39), Color32::from_gray(255));
|
||||
text_on_bg(ui, Color32::from_gray(220), Color32::from_gray(30));
|
||||
|
||||
ui.separator();
|
||||
|
||||
blending_and_feathering_test(ui);
|
||||
}
|
||||
|
||||
fn show_gradients(
|
||||
&mut self,
|
||||
ui: &mut Ui,
|
||||
bg_fill: Color32,
|
||||
(left, right): (Color32, Color32),
|
||||
interpolation: Interpolation,
|
||||
) {
|
||||
let is_opaque = left.is_opaque() && right.is_opaque();
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
let color_size = ui.spacing().interact_size;
|
||||
if !is_opaque {
|
||||
ui.label("Background:");
|
||||
show_color(ui, bg_fill, color_size);
|
||||
}
|
||||
ui.label("gradient");
|
||||
show_color(ui, left, color_size);
|
||||
ui.label("-");
|
||||
show_color(ui, right, color_size);
|
||||
});
|
||||
|
||||
ui.scope(|ui| {
|
||||
ui.spacing_mut().item_spacing.y = 0.0; // No spacing between gradients
|
||||
if is_opaque {
|
||||
let g = Gradient::ground_truth_gradient(left, right, interpolation);
|
||||
self.vertex_gradient(ui, "Ground Truth (CPU gradient) - vertices", bg_fill, &g);
|
||||
self.tex_gradient(ui, "Ground Truth (CPU gradient) - texture", bg_fill, &g);
|
||||
} else {
|
||||
let g = Gradient::ground_truth_gradient(left, right, interpolation)
|
||||
.with_bg_fill(bg_fill);
|
||||
self.vertex_gradient(
|
||||
ui,
|
||||
"Ground Truth (CPU gradient, CPU blending) - vertices",
|
||||
bg_fill,
|
||||
&g,
|
||||
);
|
||||
self.tex_gradient(
|
||||
ui,
|
||||
"Ground Truth (CPU gradient, CPU blending) - texture",
|
||||
bg_fill,
|
||||
&g,
|
||||
);
|
||||
let g = Gradient::ground_truth_gradient(left, right, interpolation);
|
||||
self.vertex_gradient(ui, "CPU gradient, GPU blending - vertices", bg_fill, &g);
|
||||
self.tex_gradient(ui, "CPU gradient, GPU blending - texture", bg_fill, &g);
|
||||
}
|
||||
|
||||
let g = Gradient::endpoints(left, right);
|
||||
|
||||
match interpolation {
|
||||
Interpolation::Linear => {
|
||||
// texture sampler is sRGBA aware, and should therefore be linear
|
||||
self.tex_gradient(ui, "Texture of width 2 (test texture sampler)", bg_fill, &g);
|
||||
}
|
||||
Interpolation::Gamma => {
|
||||
// vertex shader uses gamma
|
||||
self.vertex_gradient(
|
||||
ui,
|
||||
"Triangle mesh of width 2 (test vertex decode and interpolation)",
|
||||
bg_fill,
|
||||
&g,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn tex_gradient(&mut self, ui: &mut Ui, label: &str, bg_fill: Color32, gradient: &Gradient) {
|
||||
if !self.texture_gradients {
|
||||
return;
|
||||
}
|
||||
ui.horizontal(|ui| {
|
||||
let tex = self.tex_mngr.get(ui.ctx(), gradient);
|
||||
let texel_offset = 0.5 / (gradient.0.len() as f32);
|
||||
let uv = Rect::from_min_max(pos2(texel_offset, 0.0), pos2(1.0 - texel_offset, 1.0));
|
||||
ui.add(Image::new(tex, GRADIENT_SIZE).bg_fill(bg_fill).uv(uv))
|
||||
.on_hover_text(format!(
|
||||
"A texture that is {} texels wide",
|
||||
gradient.0.len()
|
||||
));
|
||||
ui.label(label);
|
||||
});
|
||||
}
|
||||
|
||||
fn vertex_gradient(&mut self, ui: &mut Ui, label: &str, bg_fill: Color32, gradient: &Gradient) {
|
||||
if !self.vertex_gradients {
|
||||
return;
|
||||
}
|
||||
ui.horizontal(|ui| {
|
||||
vertex_gradient(ui, bg_fill, gradient).on_hover_text(format!(
|
||||
"A triangle mesh that is {} vertices wide",
|
||||
gradient.0.len()
|
||||
));
|
||||
ui.label(label);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn vertex_gradient(ui: &mut Ui, bg_fill: Color32, gradient: &Gradient) -> Response {
|
||||
use egui::epaint::*;
|
||||
let (rect, response) = ui.allocate_at_least(GRADIENT_SIZE, Sense::hover());
|
||||
if bg_fill != Default::default() {
|
||||
let mut mesh = Mesh::default();
|
||||
mesh.add_colored_rect(rect, bg_fill);
|
||||
ui.painter().add(Shape::mesh(mesh));
|
||||
}
|
||||
{
|
||||
let n = gradient.0.len();
|
||||
assert!(n >= 2);
|
||||
let mut mesh = Mesh::default();
|
||||
for (i, &color) in gradient.0.iter().enumerate() {
|
||||
let t = i as f32 / (n as f32 - 1.0);
|
||||
let x = lerp(rect.x_range(), t);
|
||||
mesh.colored_vertex(pos2(x, rect.top()), color);
|
||||
mesh.colored_vertex(pos2(x, rect.bottom()), color);
|
||||
if i < n - 1 {
|
||||
let i = i as u32;
|
||||
mesh.add_triangle(2 * i, 2 * i + 1, 2 * i + 2);
|
||||
mesh.add_triangle(2 * i + 1, 2 * i + 2, 2 * i + 3);
|
||||
}
|
||||
}
|
||||
ui.painter().add(Shape::mesh(mesh));
|
||||
}
|
||||
response
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum Interpolation {
|
||||
Linear,
|
||||
Gamma,
|
||||
}
|
||||
|
||||
#[derive(Clone, Hash, PartialEq, Eq)]
|
||||
struct Gradient(pub Vec<Color32>);
|
||||
|
||||
impl Gradient {
|
||||
pub fn one_color(srgba: Color32) -> Self {
|
||||
Self(vec![srgba, srgba])
|
||||
}
|
||||
|
||||
pub fn endpoints(left: Color32, right: Color32) -> Self {
|
||||
Self(vec![left, right])
|
||||
}
|
||||
|
||||
pub fn ground_truth_gradient(
|
||||
left: Color32,
|
||||
right: Color32,
|
||||
interpolation: Interpolation,
|
||||
) -> Self {
|
||||
match interpolation {
|
||||
Interpolation::Linear => Self::ground_truth_linear_gradient(left, right),
|
||||
Interpolation::Gamma => Self::ground_truth_gamma_gradient(left, right),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ground_truth_linear_gradient(left: Color32, right: Color32) -> Self {
|
||||
let left = Rgba::from(left);
|
||||
let right = Rgba::from(right);
|
||||
|
||||
let n = 255;
|
||||
Self(
|
||||
(0..=n)
|
||||
.map(|i| {
|
||||
let t = i as f32 / n as f32;
|
||||
Color32::from(lerp(left..=right, t))
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn ground_truth_gamma_gradient(left: Color32, right: Color32) -> Self {
|
||||
let n = 255;
|
||||
Self(
|
||||
(0..=n)
|
||||
.map(|i| {
|
||||
let t = i as f32 / n as f32;
|
||||
lerp_color_gamma(left, right, t)
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Do premultiplied alpha-aware blending of the gradient on top of the fill color
|
||||
/// in gamma-space.
|
||||
pub fn with_bg_fill(self, bg: Color32) -> Self {
|
||||
Self(
|
||||
self.0
|
||||
.into_iter()
|
||||
.map(|fg| {
|
||||
let a = fg.a() as f32 / 255.0;
|
||||
Color32::from_rgba_premultiplied(
|
||||
(bg[0] as f32 * (1.0 - a) + fg[0] as f32).round() as u8,
|
||||
(bg[1] as f32 * (1.0 - a) + fg[1] as f32).round() as u8,
|
||||
(bg[2] as f32 * (1.0 - a) + fg[2] as f32).round() as u8,
|
||||
(bg[3] as f32 * (1.0 - a) + fg[3] as f32).round() as u8,
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn to_pixel_row(&self) -> Vec<Color32> {
|
||||
self.0.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct TextureManager(HashMap<Gradient, TextureHandle>);
|
||||
|
||||
impl TextureManager {
|
||||
fn get(&mut self, ctx: &egui::Context, gradient: &Gradient) -> &TextureHandle {
|
||||
self.0.entry(gradient.clone()).or_insert_with(|| {
|
||||
let pixels = gradient.to_pixel_row();
|
||||
let width = pixels.len();
|
||||
let height = 1;
|
||||
ctx.load_texture(
|
||||
"color_test_gradient",
|
||||
epaint::ColorImage {
|
||||
size: [width, height],
|
||||
pixels,
|
||||
},
|
||||
TextureOptions::LINEAR,
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn pixel_test(ui: &mut Ui) {
|
||||
ui.label("Each subsequent square should be one physical pixel larger than the previous. They should be exactly one physical pixel apart. They should be perfectly aligned to the pixel grid.");
|
||||
|
||||
let color = if ui.style().visuals.dark_mode {
|
||||
egui::Color32::WHITE
|
||||
} else {
|
||||
egui::Color32::BLACK
|
||||
};
|
||||
|
||||
let pixels_per_point = ui.ctx().pixels_per_point();
|
||||
let num_squares: u32 = 8;
|
||||
let size_pixels = vec2(
|
||||
((num_squares + 1) * (num_squares + 2) / 2) as f32,
|
||||
num_squares as f32,
|
||||
);
|
||||
let size_points = size_pixels / pixels_per_point + Vec2::splat(2.0);
|
||||
let (response, painter) = ui.allocate_painter(size_points, Sense::hover());
|
||||
|
||||
let mut cursor_pixel = Pos2::new(
|
||||
response.rect.min.x * pixels_per_point,
|
||||
response.rect.min.y * pixels_per_point,
|
||||
)
|
||||
.ceil();
|
||||
for size in 1..=num_squares {
|
||||
let rect_points = Rect::from_min_size(
|
||||
Pos2::new(
|
||||
cursor_pixel.x / pixels_per_point,
|
||||
cursor_pixel.y / pixels_per_point,
|
||||
),
|
||||
Vec2::splat(size as f32) / pixels_per_point,
|
||||
);
|
||||
painter.rect_filled(rect_points, 0.0, color);
|
||||
cursor_pixel.x += (1 + size) as f32;
|
||||
}
|
||||
}
|
||||
|
||||
fn blending_and_feathering_test(ui: &mut Ui) {
|
||||
let size = vec2(512.0, 512.0);
|
||||
let (response, painter) = ui.allocate_painter(size, Sense::hover());
|
||||
let rect = response.rect;
|
||||
|
||||
let mut top_half = rect;
|
||||
top_half.set_bottom(top_half.center().y);
|
||||
painter.rect_filled(top_half, 0.0, Color32::BLACK);
|
||||
paint_fine_lines_and_text(&painter, top_half, Color32::WHITE);
|
||||
|
||||
let mut bottom_half = rect;
|
||||
bottom_half.set_top(bottom_half.center().y);
|
||||
painter.rect_filled(bottom_half, 0.0, Color32::WHITE);
|
||||
paint_fine_lines_and_text(&painter, bottom_half, Color32::BLACK);
|
||||
}
|
||||
|
||||
fn text_on_bg(ui: &mut egui::Ui, fg: Color32, bg: Color32) {
|
||||
assert!(fg.is_opaque());
|
||||
assert!(bg.is_opaque());
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(
|
||||
RichText::from("▣ The quick brown fox jumps over the lazy dog and runs away.")
|
||||
.background_color(bg)
|
||||
.color(fg),
|
||||
);
|
||||
ui.label(format!(
|
||||
"({} {} {}) on ({} {} {})",
|
||||
fg.r(),
|
||||
fg.g(),
|
||||
fg.b(),
|
||||
bg.r(),
|
||||
bg.g(),
|
||||
bg.b(),
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
fn paint_fine_lines_and_text(painter: &egui::Painter, mut rect: Rect, color: Color32) {
|
||||
{
|
||||
let mut y = 0.0;
|
||||
for opacity in [1.00, 0.50, 0.25, 0.10, 0.05, 0.02, 0.01, 0.00] {
|
||||
painter.text(
|
||||
rect.center_top() + vec2(0.0, y),
|
||||
Align2::LEFT_TOP,
|
||||
format!("{:.0}% white", 100.0 * opacity),
|
||||
FontId::proportional(14.0),
|
||||
Color32::WHITE.gamma_multiply(opacity),
|
||||
);
|
||||
painter.text(
|
||||
rect.center_top() + vec2(80.0, y),
|
||||
Align2::LEFT_TOP,
|
||||
format!("{:.0}% gray", 100.0 * opacity),
|
||||
FontId::proportional(14.0),
|
||||
Color32::GRAY.gamma_multiply(opacity),
|
||||
);
|
||||
painter.text(
|
||||
rect.center_top() + vec2(160.0, y),
|
||||
Align2::LEFT_TOP,
|
||||
format!("{:.0}% black", 100.0 * opacity),
|
||||
FontId::proportional(14.0),
|
||||
Color32::BLACK.gamma_multiply(opacity),
|
||||
);
|
||||
y += 20.0;
|
||||
}
|
||||
|
||||
for font_size in [6.0, 7.0, 8.0, 9.0, 10.0, 12.0, 14.0] {
|
||||
painter.text(
|
||||
rect.center_top() + vec2(0.0, y),
|
||||
Align2::LEFT_TOP,
|
||||
format!(
|
||||
"{font_size}px - The quick brown fox jumps over the lazy dog and runs away."
|
||||
),
|
||||
FontId::proportional(font_size),
|
||||
color,
|
||||
);
|
||||
y += font_size + 1.0;
|
||||
}
|
||||
}
|
||||
|
||||
rect.max.x = rect.center().x;
|
||||
|
||||
rect = rect.shrink(16.0);
|
||||
for width in [0.05, 0.1, 0.25, 0.5, 1.0, 2.0, 4.0] {
|
||||
painter.text(
|
||||
rect.left_top(),
|
||||
Align2::CENTER_CENTER,
|
||||
width.to_string(),
|
||||
FontId::monospace(12.0),
|
||||
color,
|
||||
);
|
||||
|
||||
painter.add(egui::epaint::CubicBezierShape::from_points_stroke(
|
||||
[
|
||||
rect.left_top() + vec2(16.0, 0.0),
|
||||
rect.right_top(),
|
||||
rect.right_center(),
|
||||
rect.right_bottom(),
|
||||
],
|
||||
false,
|
||||
Color32::TRANSPARENT,
|
||||
Stroke::new(width, color),
|
||||
));
|
||||
|
||||
rect.min.y += 24.0;
|
||||
rect.max.x -= 24.0;
|
||||
}
|
||||
|
||||
rect.min.y += 16.0;
|
||||
painter.text(
|
||||
rect.left_top(),
|
||||
Align2::LEFT_CENTER,
|
||||
"transparent --> opaque",
|
||||
FontId::monospace(10.0),
|
||||
color,
|
||||
);
|
||||
rect.min.y += 12.0;
|
||||
let mut mesh = Mesh::default();
|
||||
mesh.colored_vertex(rect.left_bottom(), Color32::TRANSPARENT);
|
||||
mesh.colored_vertex(rect.left_top(), Color32::TRANSPARENT);
|
||||
mesh.colored_vertex(rect.right_bottom(), color);
|
||||
mesh.colored_vertex(rect.right_top(), color);
|
||||
mesh.add_triangle(0, 1, 2);
|
||||
mesh.add_triangle(1, 2, 3);
|
||||
painter.add(mesh);
|
||||
}
|
||||
|
||||
fn mul_color_gamma(left: Color32, right: Color32) -> Color32 {
|
||||
Color32::from_rgba_premultiplied(
|
||||
(left.r() as f32 * right.r() as f32 / 255.0).round() as u8,
|
||||
(left.g() as f32 * right.g() as f32 / 255.0).round() as u8,
|
||||
(left.b() as f32 * right.b() as f32 / 255.0).round() as u8,
|
||||
(left.a() as f32 * right.a() as f32 / 255.0).round() as u8,
|
||||
)
|
||||
}
|
||||
|
||||
fn lerp_color_gamma(left: Color32, right: Color32, t: f32) -> Color32 {
|
||||
Color32::from_rgba_premultiplied(
|
||||
lerp((left[0] as f32)..=(right[0] as f32), t).round() as u8,
|
||||
lerp((left[1] as f32)..=(right[1] as f32), t).round() as u8,
|
||||
lerp((left[2] as f32)..=(right[2] as f32), t).round() as u8,
|
||||
lerp((left[3] as f32)..=(right[3] as f32), t).round() as u8,
|
||||
)
|
||||
}
|
||||
94
egui/crates/egui_demo_lib/src/demo/about.rs
Normal file
94
egui/crates/egui_demo_lib/src/demo/about.rs
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
#[derive(Default)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
pub struct About {}
|
||||
|
||||
impl super::Demo for About {
|
||||
fn name(&self) -> &'static str {
|
||||
"About egui"
|
||||
}
|
||||
|
||||
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
|
||||
egui::Window::new(self.name())
|
||||
.default_width(320.0)
|
||||
.open(open)
|
||||
.show(ctx, |ui| {
|
||||
use super::View as _;
|
||||
self.ui(ui);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl super::View for About {
|
||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
use egui::special_emojis::{OS_APPLE, OS_LINUX, OS_WINDOWS};
|
||||
|
||||
ui.heading("egui");
|
||||
ui.label(format!(
|
||||
"egui is an immediate mode GUI library written in Rust. egui runs both on the web and natively on {}{}{}. \
|
||||
On the web it is compiled to WebAssembly and rendered with WebGL.{}",
|
||||
OS_APPLE, OS_LINUX, OS_WINDOWS,
|
||||
if cfg!(target_arch = "wasm32") {
|
||||
" Everything you see is rendered as textured triangles. There is no DOM, HTML, JS or CSS. Just Rust."
|
||||
} else {""}
|
||||
));
|
||||
ui.label("egui is designed to be easy to use, portable, and fast.");
|
||||
|
||||
ui.add_space(12.0); // ui.separator();
|
||||
ui.heading("Immediate mode");
|
||||
about_immediate_mode(ui);
|
||||
|
||||
ui.add_space(12.0); // ui.separator();
|
||||
ui.heading("Links");
|
||||
links(ui);
|
||||
}
|
||||
}
|
||||
|
||||
fn about_immediate_mode(ui: &mut egui::Ui) {
|
||||
use crate::syntax_highlighting::code_view_ui;
|
||||
ui.style_mut().spacing.interact_size.y = 0.0; // hack to make `horizontal_wrapped` work better with text.
|
||||
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = 0.0;
|
||||
ui.label("Immediate mode is a GUI paradigm that lets you create a GUI with less code and simpler control flow. For example, this is how you create a ");
|
||||
let _ = ui.small_button("button");
|
||||
ui.label(" in egui:");
|
||||
});
|
||||
|
||||
ui.add_space(8.0);
|
||||
code_view_ui(
|
||||
ui,
|
||||
r#"
|
||||
if ui.button("Save").clicked() {
|
||||
my_state.save();
|
||||
}"#
|
||||
.trim_start_matches('\n'),
|
||||
);
|
||||
ui.add_space(8.0);
|
||||
|
||||
ui.label("Note how there are no callbacks or messages, and no button state to store.");
|
||||
|
||||
ui.label("Immediate mode has its roots in gaming, where everything on the screen is painted at the display refresh rate, i.e. at 60+ frames per second. \
|
||||
In immediate mode GUIs, the entire interface is laid out and painted at the same high rate. \
|
||||
This makes immediate mode GUIs especially well suited for highly interactive applications.");
|
||||
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = 0.0;
|
||||
ui.label("More about immediate mode ");
|
||||
ui.hyperlink_to("here", "https://github.com/emilk/egui#why-immediate-mode");
|
||||
ui.label(".");
|
||||
});
|
||||
}
|
||||
|
||||
fn links(ui: &mut egui::Ui) {
|
||||
use egui::special_emojis::{GITHUB, TWITTER};
|
||||
ui.hyperlink_to(
|
||||
format!("{} egui on GitHub", GITHUB),
|
||||
"https://github.com/emilk/egui",
|
||||
);
|
||||
ui.hyperlink_to(
|
||||
format!("{} @ernerfeldt", TWITTER),
|
||||
"https://twitter.com/ernerfeldt",
|
||||
);
|
||||
ui.hyperlink_to("egui documentation", "https://docs.rs/egui/");
|
||||
}
|
||||
97
egui/crates/egui_demo_lib/src/demo/code_editor.rs
Normal file
97
egui/crates/egui_demo_lib/src/demo/code_editor.rs
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
pub struct CodeEditor {
|
||||
language: String,
|
||||
code: String,
|
||||
}
|
||||
|
||||
impl Default for CodeEditor {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
language: "rs".into(),
|
||||
code: "// A very simple example\n\
|
||||
fn main() {\n\
|
||||
\tprintln!(\"Hello world!\");\n\
|
||||
}\n\
|
||||
"
|
||||
.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl super::Demo for CodeEditor {
|
||||
fn name(&self) -> &'static str {
|
||||
"🖮 Code Editor"
|
||||
}
|
||||
|
||||
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
|
||||
use super::View as _;
|
||||
egui::Window::new(self.name())
|
||||
.open(open)
|
||||
.default_height(500.0)
|
||||
.show(ctx, |ui| self.ui(ui));
|
||||
}
|
||||
}
|
||||
|
||||
impl super::View for CodeEditor {
|
||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
let Self { language, code } = self;
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.set_height(0.0);
|
||||
ui.label("An example of syntax highlighting in a TextEdit.");
|
||||
ui.add(crate::egui_github_link_file!());
|
||||
});
|
||||
|
||||
if cfg!(feature = "syntect") {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Language:");
|
||||
ui.text_edit_singleline(language);
|
||||
});
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = 0.0;
|
||||
ui.label("Syntax highlighting powered by ");
|
||||
ui.hyperlink_to("syntect", "https://github.com/trishume/syntect");
|
||||
ui.label(".");
|
||||
});
|
||||
} else {
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = 0.0;
|
||||
ui.label("Compile the demo with the ");
|
||||
ui.code("syntax_highlighting");
|
||||
ui.label(" feature to enable more accurate syntax highlighting using ");
|
||||
ui.hyperlink_to("syntect", "https://github.com/trishume/syntect");
|
||||
ui.label(".");
|
||||
});
|
||||
}
|
||||
|
||||
let mut theme = crate::syntax_highlighting::CodeTheme::from_memory(ui.ctx());
|
||||
ui.collapsing("Theme", |ui| {
|
||||
ui.group(|ui| {
|
||||
theme.ui(ui);
|
||||
theme.clone().store_in_memory(ui.ctx());
|
||||
});
|
||||
});
|
||||
|
||||
let mut layouter = |ui: &egui::Ui, string: &str, wrap_width: f32| {
|
||||
let mut layout_job =
|
||||
crate::syntax_highlighting::highlight(ui.ctx(), &theme, string, language);
|
||||
layout_job.wrap.max_width = wrap_width;
|
||||
ui.fonts(|f| f.layout_job(layout_job))
|
||||
};
|
||||
|
||||
egui::ScrollArea::vertical().show(ui, |ui| {
|
||||
ui.add(
|
||||
egui::TextEdit::multiline(code)
|
||||
.font(egui::TextStyle::Monospace) // for cursor height
|
||||
.code_editor()
|
||||
.desired_rows(10)
|
||||
.lock_focus(true)
|
||||
.desired_width(f32::INFINITY)
|
||||
.layouter(&mut layouter),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
161
egui/crates/egui_demo_lib/src/demo/code_example.rs
Normal file
161
egui/crates/egui_demo_lib/src/demo/code_example.rs
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
#[derive(Debug)]
|
||||
pub struct CodeExample {
|
||||
name: String,
|
||||
age: u32,
|
||||
}
|
||||
|
||||
impl Default for CodeExample {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
name: "Arthur".to_owned(),
|
||||
age: 42,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CodeExample {
|
||||
fn samples_in_grid(&mut self, ui: &mut egui::Ui) {
|
||||
show_code(ui, r#"ui.heading("Code samples");"#);
|
||||
ui.heading("Code samples");
|
||||
ui.end_row();
|
||||
|
||||
show_code(
|
||||
ui,
|
||||
r#"
|
||||
// Putting things on the same line using ui.horizontal:
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Your name: ");
|
||||
ui.text_edit_singleline(&mut self.name);
|
||||
});"#,
|
||||
);
|
||||
// Putting things on the same line using ui.horizontal:
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Your name: ");
|
||||
ui.text_edit_singleline(&mut self.name);
|
||||
});
|
||||
ui.end_row();
|
||||
|
||||
show_code(
|
||||
ui,
|
||||
r#"egui::Slider::new(&mut self.age, 0..=120).text("age")"#,
|
||||
);
|
||||
ui.add(egui::Slider::new(&mut self.age, 0..=120).text("age"));
|
||||
ui.end_row();
|
||||
|
||||
show_code(
|
||||
ui,
|
||||
r#"
|
||||
if ui.button("Click each year").clicked() {
|
||||
self.age += 1;
|
||||
}"#,
|
||||
);
|
||||
if ui.button("Click each year").clicked() {
|
||||
self.age += 1;
|
||||
}
|
||||
ui.end_row();
|
||||
|
||||
show_code(
|
||||
ui,
|
||||
r#"ui.label(format!("Hello '{}', age {}", self.name, self.age));"#,
|
||||
);
|
||||
ui.label(format!("Hello '{}', age {}", self.name, self.age));
|
||||
ui.end_row();
|
||||
}
|
||||
}
|
||||
|
||||
impl super::Demo for CodeExample {
|
||||
fn name(&self) -> &'static str {
|
||||
"🖮 Code Example"
|
||||
}
|
||||
|
||||
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
|
||||
use super::View;
|
||||
egui::Window::new(self.name())
|
||||
.open(open)
|
||||
.default_size([800.0, 400.0])
|
||||
.vscroll(false)
|
||||
.hscroll(true)
|
||||
.show(ctx, |ui| self.ui(ui));
|
||||
}
|
||||
}
|
||||
|
||||
impl super::View for CodeExample {
|
||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
use crate::syntax_highlighting::code_view_ui;
|
||||
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.add(crate::egui_github_link_file!());
|
||||
});
|
||||
|
||||
code_view_ui(
|
||||
ui,
|
||||
r"
|
||||
pub struct CodeExample {
|
||||
name: String,
|
||||
age: u32,
|
||||
}
|
||||
|
||||
impl CodeExample {
|
||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
"
|
||||
.trim(),
|
||||
);
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
let font_id = egui::TextStyle::Monospace.resolve(ui.style());
|
||||
let indentation = 8.0 * ui.fonts(|f| f.glyph_width(&font_id, ' '));
|
||||
let item_spacing = ui.spacing_mut().item_spacing;
|
||||
ui.add_space(indentation - item_spacing.x);
|
||||
|
||||
egui::Grid::new("code_samples")
|
||||
.striped(true)
|
||||
.num_columns(2)
|
||||
.min_col_width(16.0)
|
||||
.spacing([16.0, 8.0])
|
||||
.show(ui, |ui| {
|
||||
self.samples_in_grid(ui);
|
||||
});
|
||||
});
|
||||
|
||||
code_view_ui(ui, " }\n}");
|
||||
|
||||
ui.separator();
|
||||
|
||||
code_view_ui(ui, &format!("{:#?}", self));
|
||||
|
||||
ui.separator();
|
||||
|
||||
let mut theme = crate::syntax_highlighting::CodeTheme::from_memory(ui.ctx());
|
||||
ui.collapsing("Theme", |ui| {
|
||||
theme.ui(ui);
|
||||
theme.store_in_memory(ui.ctx());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn show_code(ui: &mut egui::Ui, code: &str) {
|
||||
let code = remove_leading_indentation(code.trim_start_matches('\n'));
|
||||
crate::syntax_highlighting::code_view_ui(ui, &code);
|
||||
}
|
||||
|
||||
fn remove_leading_indentation(code: &str) -> String {
|
||||
fn is_indent(c: &u8) -> bool {
|
||||
matches!(*c, b' ' | b'\t')
|
||||
}
|
||||
|
||||
let first_line_indent = code.bytes().take_while(is_indent).count();
|
||||
|
||||
let mut out = String::new();
|
||||
|
||||
let mut code = code;
|
||||
while !code.is_empty() {
|
||||
let indent = code.bytes().take_while(is_indent).count();
|
||||
let start = first_line_indent.min(indent);
|
||||
let end = code
|
||||
.find('\n')
|
||||
.map_or_else(|| code.len(), |endline| endline + 1);
|
||||
out += &code[start..end];
|
||||
code = &code[end..];
|
||||
}
|
||||
out
|
||||
}
|
||||
182
egui/crates/egui_demo_lib/src/demo/context_menu.rs
Normal file
182
egui/crates/egui_demo_lib/src/demo/context_menu.rs
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
enum Plot {
|
||||
Sin,
|
||||
Bell,
|
||||
Sigmoid,
|
||||
}
|
||||
|
||||
fn gaussian(x: f64) -> f64 {
|
||||
let var: f64 = 2.0;
|
||||
f64::exp(-(x / var).powi(2)) / (var * f64::sqrt(std::f64::consts::TAU))
|
||||
}
|
||||
|
||||
fn sigmoid(x: f64) -> f64 {
|
||||
-1.0 + 2.0 / (1.0 + f64::exp(-x))
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct ContextMenus {
|
||||
plot: Plot,
|
||||
show_axes: [bool; 2],
|
||||
allow_drag: bool,
|
||||
allow_zoom: bool,
|
||||
allow_scroll: bool,
|
||||
center_x_axis: bool,
|
||||
center_y_axis: bool,
|
||||
width: f32,
|
||||
height: f32,
|
||||
}
|
||||
|
||||
impl Default for ContextMenus {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
plot: Plot::Sin,
|
||||
show_axes: [true, true],
|
||||
allow_drag: true,
|
||||
allow_zoom: true,
|
||||
allow_scroll: true,
|
||||
center_x_axis: false,
|
||||
center_y_axis: false,
|
||||
width: 400.0,
|
||||
height: 200.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl super::Demo for ContextMenus {
|
||||
fn name(&self) -> &'static str {
|
||||
"☰ Context Menus"
|
||||
}
|
||||
|
||||
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
|
||||
use super::View;
|
||||
egui::Window::new(self.name())
|
||||
.vscroll(false)
|
||||
.resizable(false)
|
||||
.open(open)
|
||||
.show(ctx, |ui| self.ui(ui));
|
||||
}
|
||||
}
|
||||
|
||||
impl super::View for ContextMenus {
|
||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
ui.horizontal(|ui| {
|
||||
ui.menu_button("Click for menu", Self::nested_menus);
|
||||
ui.button("Right-click for menu")
|
||||
.context_menu(Self::nested_menus);
|
||||
});
|
||||
|
||||
ui.separator();
|
||||
|
||||
ui.label("Right-click plot to edit it!");
|
||||
ui.horizontal(|ui| {
|
||||
self.example_plot(ui).context_menu(|ui| {
|
||||
ui.menu_button("Plot", |ui| {
|
||||
if ui.radio_value(&mut self.plot, Plot::Sin, "Sin").clicked()
|
||||
|| ui
|
||||
.radio_value(&mut self.plot, Plot::Bell, "Gaussian")
|
||||
.clicked()
|
||||
|| ui
|
||||
.radio_value(&mut self.plot, Plot::Sigmoid, "Sigmoid")
|
||||
.clicked()
|
||||
{
|
||||
ui.close_menu();
|
||||
}
|
||||
});
|
||||
egui::Grid::new("button_grid").show(ui, |ui| {
|
||||
ui.add(
|
||||
egui::DragValue::new(&mut self.width)
|
||||
.speed(1.0)
|
||||
.prefix("Width:"),
|
||||
);
|
||||
ui.add(
|
||||
egui::DragValue::new(&mut self.height)
|
||||
.speed(1.0)
|
||||
.prefix("Height:"),
|
||||
);
|
||||
ui.end_row();
|
||||
ui.checkbox(&mut self.show_axes[0], "x-Axis");
|
||||
ui.checkbox(&mut self.show_axes[1], "y-Axis");
|
||||
ui.end_row();
|
||||
if ui.checkbox(&mut self.allow_drag, "Drag").changed()
|
||||
|| ui.checkbox(&mut self.allow_zoom, "Zoom").changed()
|
||||
|| ui.checkbox(&mut self.allow_scroll, "Scroll").changed()
|
||||
{
|
||||
ui.close_menu();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.add(crate::egui_github_link_file!());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl ContextMenus {
|
||||
fn example_plot(&self, ui: &mut egui::Ui) -> egui::Response {
|
||||
use egui::plot::{Line, PlotPoints};
|
||||
let n = 128;
|
||||
let line = Line::new(
|
||||
(0..=n)
|
||||
.map(|i| {
|
||||
use std::f64::consts::TAU;
|
||||
let x = egui::remap(i as f64, 0.0..=n as f64, -TAU..=TAU);
|
||||
match self.plot {
|
||||
Plot::Sin => [x, x.sin()],
|
||||
Plot::Bell => [x, 10.0 * gaussian(x)],
|
||||
Plot::Sigmoid => [x, sigmoid(x)],
|
||||
}
|
||||
})
|
||||
.collect::<PlotPoints>(),
|
||||
);
|
||||
egui::plot::Plot::new("example_plot")
|
||||
.show_axes(self.show_axes)
|
||||
.allow_drag(self.allow_drag)
|
||||
.allow_zoom(self.allow_zoom)
|
||||
.allow_scroll(self.allow_scroll)
|
||||
.center_x_axis(self.center_x_axis)
|
||||
.center_x_axis(self.center_y_axis)
|
||||
.width(self.width)
|
||||
.height(self.height)
|
||||
.data_aspect(1.0)
|
||||
.show(ui, |plot_ui| plot_ui.line(line))
|
||||
.response
|
||||
}
|
||||
|
||||
fn nested_menus(ui: &mut egui::Ui) {
|
||||
if ui.button("Open...").clicked() {
|
||||
ui.close_menu();
|
||||
}
|
||||
ui.menu_button("SubMenu", |ui| {
|
||||
ui.menu_button("SubMenu", |ui| {
|
||||
if ui.button("Open...").clicked() {
|
||||
ui.close_menu();
|
||||
}
|
||||
let _ = ui.button("Item");
|
||||
});
|
||||
ui.menu_button("SubMenu", |ui| {
|
||||
if ui.button("Open...").clicked() {
|
||||
ui.close_menu();
|
||||
}
|
||||
let _ = ui.button("Item");
|
||||
});
|
||||
let _ = ui.button("Item");
|
||||
if ui.button("Open...").clicked() {
|
||||
ui.close_menu();
|
||||
}
|
||||
});
|
||||
ui.menu_button("SubMenu", |ui| {
|
||||
let _ = ui.button("Item1");
|
||||
let _ = ui.button("Item2");
|
||||
let _ = ui.button("Item3");
|
||||
let _ = ui.button("Item4");
|
||||
if ui.button("Open...").clicked() {
|
||||
ui.close_menu();
|
||||
}
|
||||
});
|
||||
let _ = ui.button("Very long text for this item");
|
||||
}
|
||||
}
|
||||
67
egui/crates/egui_demo_lib/src/demo/dancing_strings.rs
Normal file
67
egui/crates/egui_demo_lib/src/demo/dancing_strings.rs
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
use egui::{containers::*, *};
|
||||
|
||||
#[derive(Default)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
pub struct DancingStrings {}
|
||||
|
||||
impl super::Demo for DancingStrings {
|
||||
fn name(&self) -> &'static str {
|
||||
"♫ Dancing Strings"
|
||||
}
|
||||
|
||||
fn show(&mut self, ctx: &Context, open: &mut bool) {
|
||||
use super::View as _;
|
||||
Window::new(self.name())
|
||||
.open(open)
|
||||
.default_size(vec2(512.0, 256.0))
|
||||
.vscroll(false)
|
||||
.show(ctx, |ui| self.ui(ui));
|
||||
}
|
||||
}
|
||||
|
||||
impl super::View for DancingStrings {
|
||||
fn ui(&mut self, ui: &mut Ui) {
|
||||
let color = if ui.visuals().dark_mode {
|
||||
Color32::from_additive_luminance(196)
|
||||
} else {
|
||||
Color32::from_black_alpha(240)
|
||||
};
|
||||
|
||||
Frame::canvas(ui.style()).show(ui, |ui| {
|
||||
ui.ctx().request_repaint();
|
||||
let time = ui.input(|i| i.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!());
|
||||
});
|
||||
}
|
||||
}
|
||||
355
egui/crates/egui_demo_lib/src/demo/demo_app_windows.rs
Normal file
355
egui/crates/egui_demo_lib/src/demo/demo_app_windows.rs
Normal file
|
|
@ -0,0 +1,355 @@
|
|||
use egui::{Context, Modifiers, 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::highlighting::Highlighting::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(|i| i.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(20.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 = 16.5;
|
||||
|
||||
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(|i| i.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(150.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_mut(|mem| mem.reset_areas());
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
fn file_menu_button(ui: &mut Ui) {
|
||||
let organize_shortcut =
|
||||
egui::KeyboardShortcut::new(Modifiers::CTRL | Modifiers::SHIFT, egui::Key::O);
|
||||
let reset_shortcut =
|
||||
egui::KeyboardShortcut::new(Modifiers::CTRL | Modifiers::SHIFT, egui::Key::R);
|
||||
|
||||
// NOTE: we must check the shortcuts OUTSIDE of the actual "File" menu,
|
||||
// or else they would only be checked if the "File" menu was actually open!
|
||||
|
||||
if ui.input_mut(|i| i.consume_shortcut(&organize_shortcut)) {
|
||||
ui.ctx().memory_mut(|mem| mem.reset_areas());
|
||||
}
|
||||
|
||||
if ui.input_mut(|i| i.consume_shortcut(&reset_shortcut)) {
|
||||
ui.ctx().memory_mut(|mem| *mem = Default::default());
|
||||
}
|
||||
|
||||
ui.menu_button("File", |ui| {
|
||||
ui.set_min_width(220.0);
|
||||
ui.style_mut().wrap = Some(false);
|
||||
|
||||
// On the web the browser controls the zoom
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
egui::gui_zoom::zoom_menu_buttons(ui, None);
|
||||
ui.separator();
|
||||
}
|
||||
|
||||
if ui
|
||||
.add(
|
||||
egui::Button::new("Organize Windows")
|
||||
.shortcut_text(ui.ctx().format_shortcut(&organize_shortcut)),
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
ui.ctx().memory_mut(|mem| mem.reset_areas());
|
||||
ui.close_menu();
|
||||
}
|
||||
|
||||
if ui
|
||||
.add(
|
||||
egui::Button::new("Reset egui memory")
|
||||
.shortcut_text(ui.ctx().format_shortcut(&reset_shortcut)),
|
||||
)
|
||||
.on_hover_text("Forget scroll, positions, sizes etc")
|
||||
.clicked()
|
||||
{
|
||||
ui.ctx().memory_mut(|mem| *mem = Default::default());
|
||||
ui.close_menu();
|
||||
}
|
||||
});
|
||||
}
|
||||
177
egui/crates/egui_demo_lib/src/demo/drag_and_drop.rs
Normal file
177
egui/crates/egui_demo_lib/src/demo/drag_and_drop.rs
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
use egui::*;
|
||||
|
||||
pub fn drag_source(ui: &mut Ui, id: Id, body: impl FnOnce(&mut Ui)) {
|
||||
let is_being_dragged = ui.memory(|mem| mem.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.ctx().set_cursor_icon(CursorIcon::Grab);
|
||||
}
|
||||
} else {
|
||||
ui.ctx().set_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(|mem| mem.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 {
|
||||
fill = ui.visuals().gray_out(fill);
|
||||
stroke.color = ui.visuals().gray_out(stroke.color);
|
||||
}
|
||||
|
||||
ui.painter().set(
|
||||
where_to_put_background,
|
||||
epaint::RectShape {
|
||||
rounding: style.rounding,
|
||||
fill,
|
||||
stroke,
|
||||
rect,
|
||||
},
|
||||
);
|
||||
|
||||
InnerResponse::new(ret, response)
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
#[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(|mem| mem.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(|mem| mem.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(|i| i.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!());
|
||||
});
|
||||
}
|
||||
}
|
||||
262
egui/crates/egui_demo_lib/src/demo/font_book.rs
Normal file
262
egui/crates/egui_demo_lib/src/demo/font_book.rs
Normal file
|
|
@ -0,0 +1,262 @@
|
|||
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(18.0),
|
||||
named_chars: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl super::Demo for FontBook {
|
||||
fn name(&self) -> &'static str {
|
||||
"🔤 Font Book"
|
||||
}
|
||||
|
||||
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
|
||||
egui::Window::new(self.name()).open(open).show(ctx, |ui| {
|
||||
use super::View as _;
|
||||
self.ui(ui);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl super::View for FontBook {
|
||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.add(crate::egui_github_link_file!());
|
||||
});
|
||||
|
||||
ui.label(format!(
|
||||
"The selected font supports {} characters.",
|
||||
self.named_chars
|
||||
.get(&self.font_id.family)
|
||||
.map(|map| map.len())
|
||||
.unwrap_or_default()
|
||||
));
|
||||
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = 0.0;
|
||||
ui.label("You can add more characters by installing additional fonts with ");
|
||||
ui.add(egui::Hyperlink::from_label_and_url(
|
||||
egui::RichText::new("Context::set_fonts").text_style(egui::TextStyle::Monospace),
|
||||
"https://docs.rs/egui/latest/egui/struct.Context.html#method.set_fonts",
|
||||
));
|
||||
ui.label(".");
|
||||
});
|
||||
|
||||
ui.separator();
|
||||
|
||||
egui::introspection::font_id_ui(ui, &mut self.font_id);
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Filter:");
|
||||
ui.add(egui::TextEdit::singleline(&mut self.filter).desired_width(120.0));
|
||||
self.filter = self.filter.to_lowercase();
|
||||
if ui.button("x").clicked() {
|
||||
self.filter.clear();
|
||||
}
|
||||
});
|
||||
|
||||
let filter = &self.filter;
|
||||
let named_chars = self
|
||||
.named_chars
|
||||
.entry(self.font_id.family.clone())
|
||||
.or_insert_with(|| available_characters(ui, self.font_id.family.clone()));
|
||||
|
||||
ui.separator();
|
||||
|
||||
egui::ScrollArea::vertical().show(ui, |ui| {
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
ui.spacing_mut().item_spacing = egui::Vec2::splat(2.0);
|
||||
|
||||
for (&chr, name) in named_chars {
|
||||
if filter.is_empty() || name.contains(filter) || *filter == chr.to_string() {
|
||||
let button = egui::Button::new(
|
||||
egui::RichText::new(chr.to_string()).font(self.font_id.clone()),
|
||||
)
|
||||
.frame(false);
|
||||
|
||||
let tooltip_ui = |ui: &mut egui::Ui| {
|
||||
ui.label(
|
||||
egui::RichText::new(chr.to_string()).font(self.font_id.clone()),
|
||||
);
|
||||
ui.label(format!("{}\nU+{:X}\n\nClick to copy", name, chr as u32));
|
||||
};
|
||||
|
||||
if ui.add(button).on_hover_ui(tooltip_ui).clicked() {
|
||||
ui.output_mut(|o| o.copied_text = chr.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn available_characters(ui: &egui::Ui, family: egui::FontFamily) -> BTreeMap<char, String> {
|
||||
ui.fonts(|f| {
|
||||
f.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,
|
||||
}
|
||||
}
|
||||
37
egui/crates/egui_demo_lib/src/demo/highlighting.rs
Normal file
37
egui/crates/egui_demo_lib/src/demo/highlighting.rs
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
#[derive(Default)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
pub struct Highlighting {}
|
||||
|
||||
impl super::Demo for Highlighting {
|
||||
fn name(&self) -> &'static str {
|
||||
"Highlighting"
|
||||
}
|
||||
|
||||
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 Highlighting {
|
||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
ui.label("This demo demonstrates highlighting a widget.");
|
||||
ui.add_space(4.0);
|
||||
let label_response = ui.label("Hover me to highlight the button!");
|
||||
ui.add_space(4.0);
|
||||
let mut button_response = ui.button("Hover the button to highlight the label!");
|
||||
|
||||
if label_response.hovered() {
|
||||
button_response = button_response.highlight();
|
||||
}
|
||||
if button_response.hovered() {
|
||||
label_response.highlight();
|
||||
}
|
||||
}
|
||||
}
|
||||
180
egui/crates/egui_demo_lib/src/demo/layout_test.rs
Normal file
180
egui/crates/egui_demo_lib/src/demo/layout_test.rs
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
use egui::*;
|
||||
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
pub struct LayoutTest {
|
||||
// Identical to contents of `egui::Layout`
|
||||
layout: LayoutSettings,
|
||||
|
||||
// Extra for testing wrapping:
|
||||
wrap_column_width: f32,
|
||||
wrap_row_height: f32,
|
||||
}
|
||||
|
||||
impl Default for LayoutTest {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
layout: LayoutSettings::top_down(),
|
||||
wrap_column_width: 150.0,
|
||||
wrap_row_height: 20.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
pub struct LayoutSettings {
|
||||
// Similar to the contents of `egui::Layout`
|
||||
main_dir: Direction,
|
||||
main_wrap: bool,
|
||||
cross_align: Align,
|
||||
cross_justify: bool,
|
||||
}
|
||||
|
||||
impl Default for LayoutSettings {
|
||||
fn default() -> Self {
|
||||
Self::top_down()
|
||||
}
|
||||
}
|
||||
|
||||
impl LayoutSettings {
|
||||
fn top_down() -> Self {
|
||||
Self {
|
||||
main_dir: Direction::TopDown,
|
||||
main_wrap: false,
|
||||
cross_align: Align::Min,
|
||||
cross_justify: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn top_down_justified_centered() -> Self {
|
||||
Self {
|
||||
main_dir: Direction::TopDown,
|
||||
main_wrap: false,
|
||||
cross_align: Align::Center,
|
||||
cross_justify: true,
|
||||
}
|
||||
}
|
||||
|
||||
fn horizontal_wrapped() -> Self {
|
||||
Self {
|
||||
main_dir: Direction::LeftToRight,
|
||||
main_wrap: true,
|
||||
cross_align: Align::Center,
|
||||
cross_justify: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn layout(&self) -> Layout {
|
||||
Layout::from_main_dir_and_cross_align(self.main_dir, self.cross_align)
|
||||
.with_main_wrap(self.main_wrap)
|
||||
.with_cross_justify(self.cross_justify)
|
||||
}
|
||||
}
|
||||
|
||||
impl super::Demo for LayoutTest {
|
||||
fn name(&self) -> &'static str {
|
||||
"Layout Test"
|
||||
}
|
||||
|
||||
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
|
||||
egui::Window::new(self.name())
|
||||
.open(open)
|
||||
.resizable(false)
|
||||
.show(ctx, |ui| {
|
||||
use super::View as _;
|
||||
self.ui(ui);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl super::View for LayoutTest {
|
||||
fn ui(&mut self, ui: &mut Ui) {
|
||||
ui.label("Tests and demonstrates the limits of the egui layouts");
|
||||
self.content_ui(ui);
|
||||
Resize::default()
|
||||
.default_size([150.0, 200.0])
|
||||
.show(ui, |ui| {
|
||||
if self.layout.main_wrap {
|
||||
if self.layout.main_dir.is_horizontal() {
|
||||
ui.allocate_ui(
|
||||
vec2(ui.available_size_before_wrap().x, self.wrap_row_height),
|
||||
|ui| ui.with_layout(self.layout.layout(), demo_ui),
|
||||
);
|
||||
} else {
|
||||
ui.allocate_ui(
|
||||
vec2(self.wrap_column_width, ui.available_size_before_wrap().y),
|
||||
|ui| ui.with_layout(self.layout.layout(), demo_ui),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
ui.with_layout(self.layout.layout(), demo_ui);
|
||||
}
|
||||
});
|
||||
ui.label("Resize to see effect");
|
||||
}
|
||||
}
|
||||
|
||||
impl LayoutTest {
|
||||
pub fn content_ui(&mut self, ui: &mut Ui) {
|
||||
ui.horizontal(|ui| {
|
||||
ui.selectable_value(&mut self.layout, LayoutSettings::top_down(), "Top-down");
|
||||
ui.selectable_value(
|
||||
&mut self.layout,
|
||||
LayoutSettings::top_down_justified_centered(),
|
||||
"Top-down, centered and justified",
|
||||
);
|
||||
ui.selectable_value(
|
||||
&mut self.layout,
|
||||
LayoutSettings::horizontal_wrapped(),
|
||||
"Horizontal wrapped",
|
||||
);
|
||||
});
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Main Direction:");
|
||||
for &dir in &[
|
||||
Direction::LeftToRight,
|
||||
Direction::RightToLeft,
|
||||
Direction::TopDown,
|
||||
Direction::BottomUp,
|
||||
] {
|
||||
ui.radio_value(&mut self.layout.main_dir, dir, format!("{:?}", dir));
|
||||
}
|
||||
});
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.checkbox(&mut self.layout.main_wrap, "Main wrap")
|
||||
.on_hover_text("Wrap when next widget doesn't fit the current row/column");
|
||||
|
||||
if self.layout.main_wrap {
|
||||
if self.layout.main_dir.is_horizontal() {
|
||||
ui.add(Slider::new(&mut self.wrap_row_height, 0.0..=200.0).text("Row height"));
|
||||
} else {
|
||||
ui.add(
|
||||
Slider::new(&mut self.wrap_column_width, 0.0..=200.0).text("Column width"),
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Cross Align:");
|
||||
for &align in &[Align::Min, Align::Center, Align::Max] {
|
||||
ui.radio_value(&mut self.layout.cross_align, align, format!("{:?}", align));
|
||||
}
|
||||
});
|
||||
|
||||
ui.checkbox(&mut self.layout.cross_justify, "Cross Justified")
|
||||
.on_hover_text("Try to fill full width/height (e.g. buttons)");
|
||||
}
|
||||
}
|
||||
|
||||
fn demo_ui(ui: &mut Ui) {
|
||||
ui.add(egui::Label::new("Wrapping text followed by example widgets:").wrap(true));
|
||||
let mut dummy = false;
|
||||
ui.checkbox(&mut dummy, "checkbox");
|
||||
ui.radio_value(&mut dummy, false, "radio");
|
||||
let _ = ui.button("button");
|
||||
}
|
||||
663
egui/crates/egui_demo_lib/src/demo/misc_demo_window.rs
Normal file
663
egui/crates/egui_demo_lib/src/demo/misc_demo_window.rs
Normal file
|
|
@ -0,0 +1,663 @@
|
|||
use super::*;
|
||||
use crate::LOREM_IPSUM;
|
||||
use egui::{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(|f|f.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(17.0),
|
||||
color: default_color,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
job.append(
|
||||
"fonts, ",
|
||||
0.0,
|
||||
TextFormat {
|
||||
font_id: FontId::monospace(12.0),
|
||||
color: default_color,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
job.append(
|
||||
"raised text, ",
|
||||
0.0,
|
||||
TextFormat {
|
||||
font_id: FontId::proportional(7.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(7.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!());
|
||||
});
|
||||
}
|
||||
53
egui/crates/egui_demo_lib/src/demo/mod.rs
Normal file
53
egui/crates/egui_demo_lib/src/demo/mod.rs
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
//! 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 highlighting;
|
||||
pub mod layout_test;
|
||||
pub mod misc_demo_window;
|
||||
pub mod multi_touch;
|
||||
pub mod paint_bezier;
|
||||
pub mod painting;
|
||||
pub mod password;
|
||||
pub mod plot_demo;
|
||||
pub mod scrolling;
|
||||
pub mod sliders;
|
||||
pub mod strip_demo;
|
||||
pub mod table_demo;
|
||||
pub mod tests;
|
||||
pub mod text_edit;
|
||||
pub mod toggle_switch;
|
||||
pub mod widget_gallery;
|
||||
pub mod window_options;
|
||||
pub mod window_with_panels;
|
||||
|
||||
pub use {
|
||||
about::About, demo_app_windows::DemoWindows, misc_demo_window::MiscDemoWindow,
|
||||
widget_gallery::WidgetGallery,
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// Something to view in the demo windows
|
||||
pub trait View {
|
||||
fn ui(&mut self, ui: &mut egui::Ui);
|
||||
}
|
||||
|
||||
/// Something to view
|
||||
pub trait Demo {
|
||||
/// `&'static` so we can also use it as a key to store open/close state.
|
||||
fn name(&self) -> &'static str;
|
||||
|
||||
/// Show windows, etc
|
||||
fn show(&mut self, ctx: &egui::Context, open: &mut bool);
|
||||
}
|
||||
145
egui/crates/egui_demo_lib/src/demo/multi_touch.rs
Normal file
145
egui/crates/egui_demo_lib/src/demo/multi_touch.rs
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
use egui::{
|
||||
emath::{RectTransform, Rot2},
|
||||
vec2, Color32, Frame, Pos2, Rect, Sense, Stroke, Vec2,
|
||||
};
|
||||
|
||||
pub struct MultiTouch {
|
||||
rotation: f32,
|
||||
translation: Vec2,
|
||||
zoom: f32,
|
||||
last_touch_time: f64,
|
||||
}
|
||||
|
||||
impl Default for MultiTouch {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
rotation: 0.,
|
||||
translation: Vec2::ZERO,
|
||||
zoom: 1.,
|
||||
last_touch_time: 0.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl super::Demo for MultiTouch {
|
||||
fn name(&self) -> &'static str {
|
||||
"👌 Multi Touch"
|
||||
}
|
||||
|
||||
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
|
||||
egui::Window::new(self.name())
|
||||
.open(open)
|
||||
.default_size(vec2(512.0, 512.0))
|
||||
.resizable(true)
|
||||
.show(ctx, |ui| {
|
||||
use super::View as _;
|
||||
self.ui(ui);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl super::View for MultiTouch {
|
||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.add(crate::egui_github_link_file!());
|
||||
});
|
||||
ui.strong(
|
||||
"This demo only works on devices with multitouch support (e.g. mobiles and tablets).",
|
||||
);
|
||||
ui.separator();
|
||||
ui.label("Try touch gestures Pinch/Stretch, Rotation, and Pressure with 2+ fingers.");
|
||||
|
||||
let num_touches = ui.input(|i| i.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(|i| i.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(|i| i.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(|i| i.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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
171
egui/crates/egui_demo_lib/src/demo/paint_bezier.rs
Normal file
171
egui/crates/egui_demo_lib/src/demo/paint_bezier.rs
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
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(|to| *to);
|
||||
tessellation_options.ui(ui);
|
||||
ui.ctx()
|
||||
.tessellation_options_mut(|to| *to = 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 control_point_shapes: Vec<Shape> = self
|
||||
.control_points
|
||||
.iter_mut()
|
||||
.enumerate()
|
||||
.take(self.degree)
|
||||
.map(|(i, point)| {
|
||||
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;
|
||||
|
||||
Shape::circle_stroke(point_in_screen, control_point_radius, stroke)
|
||||
})
|
||||
.collect();
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
100
egui/crates/egui_demo_lib/src/demo/painting.rs
Normal file
100
egui/crates/egui_demo_lib/src/demo/painting.rs
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
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 shapes = self
|
||||
.lines
|
||||
.iter()
|
||||
.filter(|line| line.len() >= 2)
|
||||
.map(|line| {
|
||||
let points: Vec<Pos2> = line.iter().map(|p| to_screen * *p).collect();
|
||||
egui::Shape::line(points, self.stroke)
|
||||
});
|
||||
|
||||
painter.extend(shapes);
|
||||
|
||||
response
|
||||
}
|
||||
}
|
||||
|
||||
impl super::Demo for Painting {
|
||||
fn name(&self) -> &'static str {
|
||||
"🖊 Painting"
|
||||
}
|
||||
|
||||
fn show(&mut self, ctx: &Context, open: &mut bool) {
|
||||
use super::View as _;
|
||||
Window::new(self.name())
|
||||
.open(open)
|
||||
.default_size(vec2(512.0, 512.0))
|
||||
.vscroll(false)
|
||||
.show(ctx, |ui| self.ui(ui));
|
||||
}
|
||||
}
|
||||
|
||||
impl super::View for Painting {
|
||||
fn ui(&mut self, ui: &mut Ui) {
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.add(crate::egui_github_link_file!());
|
||||
});
|
||||
self.ui_control(ui);
|
||||
ui.label("Paint with your mouse/touch!");
|
||||
Frame::canvas(ui.style()).show(ui, |ui| {
|
||||
self.ui_content(ui);
|
||||
});
|
||||
}
|
||||
}
|
||||
66
egui/crates/egui_demo_lib/src/demo/password.rs
Normal file
66
egui/crates/egui_demo_lib/src/demo/password.rs
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
//! Source code example about creating a widget which uses `egui::Memory` to store UI state.
|
||||
//!
|
||||
//! This is meant to be read as a tutorial, hence the plethora of comments.
|
||||
|
||||
/// Password entry field with ability to toggle character hiding.
|
||||
///
|
||||
/// ## Example:
|
||||
/// ``` ignore
|
||||
/// password_ui(ui, &mut my_password);
|
||||
/// ```
|
||||
#[allow(clippy::ptr_arg)] // false positive
|
||||
pub fn password_ui(ui: &mut egui::Ui, password: &mut String) -> egui::Response {
|
||||
// This widget has its own state — show or hide password characters (`show_plaintext`).
|
||||
// In this case we use a simple `bool`, but you can also declare your own type.
|
||||
// It must implement at least `Clone` and be `'static`.
|
||||
// If you use the `persistence` feature, it also must implement `serde::{Deserialize, Serialize}`.
|
||||
|
||||
// Generate an id for the state
|
||||
let state_id = ui.id().with("show_plaintext");
|
||||
|
||||
// Get state for this widget.
|
||||
// You should get state by value, not by reference to avoid borrowing of [`Memory`].
|
||||
let mut show_plaintext = ui.data_mut(|d| d.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_mut(|d| d.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!())
|
||||
}
|
||||
978
egui/crates/egui_demo_lib/src/demo/plot_demo.rs
Normal file
978
egui/crates/egui_demo_lib/src/demo/plot_demo.rs
Normal file
|
|
@ -0,0 +1,978 @@
|
|||
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, 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
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[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(|i| i.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,
|
||||
link_cursor_x: bool,
|
||||
link_cursor_y: bool,
|
||||
}
|
||||
|
||||
impl Default for LinkedAxisDemo {
|
||||
fn default() -> Self {
|
||||
let link_x = true;
|
||||
let link_y = false;
|
||||
let link_cursor_x = true;
|
||||
let link_cursor_y = false;
|
||||
Self {
|
||||
link_x,
|
||||
link_y,
|
||||
link_cursor_x,
|
||||
link_cursor_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");
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Linked cursors:");
|
||||
ui.checkbox(&mut self.link_cursor_x, "X");
|
||||
ui.checkbox(&mut self.link_cursor_y, "Y");
|
||||
});
|
||||
|
||||
let link_group_id = ui.id().with("linked_demo");
|
||||
ui.horizontal(|ui| {
|
||||
Plot::new("linked_axis_1")
|
||||
.data_aspect(1.0)
|
||||
.width(250.0)
|
||||
.height(250.0)
|
||||
.link_axis(link_group_id, self.link_x, self.link_y)
|
||||
.link_cursor(link_group_id, self.link_cursor_x, self.link_cursor_y)
|
||||
.show(ui, LinkedAxisDemo::configure_plot);
|
||||
Plot::new("linked_axis_2")
|
||||
.data_aspect(2.0)
|
||||
.width(150.0)
|
||||
.height(250.0)
|
||||
.link_axis(link_group_id, self.link_x, self.link_y)
|
||||
.link_cursor(link_group_id, self.link_cursor_x, self.link_cursor_y)
|
||||
.show(ui, LinkedAxisDemo::configure_plot);
|
||||
});
|
||||
Plot::new("linked_axis_3")
|
||||
.data_aspect(0.5)
|
||||
.width(250.0)
|
||||
.height(150.0)
|
||||
.link_axis(link_group_id, self.link_x, self.link_y)
|
||||
.link_cursor(link_group_id, self.link_cursor_x, self.link_cursor_y)
|
||||
.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(), Default::default())
|
||||
});
|
||||
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())
|
||||
.clamp_grid(true)
|
||||
.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
|
||||
}
|
||||
}
|
||||
299
egui/crates/egui_demo_lib/src/demo/scrolling.rs
Normal file
299
egui/crates/egui_demo_lib/src/demo/scrolling.rs
Normal file
|
|
@ -0,0 +1,299 @@
|
|||
use egui::*;
|
||||
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
enum ScrollDemo {
|
||||
ScrollTo,
|
||||
ManyLines,
|
||||
LargeCanvas,
|
||||
StickToEnd,
|
||||
Bidirectional,
|
||||
}
|
||||
|
||||
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.selectable_value(&mut self.demo, ScrollDemo::Bidirectional, "Bidirectional");
|
||||
});
|
||||
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);
|
||||
}
|
||||
ScrollDemo::Bidirectional => {
|
||||
egui::ScrollArea::both().show(ui, |ui| {
|
||||
ui.style_mut().wrap = Some(false);
|
||||
for _ in 0..100 {
|
||||
ui.label(crate::LOREM_IPSUM);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn huge_content_lines(ui: &mut egui::Ui) {
|
||||
ui.label(
|
||||
"A lot of rows, but only the visible ones are laid 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(|f| f.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();
|
||||
}
|
||||
}
|
||||
194
egui/crates/egui_demo_lib/src/demo/sliders.rs
Normal file
194
egui/crates/egui_demo_lib/src/demo/sliders.rs
Normal file
|
|
@ -0,0 +1,194 @@
|
|||
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,
|
||||
pub trailing_fill: bool,
|
||||
}
|
||||
|
||||
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,
|
||||
trailing_fill: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
trailing_fill,
|
||||
} = 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)
|
||||
.trailing_fill(*trailing_fill),
|
||||
);
|
||||
*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)
|
||||
.trailing_fill(*trailing_fill),
|
||||
);
|
||||
|
||||
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")
|
||||
.trailing_fill(*trailing_fill),
|
||||
);
|
||||
ui.add(
|
||||
Slider::new(max, type_min..=type_max)
|
||||
.logarithmic(true)
|
||||
.smart_aim(*smart_aim)
|
||||
.text("right")
|
||||
.trailing_fill(*trailing_fill),
|
||||
);
|
||||
|
||||
ui.separator();
|
||||
|
||||
ui.checkbox(trailing_fill, "Toggle trailing color");
|
||||
ui.label("When enabled, trailing color will be painted up until the circle.");
|
||||
|
||||
ui.separator();
|
||||
|
||||
ui.checkbox(use_steps, "Use steps");
|
||||
ui.label("When enabled, the minimal value change would be restricted to a given step.");
|
||||
if *use_steps {
|
||||
ui.add(egui::DragValue::new(step).speed(1.0));
|
||||
}
|
||||
|
||||
ui.separator();
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Slider type:");
|
||||
ui.radio_value(integer, true, "i32");
|
||||
ui.radio_value(integer, false, "f64");
|
||||
})
|
||||
.response
|
||||
.on_hover_text("All numeric types (f32, usize, …) are supported.");
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Slider orientation:");
|
||||
ui.radio_value(vertical, false, "Horizontal");
|
||||
ui.radio_value(vertical, true, "Vertical");
|
||||
});
|
||||
ui.add_space(8.0);
|
||||
|
||||
ui.checkbox(logarithmic, "Logarithmic");
|
||||
ui.label("Logarithmic sliders are great for when you want to span a huge range, i.e. from zero to a million.");
|
||||
ui.label("Logarithmic sliders can include infinity and zero.");
|
||||
ui.add_space(8.0);
|
||||
|
||||
ui.checkbox(clamp_to_range, "Clamp to range");
|
||||
ui.label("If true, the slider will clamp incoming and outgoing values to the given range.");
|
||||
ui.label("If false, the slider can shows values outside its range, and you can manually enter values outside the range.");
|
||||
ui.add_space(8.0);
|
||||
|
||||
ui.checkbox(smart_aim, "Smart Aim");
|
||||
ui.label("Smart Aim will guide you towards round values when you drag the slider so you you are more likely to hit 250 than 247.23");
|
||||
ui.add_space(8.0);
|
||||
|
||||
ui.vertical_centered(|ui| {
|
||||
egui::reset_button(ui, self);
|
||||
ui.add(crate::egui_github_link_file!());
|
||||
});
|
||||
}
|
||||
}
|
||||
119
egui/crates/egui_demo_lib/src/demo/strip_demo.rs
Normal file
119
egui/crates/egui_demo_lib/src/demo/strip_demo.rs
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
use egui::Color32;
|
||||
use egui_extras::{Size, StripBuilder};
|
||||
|
||||
/// Shows off a table with dynamic layout
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[derive(Default)]
|
||||
pub struct StripDemo {}
|
||||
|
||||
impl super::Demo for StripDemo {
|
||||
fn name(&self) -> &'static str {
|
||||
"▣ Strip Demo"
|
||||
}
|
||||
|
||||
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
|
||||
egui::Window::new(self.name())
|
||||
.open(open)
|
||||
.resizable(true)
|
||||
.default_width(400.0)
|
||||
.show(ctx, |ui| {
|
||||
use super::View as _;
|
||||
self.ui(ui);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl super::View for StripDemo {
|
||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
let dark_mode = ui.visuals().dark_mode;
|
||||
let faded_color = ui.visuals().window_fill();
|
||||
let faded_color = |color: Color32| -> Color32 {
|
||||
use egui::Rgba;
|
||||
let t = if dark_mode { 0.95 } else { 0.8 };
|
||||
egui::lerp(Rgba::from(color)..=Rgba::from(faded_color), t).into()
|
||||
};
|
||||
|
||||
StripBuilder::new(ui)
|
||||
.size(Size::exact(50.0))
|
||||
.size(Size::remainder())
|
||||
.size(Size::relative(0.5).at_least(60.0))
|
||||
.size(Size::exact(10.0))
|
||||
.vertical(|mut strip| {
|
||||
strip.cell(|ui| {
|
||||
ui.painter().rect_filled(
|
||||
ui.available_rect_before_wrap(),
|
||||
0.0,
|
||||
faded_color(Color32::BLUE),
|
||||
);
|
||||
ui.label("width: 100%\nheight: 50px");
|
||||
});
|
||||
strip.strip(|builder| {
|
||||
builder.sizes(Size::remainder(), 2).horizontal(|mut strip| {
|
||||
strip.cell(|ui| {
|
||||
ui.painter().rect_filled(
|
||||
ui.available_rect_before_wrap(),
|
||||
0.0,
|
||||
faded_color(Color32::RED),
|
||||
);
|
||||
ui.label("width: 50%\nheight: remaining");
|
||||
});
|
||||
strip.strip(|builder| {
|
||||
builder.sizes(Size::remainder(), 3).vertical(|mut strip| {
|
||||
strip.empty();
|
||||
strip.cell(|ui| {
|
||||
ui.painter().rect_filled(
|
||||
ui.available_rect_before_wrap(),
|
||||
0.0,
|
||||
faded_color(Color32::YELLOW),
|
||||
);
|
||||
ui.label("width: 50%\nheight: 1/3 of the red region");
|
||||
});
|
||||
strip.empty();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
strip.strip(|builder| {
|
||||
builder
|
||||
.size(Size::remainder())
|
||||
.size(Size::exact(120.0))
|
||||
.size(Size::remainder())
|
||||
.size(Size::exact(70.0))
|
||||
.horizontal(|mut strip| {
|
||||
strip.empty();
|
||||
strip.strip(|builder| {
|
||||
builder
|
||||
.size(Size::remainder())
|
||||
.size(Size::exact(60.0))
|
||||
.size(Size::remainder())
|
||||
.vertical(|mut strip| {
|
||||
strip.empty();
|
||||
strip.cell(|ui| {
|
||||
ui.painter().rect_filled(
|
||||
ui.available_rect_before_wrap(),
|
||||
0.0,
|
||||
faded_color(Color32::GOLD),
|
||||
);
|
||||
ui.label("width: 120px\nheight: 60px");
|
||||
});
|
||||
});
|
||||
});
|
||||
strip.empty();
|
||||
strip.cell(|ui| {
|
||||
ui.painter().rect_filled(
|
||||
ui.available_rect_before_wrap(),
|
||||
0.0,
|
||||
faded_color(Color32::GREEN),
|
||||
);
|
||||
ui.label("width: 70px\n\nheight: 50%, but at least 60px.");
|
||||
});
|
||||
});
|
||||
});
|
||||
strip.cell(|ui| {
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.add(crate::egui_github_link_file!());
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
252
egui/crates/egui_demo_lib/src/demo/table_demo.rs
Normal file
252
egui/crates/egui_demo_lib/src/demo/table_demo.rs
Normal file
|
|
@ -0,0 +1,252 @@
|
|||
#[derive(PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
enum DemoType {
|
||||
Manual,
|
||||
ManyHomogeneous,
|
||||
ManyHeterogenous,
|
||||
}
|
||||
|
||||
/// Shows off a table with dynamic layout
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct TableDemo {
|
||||
demo: DemoType,
|
||||
striped: bool,
|
||||
resizable: bool,
|
||||
num_rows: usize,
|
||||
scroll_to_row_slider: usize,
|
||||
scroll_to_row: Option<usize>,
|
||||
}
|
||||
|
||||
impl Default for TableDemo {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
demo: DemoType::Manual,
|
||||
striped: true,
|
||||
resizable: true,
|
||||
num_rows: 10_000,
|
||||
scroll_to_row_slider: 0,
|
||||
scroll_to_row: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const NUM_MANUAL_ROWS: usize = 20;
|
||||
|
||||
impl super::View for TableDemo {
|
||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
ui.vertical(|ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.checkbox(&mut self.striped, "Striped");
|
||||
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::ManyHomogeneous,
|
||||
"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"),
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let max_rows = if self.demo == DemoType::Manual {
|
||||
NUM_MANUAL_ROWS
|
||||
} else {
|
||||
self.num_rows
|
||||
};
|
||||
|
||||
let slider_response = ui.add(
|
||||
egui::Slider::new(&mut self.scroll_to_row_slider, 0..=max_rows)
|
||||
.logarithmic(true)
|
||||
.text("Row to scroll to"),
|
||||
);
|
||||
if slider_response.changed() {
|
||||
self.scroll_to_row = Some(self.scroll_to_row_slider);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ui.separator();
|
||||
|
||||
// Leave room for the source code link after the table demo:
|
||||
use egui_extras::{Size, StripBuilder};
|
||||
StripBuilder::new(ui)
|
||||
.size(Size::remainder().at_least(100.0)) // for the table
|
||||
.size(Size::exact(10.0)) // for the source code link
|
||||
.vertical(|mut strip| {
|
||||
strip.cell(|ui| {
|
||||
egui::ScrollArea::horizontal().show(ui, |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::{Column, TableBuilder};
|
||||
|
||||
let text_height = egui::TextStyle::Body.resolve(ui.style()).size;
|
||||
|
||||
let mut table = TableBuilder::new(ui)
|
||||
.striped(self.striped)
|
||||
.resizable(self.resizable)
|
||||
.cell_layout(egui::Layout::left_to_right(egui::Align::Center))
|
||||
.column(Column::auto())
|
||||
.column(Column::initial(100.0).range(40.0..=300.0))
|
||||
.column(Column::initial(100.0).at_least(40.0).clip(true))
|
||||
.column(Column::remainder())
|
||||
.min_scrolled_height(0.0);
|
||||
|
||||
if let Some(row_nr) = self.scroll_to_row.take() {
|
||||
table = table.scroll_to_row(row_nr, None);
|
||||
}
|
||||
|
||||
table
|
||||
.header(20.0, |mut header| {
|
||||
header.col(|ui| {
|
||||
ui.strong("Row");
|
||||
});
|
||||
header.col(|ui| {
|
||||
ui.strong("Expanding content");
|
||||
});
|
||||
header.col(|ui| {
|
||||
ui.strong("Clipped text");
|
||||
});
|
||||
header.col(|ui| {
|
||||
ui.strong("Content");
|
||||
});
|
||||
})
|
||||
.body(|mut body| match self.demo {
|
||||
DemoType::Manual => {
|
||||
for row_index in 0..NUM_MANUAL_ROWS {
|
||||
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| {
|
||||
expanding_content(ui);
|
||||
});
|
||||
row.col(|ui| {
|
||||
ui.label(long_text(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::ManyHomogeneous => {
|
||||
body.rows(text_height, self.num_rows, |row_index, mut row| {
|
||||
row.col(|ui| {
|
||||
ui.label(row_index.to_string());
|
||||
});
|
||||
row.col(|ui| {
|
||||
expanding_content(ui);
|
||||
});
|
||||
row.col(|ui| {
|
||||
ui.label(long_text(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.label(row_index.to_string());
|
||||
});
|
||||
row.col(|ui| {
|
||||
expanding_content(ui);
|
||||
});
|
||||
row.col(|ui| {
|
||||
ui.label(long_text(row_index));
|
||||
});
|
||||
row.col(|ui| {
|
||||
ui.style_mut().wrap = Some(false);
|
||||
if thick_row(row_index) {
|
||||
ui.heading("Extra thick row");
|
||||
} else {
|
||||
ui.label("Normal row");
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn expanding_content(ui: &mut egui::Ui) {
|
||||
let width = ui.available_width().clamp(20.0, 200.0);
|
||||
let height = ui.available_height();
|
||||
let (rect, _response) = ui.allocate_exact_size(egui::vec2(width, height), egui::Sense::hover());
|
||||
ui.painter().hline(
|
||||
rect.x_range(),
|
||||
rect.center().y,
|
||||
(1.0, ui.visuals().text_color()),
|
||||
);
|
||||
}
|
||||
|
||||
fn long_text(row_index: usize) -> String {
|
||||
format!("Row {row_index} has some long text that you may want to clip, or it will take up too much horizontal space!")
|
||||
}
|
||||
|
||||
fn thick_row(row_index: usize) -> bool {
|
||||
row_index % 6 == 0
|
||||
}
|
||||
480
egui/crates/egui_demo_lib/src/demo/tests.rs
Normal file
480
egui/crates/egui_demo_lib/src/demo/tests.rs
Normal file
|
|
@ -0,0 +1,480 @@
|
|||
#[derive(Default)]
|
||||
pub struct CursorTest {}
|
||||
|
||||
impl super::Demo for CursorTest {
|
||||
fn name(&self) -> &'static str {
|
||||
"Cursor Test"
|
||||
}
|
||||
|
||||
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
|
||||
egui::Window::new(self.name()).open(open).show(ctx, |ui| {
|
||||
use super::View as _;
|
||||
self.ui(ui);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl super::View for CursorTest {
|
||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
ui.vertical_centered_justified(|ui| {
|
||||
ui.heading("Hover to switch cursor icon:");
|
||||
for &cursor_icon in &egui::CursorIcon::ALL {
|
||||
let _ = ui
|
||||
.button(format!("{:?}", cursor_icon))
|
||||
.on_hover_cursor(cursor_icon);
|
||||
}
|
||||
ui.add(crate::egui_github_link_file!());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct IdTest {}
|
||||
|
||||
impl super::Demo for IdTest {
|
||||
fn name(&self) -> &'static str {
|
||||
"ID Test"
|
||||
}
|
||||
|
||||
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
|
||||
egui::Window::new(self.name()).open(open).show(ctx, |ui| {
|
||||
use super::View as _;
|
||||
self.ui(ui);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl super::View for IdTest {
|
||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
ui.heading("Name collision example");
|
||||
|
||||
ui.label("\
|
||||
Widgets that store state require unique and persisting identifiers so we can track their state between frames.\n\
|
||||
For instance, collapsible 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 end up somewhere else, and so aborting 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());
|
||||
},
|
||||
);
|
||||
}
|
||||
108
egui/crates/egui_demo_lib/src/demo/text_edit.rs
Normal file
108
egui/crates/egui_demo_lib/src/demo/text_edit.rs
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
/// Showcase [`TextEdit`].
|
||||
#[derive(PartialEq, Eq)]
|
||||
#[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(|i| i.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_mut(|mem| mem.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_mut(|mem| mem.request_focus(text_edit_id)); // give focus back to the [`TextEdit`].
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
111
egui/crates/egui_demo_lib/src/demo/toggle_switch.rs
Normal file
111
egui/crates/egui_demo_lib/src/demo/toggle_switch.rs
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
//! Source code example of how to create your own widget.
|
||||
//! This is meant to be read as a tutorial, hence the plethora of comments.
|
||||
|
||||
/// iOS-style toggle switch:
|
||||
///
|
||||
/// ``` text
|
||||
/// _____________
|
||||
/// / /.....\
|
||||
/// | |.......|
|
||||
/// \_______\_____/
|
||||
/// ```
|
||||
///
|
||||
/// ## Example:
|
||||
/// ``` ignore
|
||||
/// toggle_ui(ui, &mut my_bool);
|
||||
/// ```
|
||||
pub fn toggle_ui(ui: &mut egui::Ui, on: &mut bool) -> egui::Response {
|
||||
// Widget code can be broken up in four steps:
|
||||
// 1. Decide a size for the widget
|
||||
// 2. Allocate space for it
|
||||
// 3. Handle interactions with the widget (if any)
|
||||
// 4. Paint the widget
|
||||
|
||||
// 1. Deciding widget size:
|
||||
// You can query the `ui` how much space is available,
|
||||
// but in this example we have a fixed size widget based on the height of a standard button:
|
||||
let desired_size = ui.spacing().interact_size.y * egui::vec2(2.0, 1.0);
|
||||
|
||||
// 2. Allocating space:
|
||||
// This is where we get a region of the screen assigned.
|
||||
// We also tell the Ui to sense clicks in the allocated region.
|
||||
let (rect, mut response) = ui.allocate_exact_size(desired_size, egui::Sense::click());
|
||||
|
||||
// 3. Interact: Time to check for clicks!
|
||||
if response.clicked() {
|
||||
*on = !*on;
|
||||
response.mark_changed(); // report back that the value changed
|
||||
}
|
||||
|
||||
// Attach some meta-data to the response which can be used by screen readers:
|
||||
response.widget_info(|| egui::WidgetInfo::selected(egui::WidgetType::Checkbox, *on, ""));
|
||||
|
||||
// 4. Paint!
|
||||
// Make sure we need to paint:
|
||||
if ui.is_rect_visible(rect) {
|
||||
// Let's ask for a simple animation from egui.
|
||||
// egui keeps track of changes in the boolean associated with the id and
|
||||
// returns an animated value in the 0-1 range for how much "on" we are.
|
||||
let how_on = ui.ctx().animate_bool(response.id, *on);
|
||||
// We will follow the current style by asking
|
||||
// "how should something that is being interacted with be painted?".
|
||||
// This will, for instance, give us different colors when the widget is hovered or clicked.
|
||||
let visuals = ui.style().interact_selectable(&response, *on);
|
||||
// All coordinates are in absolute screen coordinates so we use `rect` to place the elements.
|
||||
let rect = rect.expand(visuals.expansion);
|
||||
let radius = 0.5 * rect.height();
|
||||
ui.painter()
|
||||
.rect(rect, radius, visuals.bg_fill, visuals.bg_stroke);
|
||||
// Paint the circle, animating it from left to right with `how_on`:
|
||||
let circle_x = egui::lerp((rect.left() + radius)..=(rect.right() - radius), how_on);
|
||||
let center = egui::pos2(circle_x, rect.center().y);
|
||||
ui.painter()
|
||||
.circle(center, 0.75 * radius, visuals.bg_fill, visuals.fg_stroke);
|
||||
}
|
||||
|
||||
// All done! Return the interaction response so the user can check what happened
|
||||
// (hovered, clicked, ...) and maybe show a tooltip:
|
||||
response
|
||||
}
|
||||
|
||||
/// Here is the same code again, but a bit more compact:
|
||||
#[allow(dead_code)]
|
||||
fn toggle_ui_compact(ui: &mut egui::Ui, on: &mut bool) -> egui::Response {
|
||||
let desired_size = ui.spacing().interact_size.y * egui::vec2(2.0, 1.0);
|
||||
let (rect, mut response) = ui.allocate_exact_size(desired_size, egui::Sense::click());
|
||||
if response.clicked() {
|
||||
*on = !*on;
|
||||
response.mark_changed();
|
||||
}
|
||||
response.widget_info(|| egui::WidgetInfo::selected(egui::WidgetType::Checkbox, *on, ""));
|
||||
|
||||
if ui.is_rect_visible(rect) {
|
||||
let how_on = ui.ctx().animate_bool(response.id, *on);
|
||||
let visuals = ui.style().interact_selectable(&response, *on);
|
||||
let rect = rect.expand(visuals.expansion);
|
||||
let radius = 0.5 * rect.height();
|
||||
ui.painter()
|
||||
.rect(rect, radius, visuals.bg_fill, visuals.bg_stroke);
|
||||
let circle_x = egui::lerp((rect.left() + radius)..=(rect.right() - radius), how_on);
|
||||
let center = egui::pos2(circle_x, rect.center().y);
|
||||
ui.painter()
|
||||
.circle(center, 0.75 * radius, visuals.bg_fill, visuals.fg_stroke);
|
||||
}
|
||||
|
||||
response
|
||||
}
|
||||
|
||||
// A wrapper that allows the more idiomatic usage pattern: `ui.add(toggle(&mut my_bool))`
|
||||
/// iOS-style toggle switch.
|
||||
///
|
||||
/// ## Example:
|
||||
/// ``` ignore
|
||||
/// ui.add(toggle(&mut my_bool));
|
||||
/// ```
|
||||
pub fn toggle(on: &mut bool) -> impl egui::Widget + '_ {
|
||||
move |ui: &mut egui::Ui| toggle_ui(ui, on)
|
||||
}
|
||||
|
||||
pub fn url_to_file_source_code() -> String {
|
||||
format!("https://github.com/emilk/egui/blob/master/{}", file!())
|
||||
}
|
||||
290
egui/crates/egui_demo_lib/src/demo/widget_gallery.rs
Normal file
290
egui/crates/egui_demo_lib/src/demo/widget_gallery.rs
Normal file
|
|
@ -0,0 +1,290 @@
|
|||
#[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::NaiveDate>,
|
||||
|
||||
#[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(), Default::default())
|
||||
});
|
||||
|
||||
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.style_mut().wrap = Some(false);
|
||||
ui.set_min_width(60.0);
|
||||
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_naive());
|
||||
ui.add(doc_link_label("DatePickerButton", "DatePickerButton"));
|
||||
ui.add(egui_extras::DatePickerButton::new(date));
|
||||
ui.end_row();
|
||||
}
|
||||
|
||||
ui.add(doc_link_label("Separator", "separator"));
|
||||
ui.separator();
|
||||
ui.end_row();
|
||||
|
||||
ui.add(doc_link_label("CollapsingHeader", "collapsing"));
|
||||
ui.collapsing("Click to see what is hidden!", |ui| {
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = 0.0;
|
||||
ui.label("It's a ");
|
||||
ui.add(doc_link_label("Spinner", "spinner"));
|
||||
ui.add_space(4.0);
|
||||
ui.add(egui::Spinner::new());
|
||||
});
|
||||
});
|
||||
ui.end_row();
|
||||
|
||||
ui.add(doc_link_label("Plot", "plot"));
|
||||
example_plot(ui);
|
||||
ui.end_row();
|
||||
|
||||
ui.hyperlink_to(
|
||||
"Custom widget:",
|
||||
super::toggle_switch::url_to_file_source_code(),
|
||||
);
|
||||
ui.add(super::toggle_switch::toggle(boolean)).on_hover_text(
|
||||
"It's easy to create your own widgets!\n\
|
||||
This toggle switch is just 15 lines of code.",
|
||||
);
|
||||
ui.end_row();
|
||||
}
|
||||
}
|
||||
|
||||
fn example_plot(ui: &mut egui::Ui) -> egui::Response {
|
||||
use egui::plot::{Line, PlotPoints};
|
||||
let n = 128;
|
||||
let line_points: PlotPoints = (0..=n)
|
||||
.map(|i| {
|
||||
use std::f64::consts::TAU;
|
||||
let x = egui::remap(i as f64, 0.0..=n as f64, -TAU..=TAU);
|
||||
[x, x.sin()]
|
||||
})
|
||||
.collect();
|
||||
let line = Line::new(line_points);
|
||||
egui::plot::Plot::new("example_plot")
|
||||
.height(32.0)
|
||||
.data_aspect(1.0)
|
||||
.show(ui, |plot_ui| plot_ui.line(line))
|
||||
.response
|
||||
}
|
||||
|
||||
fn doc_link_label<'a>(title: &'a str, search_term: &'a str) -> impl egui::Widget + 'a {
|
||||
let label = format!("{}:", title);
|
||||
let url = format!("https://docs.rs/egui?search={}", search_term);
|
||||
move |ui: &mut egui::Ui| {
|
||||
ui.hyperlink_to(label, url).on_hover_ui(|ui| {
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
ui.label("Search egui docs for");
|
||||
ui.code(search_term);
|
||||
});
|
||||
})
|
||||
}
|
||||
}
|
||||
141
egui/crates/egui_demo_lib/src/demo/window_options.rs
Normal file
141
egui/crates/egui_demo_lib/src/demo/window_options.rs
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
#[derive(Clone, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct WindowOptions {
|
||||
title: String,
|
||||
title_bar: bool,
|
||||
closable: bool,
|
||||
collapsible: bool,
|
||||
resizable: bool,
|
||||
scroll2: [bool; 2],
|
||||
disabled_time: f64,
|
||||
|
||||
anchored: bool,
|
||||
anchor: egui::Align2,
|
||||
anchor_offset: egui::Vec2,
|
||||
}
|
||||
|
||||
impl Default for WindowOptions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
title: "🗖 Window Options".to_owned(),
|
||||
title_bar: true,
|
||||
closable: true,
|
||||
collapsible: true,
|
||||
resizable: true,
|
||||
scroll2: [true; 2],
|
||||
disabled_time: f64::NEG_INFINITY,
|
||||
anchored: false,
|
||||
anchor: egui::Align2::RIGHT_TOP,
|
||||
anchor_offset: egui::Vec2::ZERO,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl super::Demo for WindowOptions {
|
||||
fn name(&self) -> &'static str {
|
||||
"🗖 Window Options"
|
||||
}
|
||||
|
||||
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
|
||||
let Self {
|
||||
title,
|
||||
title_bar,
|
||||
closable,
|
||||
collapsible,
|
||||
resizable,
|
||||
scroll2,
|
||||
disabled_time,
|
||||
anchored,
|
||||
anchor,
|
||||
anchor_offset,
|
||||
} = self.clone();
|
||||
|
||||
let enabled = ctx.input(|i| i.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(|i| i.time);
|
||||
}
|
||||
egui::reset_button(ui, self);
|
||||
ui.add(crate::egui_github_link_file!());
|
||||
});
|
||||
}
|
||||
}
|
||||
92
egui/crates/egui_demo_lib/src/demo/window_with_panels.rs
Normal file
92
egui/crates/egui_demo_lib/src/demo/window_with_panels.rs
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
#[derive(Clone, Default, PartialEq, Eq)]
|
||||
#[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());
|
||||
ui.add(egui::Separator::default().grow(8.0));
|
||||
ui.label(egui::RichText::new(crate::LOREM_IPSUM_LONG).small().weak());
|
||||
},
|
||||
);
|
||||
}
|
||||
281
egui/crates/egui_demo_lib/src/easy_mark/easy_mark_editor.rs
Normal file
281
egui/crates/egui_demo_lib/src/easy_mark/easy_mark_editor.rs
Normal file
|
|
@ -0,0 +1,281 @@
|
|||
use egui::{text_edit::CCursorRange, *};
|
||||
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
pub struct EasyMarkEditor {
|
||||
code: String,
|
||||
highlight_editor: bool,
|
||||
show_rendered: bool,
|
||||
|
||||
#[cfg_attr(feature = "serde", serde(skip))]
|
||||
highlighter: crate::easy_mark::MemoizedEasymarkHighlighter,
|
||||
}
|
||||
|
||||
impl PartialEq for EasyMarkEditor {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
(&self.code, self.highlight_editor, self.show_rendered)
|
||||
== (&other.code, other.highlight_editor, other.show_rendered)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for EasyMarkEditor {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
code: DEFAULT_CODE.trim().to_owned(),
|
||||
highlight_editor: true,
|
||||
show_rendered: true,
|
||||
highlighter: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EasyMarkEditor {
|
||||
pub fn panels(&mut self, ctx: &egui::Context) {
|
||||
egui::TopBottomPanel::bottom("easy_mark_bottom").show(ctx, |ui| {
|
||||
let layout = egui::Layout::top_down(egui::Align::Center).with_main_justify(true);
|
||||
ui.allocate_ui_with_layout(ui.available_size(), layout, |ui| {
|
||||
ui.add(crate::egui_github_link_file!())
|
||||
})
|
||||
});
|
||||
|
||||
egui::CentralPanel::default().show(ctx, |ui| {
|
||||
self.ui(ui);
|
||||
});
|
||||
}
|
||||
|
||||
pub fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
egui::Grid::new("controls").show(ui, |ui| {
|
||||
let _ = ui.button("Hotkeys").on_hover_ui(nested_hotkeys_ui);
|
||||
ui.checkbox(&mut self.show_rendered, "Show rendered");
|
||||
ui.checkbox(&mut self.highlight_editor, "Highlight editor");
|
||||
egui::reset_button(ui, self);
|
||||
ui.end_row();
|
||||
});
|
||||
ui.separator();
|
||||
|
||||
if self.show_rendered {
|
||||
ui.columns(2, |columns| {
|
||||
ScrollArea::vertical()
|
||||
.id_source("source")
|
||||
.show(&mut columns[0], |ui| self.editor_ui(ui));
|
||||
ScrollArea::vertical()
|
||||
.id_source("rendered")
|
||||
.show(&mut columns[1], |ui| {
|
||||
// TODO(emilk): we can save some more CPU by caching the rendered output.
|
||||
crate::easy_mark::easy_mark(ui, &self.code);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
ScrollArea::vertical()
|
||||
.id_source("source")
|
||||
.show(ui, |ui| self.editor_ui(ui));
|
||||
}
|
||||
}
|
||||
|
||||
fn editor_ui(&mut self, ui: &mut egui::Ui) {
|
||||
let Self {
|
||||
code, highlighter, ..
|
||||
} = self;
|
||||
|
||||
let response = if self.highlight_editor {
|
||||
let mut layouter = |ui: &egui::Ui, easymark: &str, wrap_width: f32| {
|
||||
let mut layout_job = highlighter.highlight(ui.style(), easymark);
|
||||
layout_job.wrap.max_width = wrap_width;
|
||||
ui.fonts(|f| f.layout_job(layout_job))
|
||||
};
|
||||
|
||||
ui.add(
|
||||
egui::TextEdit::multiline(code)
|
||||
.desired_width(f32::INFINITY)
|
||||
.font(egui::TextStyle::Monospace) // for cursor height
|
||||
.layouter(&mut layouter),
|
||||
)
|
||||
} else {
|
||||
ui.add(egui::TextEdit::multiline(code).desired_width(f32::INFINITY))
|
||||
};
|
||||
|
||||
if let Some(mut state) = TextEdit::load_state(ui.ctx(), response.id) {
|
||||
if let Some(mut ccursor_range) = state.ccursor_range() {
|
||||
let any_change = shortcuts(ui, code, &mut ccursor_range);
|
||||
if any_change {
|
||||
state.set_ccursor_range(Some(ccursor_range));
|
||||
state.store(ui.ctx(), response.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub const SHORTCUT_BOLD: KeyboardShortcut = KeyboardShortcut::new(Modifiers::COMMAND, Key::B);
|
||||
pub const SHORTCUT_CODE: KeyboardShortcut = KeyboardShortcut::new(Modifiers::COMMAND, Key::N);
|
||||
pub const SHORTCUT_ITALICS: KeyboardShortcut = KeyboardShortcut::new(Modifiers::COMMAND, Key::I);
|
||||
pub const SHORTCUT_SUBSCRIPT: KeyboardShortcut = KeyboardShortcut::new(Modifiers::COMMAND, Key::L);
|
||||
pub const SHORTCUT_SUPERSCRIPT: KeyboardShortcut =
|
||||
KeyboardShortcut::new(Modifiers::COMMAND, Key::Y);
|
||||
pub const SHORTCUT_STRIKETHROUGH: KeyboardShortcut =
|
||||
KeyboardShortcut::new(Modifiers::CTRL.plus(Modifiers::SHIFT), Key::Q);
|
||||
pub const SHORTCUT_UNDERLINE: KeyboardShortcut =
|
||||
KeyboardShortcut::new(Modifiers::CTRL.plus(Modifiers::SHIFT), Key::W);
|
||||
pub const SHORTCUT_INDENT: KeyboardShortcut =
|
||||
KeyboardShortcut::new(Modifiers::CTRL.plus(Modifiers::SHIFT), Key::E);
|
||||
|
||||
fn nested_hotkeys_ui(ui: &mut egui::Ui) {
|
||||
egui::Grid::new("shortcuts").striped(true).show(ui, |ui| {
|
||||
let mut label = |shortcut, what| {
|
||||
ui.label(what);
|
||||
ui.weak(ui.ctx().format_shortcut(&shortcut));
|
||||
ui.end_row();
|
||||
};
|
||||
|
||||
label(SHORTCUT_BOLD, "*bold*");
|
||||
label(SHORTCUT_CODE, "`code`");
|
||||
label(SHORTCUT_ITALICS, "/italics/");
|
||||
label(SHORTCUT_SUBSCRIPT, "$subscript$");
|
||||
label(SHORTCUT_SUPERSCRIPT, "^superscript^");
|
||||
label(SHORTCUT_STRIKETHROUGH, "~strikethrough~");
|
||||
label(SHORTCUT_UNDERLINE, "_underline_");
|
||||
label(SHORTCUT_INDENT, "two spaces"); // Placeholder for tab indent
|
||||
});
|
||||
}
|
||||
|
||||
fn shortcuts(ui: &Ui, code: &mut dyn TextBuffer, ccursor_range: &mut CCursorRange) -> bool {
|
||||
let mut any_change = false;
|
||||
|
||||
if ui.input_mut(|i| i.consume_shortcut(&SHORTCUT_INDENT)) {
|
||||
// This is a placeholder till we can indent the active line
|
||||
any_change = true;
|
||||
let [primary, _secondary] = ccursor_range.sorted();
|
||||
|
||||
let advance = code.insert_text(" ", primary.index);
|
||||
ccursor_range.primary.index += advance;
|
||||
ccursor_range.secondary.index += advance;
|
||||
}
|
||||
|
||||
for (shortcut, surrounding) in [
|
||||
(SHORTCUT_BOLD, "*"),
|
||||
(SHORTCUT_CODE, "`"),
|
||||
(SHORTCUT_ITALICS, "/"),
|
||||
(SHORTCUT_SUBSCRIPT, "$"),
|
||||
(SHORTCUT_SUPERSCRIPT, "^"),
|
||||
(SHORTCUT_STRIKETHROUGH, "~"),
|
||||
(SHORTCUT_UNDERLINE, "_"),
|
||||
] {
|
||||
if ui.input_mut(|i| i.consume_shortcut(&shortcut)) {
|
||||
any_change = true;
|
||||
toggle_surrounding(code, ccursor_range, surrounding);
|
||||
};
|
||||
}
|
||||
|
||||
any_change
|
||||
}
|
||||
|
||||
/// E.g. toggle *strong* with `toggle_surrounding(&mut text, &mut cursor, "*")`
|
||||
fn toggle_surrounding(
|
||||
code: &mut dyn TextBuffer,
|
||||
ccursor_range: &mut CCursorRange,
|
||||
surrounding: &str,
|
||||
) {
|
||||
let [primary, secondary] = ccursor_range.sorted();
|
||||
|
||||
let surrounding_ccount = surrounding.chars().count();
|
||||
|
||||
let prefix_crange = primary.index.saturating_sub(surrounding_ccount)..primary.index;
|
||||
let suffix_crange = secondary.index..secondary.index.saturating_add(surrounding_ccount);
|
||||
let already_surrounded = code.char_range(prefix_crange.clone()) == surrounding
|
||||
&& code.char_range(suffix_crange.clone()) == surrounding;
|
||||
|
||||
if already_surrounded {
|
||||
code.delete_char_range(suffix_crange);
|
||||
code.delete_char_range(prefix_crange);
|
||||
ccursor_range.primary.index -= surrounding_ccount;
|
||||
ccursor_range.secondary.index -= surrounding_ccount;
|
||||
} else {
|
||||
code.insert_text(surrounding, secondary.index);
|
||||
let advance = code.insert_text(surrounding, primary.index);
|
||||
|
||||
ccursor_range.primary.index += advance;
|
||||
ccursor_range.secondary.index += advance;
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
const DEFAULT_CODE: &str = r#"
|
||||
# EasyMark
|
||||
EasyMark is a markup language, designed for extreme simplicity.
|
||||
|
||||
```
|
||||
WARNING: EasyMark is still an evolving specification,
|
||||
and is also missing some features.
|
||||
```
|
||||
|
||||
----------------
|
||||
|
||||
# At a glance
|
||||
- inline text:
|
||||
- normal, `code`, *strong*, ~strikethrough~, _underline_, /italics/, ^raised^, $small$
|
||||
- `\` escapes the next character
|
||||
- [hyperlink](https://github.com/emilk/egui)
|
||||
- Embedded URL: <https://github.com/emilk/egui>
|
||||
- `# ` header
|
||||
- `---` separator (horizontal line)
|
||||
- `> ` quote
|
||||
- `- ` bullet list
|
||||
- `1. ` numbered list
|
||||
- \`\`\` code fence
|
||||
- a^2^ + b^2^ = c^2^
|
||||
- $Remember to read the small print$
|
||||
|
||||
# Design
|
||||
> /"Why do what everyone else is doing, when everyone else is already doing it?"
|
||||
> \- Emil
|
||||
|
||||
Goals:
|
||||
1. easy to parse
|
||||
2. easy to learn
|
||||
3. similar to markdown
|
||||
|
||||
[The reference parser](https://github.com/emilk/egui/blob/master/crates/egui_demo_lib/src/easy_mark/easy_mark_parser.rs) is \~250 lines of code, using only the Rust standard library. The parser uses no look-ahead or recursion.
|
||||
|
||||
There is never more than one way to accomplish the same thing, and each special character is only used for one thing. For instance `*` is used for *strong* and `-` is used for bullet lists. There is no alternative way to specify the *strong* style or getting a bullet list.
|
||||
|
||||
Similarity to markdown is kept when possible, but with much less ambiguity and some improvements (like _underlining_).
|
||||
|
||||
# Details
|
||||
All style changes are single characters, so it is `*strong*`, NOT `**strong**`. Style is reset by a matching character, or at the end of the line.
|
||||
|
||||
Style change characters and escapes (`\`) work everywhere except for in inline code, code blocks and in URLs.
|
||||
|
||||
You can mix styles. For instance: /italics _underline_/ and *strong `code`*.
|
||||
|
||||
You can use styles on URLs: ~my webpage is at <http://www.example.com>~.
|
||||
|
||||
Newlines are preserved. If you want to continue text on the same line, just do so. Alternatively, escape the newline by ending the line with a backslash (`\`). \
|
||||
Escaping the newline effectively ignores it.
|
||||
|
||||
The style characters are chosen to be similar to what they are representing:
|
||||
`_` = _underline_
|
||||
`~` = ~strikethrough~ (`-` is used for bullet points)
|
||||
`/` = /italics/
|
||||
`*` = *strong*
|
||||
`$` = $small$
|
||||
`^` = ^raised^
|
||||
|
||||
# TODO
|
||||
- Sub-headers (`## h2`, `### h3` etc)
|
||||
- Hotkey Editor
|
||||
- International keyboard algorithm for non-letter keys
|
||||
- ALT+SHIFT+Num1 is not a functioning hotkey
|
||||
- Tab Indent Increment/Decrement CTRL+], CTRL+[
|
||||
|
||||
- Images
|
||||
- we want to be able to optionally specify size (width and\/or height)
|
||||
- centering of images is very desirable
|
||||
- captioning (image with a text underneath it)
|
||||
- `![caption=My image][width=200][center](url)` ?
|
||||
- Nicer URL:s
|
||||
- `<url>` and `[url](url)` do the same thing yet look completely different.
|
||||
- let's keep similarity with images
|
||||
- Tables
|
||||
- Inspiration: <https://mycorrhiza.lesarbr.es/page/mycomarkup>
|
||||
"#;
|
||||
192
egui/crates/egui_demo_lib/src/easy_mark/easy_mark_highlighter.rs
Normal file
192
egui/crates/egui_demo_lib/src/easy_mark/easy_mark_highlighter.rs
Normal file
|
|
@ -0,0 +1,192 @@
|
|||
use crate::easy_mark::easy_mark_parser;
|
||||
|
||||
/// Highlight easymark, memoizing previous output to save CPU.
|
||||
///
|
||||
/// In practice, the highlighter is fast enough not to need any caching.
|
||||
#[derive(Default)]
|
||||
pub struct MemoizedEasymarkHighlighter {
|
||||
style: egui::Style,
|
||||
code: String,
|
||||
output: egui::text::LayoutJob,
|
||||
}
|
||||
|
||||
impl MemoizedEasymarkHighlighter {
|
||||
pub fn highlight(&mut self, egui_style: &egui::Style, code: &str) -> egui::text::LayoutJob {
|
||||
if (&self.style, self.code.as_str()) != (egui_style, code) {
|
||||
self.style = egui_style.clone();
|
||||
self.code = code.to_owned();
|
||||
self.output = highlight_easymark(egui_style, code);
|
||||
}
|
||||
self.output.clone()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn highlight_easymark(egui_style: &egui::Style, mut text: &str) -> egui::text::LayoutJob {
|
||||
let mut job = egui::text::LayoutJob::default();
|
||||
let mut style = easy_mark_parser::Style::default();
|
||||
let mut start_of_line = true;
|
||||
|
||||
while !text.is_empty() {
|
||||
if start_of_line && text.starts_with("```") {
|
||||
let end = text.find("\n```").map_or_else(|| text.len(), |i| i + 4);
|
||||
job.append(
|
||||
&text[..end],
|
||||
0.0,
|
||||
format_from_style(
|
||||
egui_style,
|
||||
&easy_mark_parser::Style {
|
||||
code: true,
|
||||
..Default::default()
|
||||
},
|
||||
),
|
||||
);
|
||||
text = &text[end..];
|
||||
style = Default::default();
|
||||
continue;
|
||||
}
|
||||
|
||||
if text.starts_with('`') {
|
||||
style.code = true;
|
||||
let end = text[1..]
|
||||
.find(&['`', '\n'][..])
|
||||
.map_or_else(|| text.len(), |i| i + 2);
|
||||
job.append(&text[..end], 0.0, format_from_style(egui_style, &style));
|
||||
text = &text[end..];
|
||||
style.code = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut skip;
|
||||
|
||||
if text.starts_with('\\') && text.len() >= 2 {
|
||||
skip = 2;
|
||||
} else if start_of_line && text.starts_with(' ') {
|
||||
// we don't preview indentation, because it is confusing
|
||||
skip = 1;
|
||||
} else if start_of_line && text.starts_with("# ") {
|
||||
style.heading = true;
|
||||
skip = 2;
|
||||
} else if start_of_line && text.starts_with("> ") {
|
||||
style.quoted = true;
|
||||
skip = 2;
|
||||
// we don't preview indentation, because it is confusing
|
||||
} else if start_of_line && text.starts_with("- ") {
|
||||
skip = 2;
|
||||
// we don't preview indentation, because it is confusing
|
||||
} else if text.starts_with('*') {
|
||||
skip = 1;
|
||||
if style.strong {
|
||||
// Include the character that is ending this style:
|
||||
job.append(&text[..skip], 0.0, format_from_style(egui_style, &style));
|
||||
text = &text[skip..];
|
||||
skip = 0;
|
||||
}
|
||||
style.strong ^= true;
|
||||
} else if text.starts_with('$') {
|
||||
skip = 1;
|
||||
if style.small {
|
||||
// Include the character that is ending this style:
|
||||
job.append(&text[..skip], 0.0, format_from_style(egui_style, &style));
|
||||
text = &text[skip..];
|
||||
skip = 0;
|
||||
}
|
||||
style.small ^= true;
|
||||
} else if text.starts_with('^') {
|
||||
skip = 1;
|
||||
if style.raised {
|
||||
// Include the character that is ending this style:
|
||||
job.append(&text[..skip], 0.0, format_from_style(egui_style, &style));
|
||||
text = &text[skip..];
|
||||
skip = 0;
|
||||
}
|
||||
style.raised ^= true;
|
||||
} else {
|
||||
skip = 0;
|
||||
}
|
||||
// Note: we don't preview underline, strikethrough and italics because it confuses things.
|
||||
|
||||
// Swallow everything up to the next special character:
|
||||
let line_end = text[skip..]
|
||||
.find('\n')
|
||||
.map_or_else(|| text.len(), |i| (skip + i + 1));
|
||||
let end = text[skip..]
|
||||
.find(&['*', '`', '~', '_', '/', '$', '^', '\\', '<', '['][..])
|
||||
.map_or_else(|| text.len(), |i| (skip + i).max(1));
|
||||
|
||||
if line_end <= end {
|
||||
job.append(
|
||||
&text[..line_end],
|
||||
0.0,
|
||||
format_from_style(egui_style, &style),
|
||||
);
|
||||
text = &text[line_end..];
|
||||
start_of_line = true;
|
||||
style = Default::default();
|
||||
} else {
|
||||
job.append(&text[..end], 0.0, format_from_style(egui_style, &style));
|
||||
text = &text[end..];
|
||||
start_of_line = false;
|
||||
}
|
||||
}
|
||||
|
||||
job
|
||||
}
|
||||
|
||||
fn format_from_style(
|
||||
egui_style: &egui::Style,
|
||||
emark_style: &easy_mark_parser::Style,
|
||||
) -> egui::text::TextFormat {
|
||||
use egui::{Align, Color32, Stroke, TextStyle};
|
||||
|
||||
let color = if emark_style.strong || emark_style.heading {
|
||||
egui_style.visuals.strong_text_color()
|
||||
} else if emark_style.quoted {
|
||||
egui_style.visuals.weak_text_color()
|
||||
} else {
|
||||
egui_style.visuals.text_color()
|
||||
};
|
||||
|
||||
let text_style = if emark_style.heading {
|
||||
TextStyle::Heading
|
||||
} else if emark_style.code {
|
||||
TextStyle::Monospace
|
||||
} else if emark_style.small | emark_style.raised {
|
||||
TextStyle::Small
|
||||
} else {
|
||||
TextStyle::Body
|
||||
};
|
||||
|
||||
let background = if emark_style.code {
|
||||
egui_style.visuals.code_bg_color
|
||||
} else {
|
||||
Color32::TRANSPARENT
|
||||
};
|
||||
|
||||
let underline = if emark_style.underline {
|
||||
Stroke::new(1.0, color)
|
||||
} else {
|
||||
Stroke::NONE
|
||||
};
|
||||
|
||||
let strikethrough = if emark_style.strikethrough {
|
||||
Stroke::new(1.0, color)
|
||||
} else {
|
||||
Stroke::NONE
|
||||
};
|
||||
|
||||
let valign = if emark_style.raised {
|
||||
Align::TOP
|
||||
} else {
|
||||
Align::BOTTOM
|
||||
};
|
||||
|
||||
egui::text::TextFormat {
|
||||
font_id: text_style.resolve(egui_style),
|
||||
color,
|
||||
background,
|
||||
italics: emark_style.italics,
|
||||
underline,
|
||||
strikethrough,
|
||||
valign,
|
||||
}
|
||||
}
|
||||
356
egui/crates/egui_demo_lib/src/easy_mark/easy_mark_parser.rs
Normal file
356
egui/crates/egui_demo_lib/src/easy_mark/easy_mark_parser.rs
Normal file
|
|
@ -0,0 +1,356 @@
|
|||
//! A parser for `EasyMark`: a very simple markup language.
|
||||
//!
|
||||
//! WARNING: `EasyMark` is subject to change.
|
||||
//
|
||||
//! # `EasyMark` design goals:
|
||||
//! 1. easy to parse
|
||||
//! 2. easy to learn
|
||||
//! 3. similar to markdown
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||
pub enum Item<'a> {
|
||||
/// `\n`
|
||||
// TODO(emilk): add Style here so empty heading still uses up the right amount of space.
|
||||
Newline,
|
||||
|
||||
///
|
||||
Text(Style, &'a str),
|
||||
|
||||
/// title, url
|
||||
Hyperlink(Style, &'a str, &'a str),
|
||||
|
||||
/// leading space before e.g. a [`Self::BulletPoint`].
|
||||
Indentation(usize),
|
||||
|
||||
/// >
|
||||
QuoteIndent,
|
||||
|
||||
/// - a point well made.
|
||||
BulletPoint,
|
||||
|
||||
/// 1. numbered list. The string is the number(s).
|
||||
NumberedPoint(&'a str),
|
||||
|
||||
/// ---
|
||||
Separator,
|
||||
|
||||
/// language, code
|
||||
CodeBlock(&'a str, &'a str),
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
|
||||
pub struct Style {
|
||||
/// # heading (large text)
|
||||
pub heading: bool,
|
||||
|
||||
/// > quoted (slightly dimmer color or other font style)
|
||||
pub quoted: bool,
|
||||
|
||||
/// `code` (monospace, some other color)
|
||||
pub code: bool,
|
||||
|
||||
/// self.strong* (emphasized, e.g. bold)
|
||||
pub strong: bool,
|
||||
|
||||
/// _underline_
|
||||
pub underline: bool,
|
||||
|
||||
/// ~strikethrough~
|
||||
pub strikethrough: bool,
|
||||
|
||||
/// /italics/
|
||||
pub italics: bool,
|
||||
|
||||
/// $small$
|
||||
pub small: bool,
|
||||
|
||||
/// ^raised^
|
||||
pub raised: bool,
|
||||
}
|
||||
|
||||
/// Parser for the `EasyMark` markup language.
|
||||
///
|
||||
/// See the module-level documentation for details.
|
||||
///
|
||||
/// # Example:
|
||||
/// ```
|
||||
/// # use egui_demo_lib::easy_mark::parser::Parser;
|
||||
/// for item in Parser::new("Hello *world*!") {
|
||||
/// }
|
||||
///
|
||||
/// ```
|
||||
pub struct Parser<'a> {
|
||||
/// The remainder of the input text
|
||||
s: &'a str,
|
||||
|
||||
/// Are we at the start of a line?
|
||||
start_of_line: bool,
|
||||
|
||||
/// Current self.style. Reset after a newline.
|
||||
style: Style,
|
||||
}
|
||||
|
||||
impl<'a> Parser<'a> {
|
||||
pub fn new(s: &'a str) -> Self {
|
||||
Self {
|
||||
s,
|
||||
start_of_line: true,
|
||||
style: Style::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// `1. `, `42. ` etc.
|
||||
fn numbered_list(&mut self) -> Option<Item<'a>> {
|
||||
let n_digits = self.s.chars().take_while(|c| c.is_ascii_digit()).count();
|
||||
if n_digits > 0 && self.s.chars().skip(n_digits).take(2).eq(". ".chars()) {
|
||||
let number = &self.s[..n_digits];
|
||||
self.s = &self.s[(n_digits + 2)..];
|
||||
self.start_of_line = false;
|
||||
return Some(Item::NumberedPoint(number));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
// ```{language}\n{code}```
|
||||
fn code_block(&mut self) -> Option<Item<'a>> {
|
||||
if let Some(language_start) = self.s.strip_prefix("```") {
|
||||
if let Some(newline) = language_start.find('\n') {
|
||||
let language = &language_start[..newline];
|
||||
let code_start = &language_start[newline + 1..];
|
||||
if let Some(end) = code_start.find("\n```") {
|
||||
let code = &code_start[..end].trim();
|
||||
self.s = &code_start[end + 4..];
|
||||
self.start_of_line = false;
|
||||
return Some(Item::CodeBlock(language, code));
|
||||
} else {
|
||||
self.s = "";
|
||||
return Some(Item::CodeBlock(language, code_start));
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
// `code`
|
||||
fn inline_code(&mut self) -> Option<Item<'a>> {
|
||||
if let Some(rest) = self.s.strip_prefix('`') {
|
||||
self.s = rest;
|
||||
self.start_of_line = false;
|
||||
self.style.code = true;
|
||||
let rest_of_line = &self.s[..self.s.find('\n').unwrap_or(self.s.len())];
|
||||
if let Some(end) = rest_of_line.find('`') {
|
||||
let item = Item::Text(self.style, &self.s[..end]);
|
||||
self.s = &self.s[end + 1..];
|
||||
self.style.code = false;
|
||||
return Some(item);
|
||||
} else {
|
||||
let end = rest_of_line.len();
|
||||
let item = Item::Text(self.style, rest_of_line);
|
||||
self.s = &self.s[end..];
|
||||
self.style.code = false;
|
||||
return Some(item);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// `<url>` or `[link](url)`
|
||||
fn url(&mut self) -> Option<Item<'a>> {
|
||||
if self.s.starts_with('<') {
|
||||
let this_line = &self.s[..self.s.find('\n').unwrap_or(self.s.len())];
|
||||
if let Some(url_end) = this_line.find('>') {
|
||||
let url = &self.s[1..url_end];
|
||||
self.s = &self.s[url_end + 1..];
|
||||
self.start_of_line = false;
|
||||
return Some(Item::Hyperlink(self.style, url, url));
|
||||
}
|
||||
}
|
||||
|
||||
// [text](url)
|
||||
if self.s.starts_with('[') {
|
||||
let this_line = &self.s[..self.s.find('\n').unwrap_or(self.s.len())];
|
||||
if let Some(bracket_end) = this_line.find(']') {
|
||||
let text = &this_line[1..bracket_end];
|
||||
if this_line[bracket_end + 1..].starts_with('(') {
|
||||
if let Some(parens_end) = this_line[bracket_end + 2..].find(')') {
|
||||
let parens_end = bracket_end + 2 + parens_end;
|
||||
let url = &self.s[bracket_end + 2..parens_end];
|
||||
self.s = &self.s[parens_end + 1..];
|
||||
self.start_of_line = false;
|
||||
return Some(Item::Hyperlink(self.style, text, url));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for Parser<'a> {
|
||||
type Item = Item<'a>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
loop {
|
||||
if self.s.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// \n
|
||||
if self.s.starts_with('\n') {
|
||||
self.s = &self.s[1..];
|
||||
self.start_of_line = true;
|
||||
self.style = Style::default();
|
||||
return Some(Item::Newline);
|
||||
}
|
||||
|
||||
// Ignore line break (continue on the same line)
|
||||
if self.s.starts_with("\\\n") && self.s.len() >= 2 {
|
||||
self.s = &self.s[2..];
|
||||
self.start_of_line = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
// \ escape (to show e.g. a backtick)
|
||||
if self.s.starts_with('\\') && self.s.len() >= 2 {
|
||||
let text = &self.s[1..2];
|
||||
self.s = &self.s[2..];
|
||||
self.start_of_line = false;
|
||||
return Some(Item::Text(self.style, text));
|
||||
}
|
||||
|
||||
if self.start_of_line {
|
||||
// leading space (indentation)
|
||||
if self.s.starts_with(' ') {
|
||||
let length = self.s.find(|c| c != ' ').unwrap_or(self.s.len());
|
||||
self.s = &self.s[length..];
|
||||
self.start_of_line = true; // indentation doesn't count
|
||||
return Some(Item::Indentation(length));
|
||||
}
|
||||
|
||||
// # Heading
|
||||
if let Some(after) = self.s.strip_prefix("# ") {
|
||||
self.s = after;
|
||||
self.start_of_line = false;
|
||||
self.style.heading = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
// > quote
|
||||
if let Some(after) = self.s.strip_prefix("> ") {
|
||||
self.s = after;
|
||||
self.start_of_line = true; // quote indentation doesn't count
|
||||
self.style.quoted = true;
|
||||
return Some(Item::QuoteIndent);
|
||||
}
|
||||
|
||||
// - bullet point
|
||||
if self.s.starts_with("- ") {
|
||||
self.s = &self.s[2..];
|
||||
self.start_of_line = false;
|
||||
return Some(Item::BulletPoint);
|
||||
}
|
||||
|
||||
// `1. `, `42. ` etc.
|
||||
if let Some(item) = self.numbered_list() {
|
||||
return Some(item);
|
||||
}
|
||||
|
||||
// --- separator
|
||||
if let Some(after) = self.s.strip_prefix("---") {
|
||||
self.s = after.trim_start_matches('-'); // remove extra dashes
|
||||
self.s = self.s.strip_prefix('\n').unwrap_or(self.s); // remove trailing newline
|
||||
self.start_of_line = false;
|
||||
return Some(Item::Separator);
|
||||
}
|
||||
|
||||
// ```{language}\n{code}```
|
||||
if let Some(item) = self.code_block() {
|
||||
return Some(item);
|
||||
}
|
||||
}
|
||||
|
||||
// `code`
|
||||
if let Some(item) = self.inline_code() {
|
||||
return Some(item);
|
||||
}
|
||||
|
||||
if let Some(rest) = self.s.strip_prefix('*') {
|
||||
self.s = rest;
|
||||
self.start_of_line = false;
|
||||
self.style.strong = !self.style.strong;
|
||||
continue;
|
||||
}
|
||||
if let Some(rest) = self.s.strip_prefix('_') {
|
||||
self.s = rest;
|
||||
self.start_of_line = false;
|
||||
self.style.underline = !self.style.underline;
|
||||
continue;
|
||||
}
|
||||
if let Some(rest) = self.s.strip_prefix('~') {
|
||||
self.s = rest;
|
||||
self.start_of_line = false;
|
||||
self.style.strikethrough = !self.style.strikethrough;
|
||||
continue;
|
||||
}
|
||||
if let Some(rest) = self.s.strip_prefix('/') {
|
||||
self.s = rest;
|
||||
self.start_of_line = false;
|
||||
self.style.italics = !self.style.italics;
|
||||
continue;
|
||||
}
|
||||
if let Some(rest) = self.s.strip_prefix('$') {
|
||||
self.s = rest;
|
||||
self.start_of_line = false;
|
||||
self.style.small = !self.style.small;
|
||||
continue;
|
||||
}
|
||||
if let Some(rest) = self.s.strip_prefix('^') {
|
||||
self.s = rest;
|
||||
self.start_of_line = false;
|
||||
self.style.raised = !self.style.raised;
|
||||
continue;
|
||||
}
|
||||
|
||||
// `<url>` or `[link](url)`
|
||||
if let Some(item) = self.url() {
|
||||
return Some(item);
|
||||
}
|
||||
|
||||
// Swallow everything up to the next special character:
|
||||
let end = self
|
||||
.s
|
||||
.find(&['*', '`', '~', '_', '/', '$', '^', '\\', '<', '[', '\n'][..])
|
||||
.map_or_else(|| self.s.len(), |special| special.max(1));
|
||||
|
||||
let item = Item::Text(self.style, &self.s[..end]);
|
||||
self.s = &self.s[end..];
|
||||
self.start_of_line = false;
|
||||
return Some(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_easy_mark_parser() {
|
||||
let items: Vec<_> = Parser::new("~strikethrough `code`~").collect();
|
||||
assert_eq!(
|
||||
items,
|
||||
vec![
|
||||
Item::Text(
|
||||
Style {
|
||||
strikethrough: true,
|
||||
..Default::default()
|
||||
},
|
||||
"strikethrough "
|
||||
),
|
||||
Item::Text(
|
||||
Style {
|
||||
code: true,
|
||||
strikethrough: true,
|
||||
..Default::default()
|
||||
},
|
||||
"code"
|
||||
),
|
||||
]
|
||||
);
|
||||
}
|
||||
174
egui/crates/egui_demo_lib/src/easy_mark/easy_mark_viewer.rs
Normal file
174
egui/crates/egui_demo_lib/src/easy_mark/easy_mark_viewer.rs
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
use super::easy_mark_parser as easy_mark;
|
||||
use egui::*;
|
||||
|
||||
/// Parse and display a VERY simple and small subset of Markdown.
|
||||
pub fn easy_mark(ui: &mut Ui, easy_mark: &str) {
|
||||
easy_mark_it(ui, easy_mark::Parser::new(easy_mark));
|
||||
}
|
||||
|
||||
pub fn easy_mark_it<'em>(ui: &mut Ui, items: impl Iterator<Item = easy_mark::Item<'em>>) {
|
||||
let initial_size = vec2(
|
||||
ui.available_width(),
|
||||
ui.spacing().interact_size.y, // Assume there will be
|
||||
);
|
||||
|
||||
let layout = Layout::left_to_right(Align::BOTTOM).with_main_wrap(true);
|
||||
|
||||
ui.allocate_ui_with_layout(initial_size, layout, |ui| {
|
||||
ui.spacing_mut().item_spacing.x = 0.0;
|
||||
let row_height = ui.text_style_height(&TextStyle::Body);
|
||||
ui.set_row_height(row_height);
|
||||
|
||||
for item in items {
|
||||
item_ui(ui, item);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn item_ui(ui: &mut Ui, item: easy_mark::Item<'_>) {
|
||||
let row_height = ui.text_style_height(&TextStyle::Body);
|
||||
let one_indent = row_height / 2.0;
|
||||
|
||||
match item {
|
||||
easy_mark::Item::Newline => {
|
||||
// ui.label("\n"); // too much spacing (paragraph spacing)
|
||||
ui.allocate_exact_size(vec2(0.0, row_height), Sense::hover()); // make sure we take up some height
|
||||
ui.end_row();
|
||||
ui.set_row_height(row_height);
|
||||
}
|
||||
|
||||
easy_mark::Item::Text(style, text) => {
|
||||
let label = rich_text_from_style(text, &style);
|
||||
if style.small && !style.raised {
|
||||
ui.with_layout(Layout::left_to_right(Align::BOTTOM), |ui| {
|
||||
ui.set_min_height(row_height);
|
||||
ui.label(label);
|
||||
});
|
||||
} else {
|
||||
ui.label(label);
|
||||
}
|
||||
}
|
||||
easy_mark::Item::Hyperlink(style, text, url) => {
|
||||
let label = rich_text_from_style(text, &style);
|
||||
if style.small && !style.raised {
|
||||
ui.with_layout(Layout::left_to_right(Align::BOTTOM), |ui| {
|
||||
ui.set_height(row_height);
|
||||
ui.add(Hyperlink::from_label_and_url(label, url));
|
||||
});
|
||||
} else {
|
||||
ui.add(Hyperlink::from_label_and_url(label, url));
|
||||
}
|
||||
}
|
||||
|
||||
easy_mark::Item::Separator => {
|
||||
ui.add(Separator::default().horizontal());
|
||||
}
|
||||
easy_mark::Item::Indentation(indent) => {
|
||||
let indent = indent as f32 * one_indent;
|
||||
ui.allocate_exact_size(vec2(indent, row_height), Sense::hover());
|
||||
}
|
||||
easy_mark::Item::QuoteIndent => {
|
||||
let rect = ui
|
||||
.allocate_exact_size(vec2(2.0 * one_indent, row_height), Sense::hover())
|
||||
.0;
|
||||
let rect = rect.expand2(ui.style().spacing.item_spacing * 0.5);
|
||||
ui.painter().line_segment(
|
||||
[rect.center_top(), rect.center_bottom()],
|
||||
(1.0, ui.visuals().weak_text_color()),
|
||||
);
|
||||
}
|
||||
easy_mark::Item::BulletPoint => {
|
||||
ui.allocate_exact_size(vec2(one_indent, row_height), Sense::hover());
|
||||
bullet_point(ui, one_indent);
|
||||
ui.allocate_exact_size(vec2(one_indent, row_height), Sense::hover());
|
||||
}
|
||||
easy_mark::Item::NumberedPoint(number) => {
|
||||
let width = 3.0 * one_indent;
|
||||
numbered_point(ui, width, number);
|
||||
ui.allocate_exact_size(vec2(one_indent, row_height), Sense::hover());
|
||||
}
|
||||
easy_mark::Item::CodeBlock(_language, code) => {
|
||||
let where_to_put_background = ui.painter().add(Shape::Noop);
|
||||
let mut rect = ui.monospace(code).rect;
|
||||
rect = rect.expand(1.0); // looks better
|
||||
rect.max.x = ui.max_rect().max.x;
|
||||
let code_bg_color = ui.visuals().code_bg_color;
|
||||
ui.painter().set(
|
||||
where_to_put_background,
|
||||
Shape::rect_filled(rect, 1.0, code_bg_color),
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
fn rich_text_from_style(text: &str, style: &easy_mark::Style) -> RichText {
|
||||
let easy_mark::Style {
|
||||
heading,
|
||||
quoted,
|
||||
code,
|
||||
strong,
|
||||
underline,
|
||||
strikethrough,
|
||||
italics,
|
||||
small,
|
||||
raised,
|
||||
} = *style;
|
||||
|
||||
let small = small || raised; // Raised text is also smaller
|
||||
|
||||
let mut rich_text = RichText::new(text);
|
||||
if heading && !small {
|
||||
rich_text = rich_text.heading().strong();
|
||||
}
|
||||
if small && !heading {
|
||||
rich_text = rich_text.small();
|
||||
}
|
||||
if code {
|
||||
rich_text = rich_text.code();
|
||||
}
|
||||
if strong {
|
||||
rich_text = rich_text.strong();
|
||||
} else if quoted {
|
||||
rich_text = rich_text.weak();
|
||||
}
|
||||
if underline {
|
||||
rich_text = rich_text.underline();
|
||||
}
|
||||
if strikethrough {
|
||||
rich_text = rich_text.strikethrough();
|
||||
}
|
||||
if italics {
|
||||
rich_text = rich_text.italics();
|
||||
}
|
||||
if raised {
|
||||
rich_text = rich_text.raised();
|
||||
}
|
||||
rich_text
|
||||
}
|
||||
|
||||
fn bullet_point(ui: &mut Ui, width: f32) -> Response {
|
||||
let row_height = ui.text_style_height(&TextStyle::Body);
|
||||
let (rect, response) = ui.allocate_exact_size(vec2(width, row_height), Sense::hover());
|
||||
ui.painter().circle_filled(
|
||||
rect.center(),
|
||||
rect.height() / 8.0,
|
||||
ui.visuals().strong_text_color(),
|
||||
);
|
||||
response
|
||||
}
|
||||
|
||||
fn numbered_point(ui: &mut Ui, width: f32, number: &str) -> Response {
|
||||
let font_id = TextStyle::Body.resolve(ui.style());
|
||||
let row_height = ui.fonts(|f| f.row_height(&font_id));
|
||||
let (rect, response) = ui.allocate_exact_size(vec2(width, row_height), Sense::hover());
|
||||
let text = format!("{}.", number);
|
||||
let text_color = ui.visuals().strong_text_color();
|
||||
ui.painter().text(
|
||||
rect.right_center(),
|
||||
Align2::RIGHT_CENTER,
|
||||
text,
|
||||
font_id,
|
||||
text_color,
|
||||
);
|
||||
response
|
||||
}
|
||||
11
egui/crates/egui_demo_lib/src/easy_mark/mod.rs
Normal file
11
egui/crates/egui_demo_lib/src/easy_mark/mod.rs
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
//! Experimental markup language
|
||||
|
||||
mod easy_mark_editor;
|
||||
mod easy_mark_highlighter;
|
||||
pub mod easy_mark_parser;
|
||||
mod easy_mark_viewer;
|
||||
|
||||
pub use easy_mark_editor::EasyMarkEditor;
|
||||
pub use easy_mark_highlighter::MemoizedEasymarkHighlighter;
|
||||
pub use easy_mark_parser as parser;
|
||||
pub use easy_mark_viewer::easy_mark;
|
||||
109
egui/crates/egui_demo_lib/src/lib.rs
Normal file
109
egui/crates/egui_demo_lib/src/lib.rs
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
//! Demo-code for showing how egui is used.
|
||||
//!
|
||||
//! This library can be used to test 3rd party egui integrations (see for instance <https://github.com/not-fl3/egui-miniquad/blob/master/examples/demo.rs>).
|
||||
//!
|
||||
//! The demo is also used in benchmarks and tests.
|
||||
//!
|
||||
//! ## Feature flags
|
||||
#![cfg_attr(feature = "document-features", doc = document_features::document_features!())]
|
||||
//!
|
||||
|
||||
#![allow(clippy::float_cmp)]
|
||||
#![allow(clippy::manual_range_contains)]
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
mod color_test;
|
||||
mod demo;
|
||||
pub mod easy_mark;
|
||||
pub mod syntax_highlighting;
|
||||
|
||||
pub use color_test::ColorTest;
|
||||
pub use demo::DemoWindows;
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// Create a [`Hyperlink`](egui::Hyperlink) to this egui source code file on github.
|
||||
#[macro_export]
|
||||
macro_rules! egui_github_link_file {
|
||||
() => {
|
||||
$crate::egui_github_link_file!("(source code)")
|
||||
};
|
||||
($label: expr) => {
|
||||
egui::github_link_file!(
|
||||
"https://github.com/emilk/egui/blob/master/",
|
||||
egui::RichText::new($label).small()
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
/// Create a [`Hyperlink`](egui::Hyperlink) to this egui source code file and line on github.
|
||||
#[macro_export]
|
||||
macro_rules! egui_github_link_file_line {
|
||||
() => {
|
||||
$crate::egui_github_link_file_line!("(source code)")
|
||||
};
|
||||
($label: expr) => {
|
||||
egui::github_link_file_line!(
|
||||
"https://github.com/emilk/egui/blob/master/",
|
||||
egui::RichText::new($label).small()
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
pub const LOREM_IPSUM: &str = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.";
|
||||
|
||||
pub const LOREM_IPSUM_LONG: &str = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
|
||||
|
||||
Curabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam various, turpis et commodo pharetra, est eros bibendum elit, nec luctus magna felis sollicitudin mauris. Integer in mauris eu nibh euismod gravida. Duis ac tellus et risus vulputate vehicula. Donec lobortis risus a elit. Etiam tempor. Ut ullamcorper, ligula eu tempor congue, eros est euismod turpis, id tincidunt sapien risus a quam. Maecenas fermentum consequat mi. Donec fermentum. Pellentesque malesuada nulla a mi. Duis sapien sem, aliquet nec, commodo eget, consequat quis, neque. Aliquam faucibus, elit ut dictum aliquet, felis nisl adipiscing sapien, sed malesuada diam lacus eget erat. Cras mollis scelerisque nunc. Nullam arcu. Aliquam consequat. Curabitur augue lorem, dapibus quis, laoreet et, pretium ac, nisi. Aenean magna nisl, mollis quis, molestie eu, feugiat in, orci. In hac habitasse platea dictumst.";
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn test_egui_e2e() {
|
||||
let mut demo_windows = crate::DemoWindows::default();
|
||||
let ctx = egui::Context::default();
|
||||
let raw_input = egui::RawInput::default();
|
||||
|
||||
const NUM_FRAMES: usize = 5;
|
||||
for _ in 0..NUM_FRAMES {
|
||||
let full_output = ctx.run(raw_input.clone(), |ctx| {
|
||||
demo_windows.ui(ctx);
|
||||
});
|
||||
let clipped_primitives = ctx.tessellate(full_output.shapes);
|
||||
assert!(!clipped_primitives.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_egui_zero_window_size() {
|
||||
let mut demo_windows = crate::DemoWindows::default();
|
||||
let ctx = egui::Context::default();
|
||||
let raw_input = egui::RawInput {
|
||||
screen_rect: Some(egui::Rect::from_min_max(egui::Pos2::ZERO, egui::Pos2::ZERO)),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
const NUM_FRAMES: usize = 5;
|
||||
for _ in 0..NUM_FRAMES {
|
||||
let full_output = ctx.run(raw_input.clone(), |ctx| {
|
||||
demo_windows.ui(ctx);
|
||||
});
|
||||
let clipped_primitives = ctx.tessellate(full_output.shapes);
|
||||
assert!(
|
||||
clipped_primitives.is_empty(),
|
||||
"There should be nothing to show, has at least one primitive with clip_rect: {:?}",
|
||||
clipped_primitives[0].clip_rect
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// Detect narrow screens. This is used to show a simpler UI on mobile devices,
|
||||
/// especially for the web demo at <https://egui.rs>.
|
||||
pub fn is_mobile(ctx: &egui::Context) -> bool {
|
||||
let screen_size = ctx.screen_rect().size();
|
||||
screen_size.x < 550.0
|
||||
}
|
||||
506
egui/crates/egui_demo_lib/src/syntax_highlighting.rs
Normal file
506
egui/crates/egui_demo_lib/src/syntax_highlighting.rs
Normal file
|
|
@ -0,0 +1,506 @@
|
|||
use egui::text::LayoutJob;
|
||||
|
||||
/// View some code with syntax highlighting and selection.
|
||||
pub fn code_view_ui(ui: &mut egui::Ui, mut code: &str) {
|
||||
let language = "rs";
|
||||
let theme = CodeTheme::from_memory(ui.ctx());
|
||||
|
||||
let mut layouter = |ui: &egui::Ui, string: &str, _wrap_width: f32| {
|
||||
let layout_job = highlight(ui.ctx(), &theme, string, language);
|
||||
// layout_job.wrap.max_width = wrap_width; // no wrapping
|
||||
ui.fonts(|f| f.layout_job(layout_job))
|
||||
};
|
||||
|
||||
ui.add(
|
||||
egui::TextEdit::multiline(&mut code)
|
||||
.font(egui::TextStyle::Monospace) // for cursor height
|
||||
.code_editor()
|
||||
.desired_rows(1)
|
||||
.lock_focus(true)
|
||||
.layouter(&mut layouter),
|
||||
);
|
||||
}
|
||||
|
||||
/// Memoized Code highlighting
|
||||
pub fn highlight(ctx: &egui::Context, theme: &CodeTheme, code: &str, language: &str) -> LayoutJob {
|
||||
impl egui::util::cache::ComputerMut<(&CodeTheme, &str, &str), LayoutJob> for Highlighter {
|
||||
fn compute(&mut self, (theme, code, lang): (&CodeTheme, &str, &str)) -> LayoutJob {
|
||||
self.highlight(theme, code, lang)
|
||||
}
|
||||
}
|
||||
|
||||
type HighlightCache = egui::util::cache::FrameCache<LayoutJob, Highlighter>;
|
||||
|
||||
ctx.memory_mut(|mem| {
|
||||
mem.caches
|
||||
.cache::<HighlightCache>()
|
||||
.get((theme, code, language))
|
||||
})
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[cfg(not(feature = "syntect"))]
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[derive(enum_map::Enum)]
|
||||
enum TokenType {
|
||||
Comment,
|
||||
Keyword,
|
||||
Literal,
|
||||
StringLiteral,
|
||||
Punctuation,
|
||||
Whitespace,
|
||||
}
|
||||
|
||||
#[cfg(feature = "syntect")]
|
||||
#[derive(Clone, Copy, Hash, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
enum SyntectTheme {
|
||||
Base16EightiesDark,
|
||||
Base16MochaDark,
|
||||
Base16OceanDark,
|
||||
Base16OceanLight,
|
||||
InspiredGitHub,
|
||||
SolarizedDark,
|
||||
SolarizedLight,
|
||||
}
|
||||
|
||||
#[cfg(feature = "syntect")]
|
||||
impl SyntectTheme {
|
||||
fn all() -> impl ExactSizeIterator<Item = Self> {
|
||||
[
|
||||
Self::Base16EightiesDark,
|
||||
Self::Base16MochaDark,
|
||||
Self::Base16OceanDark,
|
||||
Self::Base16OceanLight,
|
||||
Self::InspiredGitHub,
|
||||
Self::SolarizedDark,
|
||||
Self::SolarizedLight,
|
||||
]
|
||||
.iter()
|
||||
.copied()
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Base16EightiesDark => "Base16 Eighties (dark)",
|
||||
Self::Base16MochaDark => "Base16 Mocha (dark)",
|
||||
Self::Base16OceanDark => "Base16 Ocean (dark)",
|
||||
Self::Base16OceanLight => "Base16 Ocean (light)",
|
||||
Self::InspiredGitHub => "InspiredGitHub (light)",
|
||||
Self::SolarizedDark => "Solarized (dark)",
|
||||
Self::SolarizedLight => "Solarized (light)",
|
||||
}
|
||||
}
|
||||
|
||||
fn syntect_key_name(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Base16EightiesDark => "base16-eighties.dark",
|
||||
Self::Base16MochaDark => "base16-mocha.dark",
|
||||
Self::Base16OceanDark => "base16-ocean.dark",
|
||||
Self::Base16OceanLight => "base16-ocean.light",
|
||||
Self::InspiredGitHub => "InspiredGitHub",
|
||||
Self::SolarizedDark => "Solarized (dark)",
|
||||
Self::SolarizedLight => "Solarized (light)",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_dark(&self) -> bool {
|
||||
match self {
|
||||
Self::Base16EightiesDark
|
||||
| Self::Base16MochaDark
|
||||
| Self::Base16OceanDark
|
||||
| Self::SolarizedDark => true,
|
||||
|
||||
Self::Base16OceanLight | Self::InspiredGitHub | Self::SolarizedLight => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Hash, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
pub struct CodeTheme {
|
||||
dark_mode: bool,
|
||||
|
||||
#[cfg(feature = "syntect")]
|
||||
syntect_theme: SyntectTheme,
|
||||
|
||||
#[cfg(not(feature = "syntect"))]
|
||||
formats: enum_map::EnumMap<TokenType, egui::TextFormat>,
|
||||
}
|
||||
|
||||
impl Default for CodeTheme {
|
||||
fn default() -> Self {
|
||||
Self::dark()
|
||||
}
|
||||
}
|
||||
|
||||
impl CodeTheme {
|
||||
pub fn from_style(style: &egui::Style) -> Self {
|
||||
if style.visuals.dark_mode {
|
||||
Self::dark()
|
||||
} else {
|
||||
Self::light()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_memory(ctx: &egui::Context) -> Self {
|
||||
if ctx.style().visuals.dark_mode {
|
||||
ctx.data_mut(|d| {
|
||||
d.get_persisted(egui::Id::new("dark"))
|
||||
.unwrap_or_else(CodeTheme::dark)
|
||||
})
|
||||
} else {
|
||||
ctx.data_mut(|d| {
|
||||
d.get_persisted(egui::Id::new("light"))
|
||||
.unwrap_or_else(CodeTheme::light)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn store_in_memory(self, ctx: &egui::Context) {
|
||||
if self.dark_mode {
|
||||
ctx.data_mut(|d| d.insert_persisted(egui::Id::new("dark"), self));
|
||||
} else {
|
||||
ctx.data_mut(|d| d.insert_persisted(egui::Id::new("light"), self));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "syntect")]
|
||||
impl CodeTheme {
|
||||
pub fn dark() -> Self {
|
||||
Self {
|
||||
dark_mode: true,
|
||||
syntect_theme: SyntectTheme::Base16MochaDark,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn light() -> Self {
|
||||
Self {
|
||||
dark_mode: false,
|
||||
syntect_theme: SyntectTheme::SolarizedLight,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
egui::widgets::global_dark_light_mode_buttons(ui);
|
||||
|
||||
for theme in SyntectTheme::all() {
|
||||
if theme.is_dark() == self.dark_mode {
|
||||
ui.radio_value(&mut self.syntect_theme, theme, theme.name());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "syntect"))]
|
||||
impl CodeTheme {
|
||||
pub fn dark() -> Self {
|
||||
let font_id = egui::FontId::monospace(10.0);
|
||||
use egui::{Color32, TextFormat};
|
||||
Self {
|
||||
dark_mode: true,
|
||||
formats: enum_map::enum_map![
|
||||
TokenType::Comment => TextFormat::simple(font_id.clone(), Color32::from_gray(120)),
|
||||
TokenType::Keyword => TextFormat::simple(font_id.clone(), Color32::from_rgb(255, 100, 100)),
|
||||
TokenType::Literal => TextFormat::simple(font_id.clone(), Color32::from_rgb(87, 165, 171)),
|
||||
TokenType::StringLiteral => TextFormat::simple(font_id.clone(), Color32::from_rgb(109, 147, 226)),
|
||||
TokenType::Punctuation => TextFormat::simple(font_id.clone(), Color32::LIGHT_GRAY),
|
||||
TokenType::Whitespace => TextFormat::simple(font_id.clone(), Color32::TRANSPARENT),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn light() -> Self {
|
||||
let font_id = egui::FontId::monospace(10.0);
|
||||
use egui::{Color32, TextFormat};
|
||||
Self {
|
||||
dark_mode: false,
|
||||
#[cfg(not(feature = "syntect"))]
|
||||
formats: enum_map::enum_map![
|
||||
TokenType::Comment => TextFormat::simple(font_id.clone(), Color32::GRAY),
|
||||
TokenType::Keyword => TextFormat::simple(font_id.clone(), Color32::from_rgb(235, 0, 0)),
|
||||
TokenType::Literal => TextFormat::simple(font_id.clone(), Color32::from_rgb(153, 134, 255)),
|
||||
TokenType::StringLiteral => TextFormat::simple(font_id.clone(), Color32::from_rgb(37, 203, 105)),
|
||||
TokenType::Punctuation => TextFormat::simple(font_id.clone(), Color32::DARK_GRAY),
|
||||
TokenType::Whitespace => TextFormat::simple(font_id.clone(), Color32::TRANSPARENT),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
ui.horizontal_top(|ui| {
|
||||
let selected_id = egui::Id::null();
|
||||
let mut selected_tt: TokenType =
|
||||
ui.data_mut(|d| *d.get_persisted_mut_or(selected_id, TokenType::Comment));
|
||||
|
||||
ui.vertical(|ui| {
|
||||
ui.set_width(150.0);
|
||||
egui::widgets::global_dark_light_mode_buttons(ui);
|
||||
|
||||
ui.add_space(8.0);
|
||||
ui.separator();
|
||||
ui.add_space(8.0);
|
||||
|
||||
ui.scope(|ui| {
|
||||
for (tt, tt_name) in [
|
||||
(TokenType::Comment, "// comment"),
|
||||
(TokenType::Keyword, "keyword"),
|
||||
(TokenType::Literal, "literal"),
|
||||
(TokenType::StringLiteral, "\"string literal\""),
|
||||
(TokenType::Punctuation, "punctuation ;"),
|
||||
// (TokenType::Whitespace, "whitespace"),
|
||||
] {
|
||||
let format = &mut self.formats[tt];
|
||||
ui.style_mut().override_font_id = Some(format.font_id.clone());
|
||||
ui.visuals_mut().override_text_color = Some(format.color);
|
||||
ui.radio_value(&mut selected_tt, tt, tt_name);
|
||||
}
|
||||
});
|
||||
|
||||
let reset_value = if self.dark_mode {
|
||||
CodeTheme::dark()
|
||||
} else {
|
||||
CodeTheme::light()
|
||||
};
|
||||
|
||||
if ui
|
||||
.add_enabled(*self != reset_value, egui::Button::new("Reset theme"))
|
||||
.clicked()
|
||||
{
|
||||
*self = reset_value;
|
||||
}
|
||||
});
|
||||
|
||||
ui.add_space(16.0);
|
||||
|
||||
ui.data_mut(|d| d.insert_persisted(selected_id, selected_tt));
|
||||
|
||||
egui::Frame::group(ui.style())
|
||||
.inner_margin(egui::Vec2::splat(2.0))
|
||||
.show(ui, |ui| {
|
||||
// ui.group(|ui| {
|
||||
ui.style_mut().override_text_style = Some(egui::TextStyle::Small);
|
||||
ui.spacing_mut().slider_width = 128.0; // Controls color picker size
|
||||
egui::widgets::color_picker::color_picker_color32(
|
||||
ui,
|
||||
&mut self.formats[selected_tt].color,
|
||||
egui::color_picker::Alpha::Opaque,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[cfg(feature = "syntect")]
|
||||
struct Highlighter {
|
||||
ps: syntect::parsing::SyntaxSet,
|
||||
ts: syntect::highlighting::ThemeSet,
|
||||
}
|
||||
|
||||
#[cfg(feature = "syntect")]
|
||||
impl Default for Highlighter {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
ps: syntect::parsing::SyntaxSet::load_defaults_newlines(),
|
||||
ts: syntect::highlighting::ThemeSet::load_defaults(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "syntect")]
|
||||
impl Highlighter {
|
||||
#[allow(clippy::unused_self, clippy::unnecessary_wraps)]
|
||||
fn highlight(&self, theme: &CodeTheme, code: &str, lang: &str) -> LayoutJob {
|
||||
self.highlight_impl(theme, code, lang).unwrap_or_else(|| {
|
||||
// Fallback:
|
||||
LayoutJob::simple(
|
||||
code.into(),
|
||||
egui::FontId::monospace(12.0),
|
||||
if theme.dark_mode {
|
||||
egui::Color32::LIGHT_GRAY
|
||||
} else {
|
||||
egui::Color32::DARK_GRAY
|
||||
},
|
||||
f32::INFINITY,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn highlight_impl(&self, theme: &CodeTheme, text: &str, language: &str) -> Option<LayoutJob> {
|
||||
use syntect::easy::HighlightLines;
|
||||
use syntect::highlighting::FontStyle;
|
||||
use syntect::util::LinesWithEndings;
|
||||
|
||||
let syntax = self
|
||||
.ps
|
||||
.find_syntax_by_name(language)
|
||||
.or_else(|| self.ps.find_syntax_by_extension(language))?;
|
||||
|
||||
let theme = theme.syntect_theme.syntect_key_name();
|
||||
let mut h = HighlightLines::new(syntax, &self.ts.themes[theme]);
|
||||
|
||||
use egui::text::{LayoutSection, TextFormat};
|
||||
|
||||
let mut job = LayoutJob {
|
||||
text: text.into(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
for line in LinesWithEndings::from(text) {
|
||||
for (style, range) in h.highlight_line(line, &self.ps).ok()? {
|
||||
let fg = style.foreground;
|
||||
let text_color = egui::Color32::from_rgb(fg.r, fg.g, fg.b);
|
||||
let italics = style.font_style.contains(FontStyle::ITALIC);
|
||||
let underline = style.font_style.contains(FontStyle::ITALIC);
|
||||
let underline = if underline {
|
||||
egui::Stroke::new(1.0, text_color)
|
||||
} else {
|
||||
egui::Stroke::NONE
|
||||
};
|
||||
job.sections.push(LayoutSection {
|
||||
leading_space: 0.0,
|
||||
byte_range: as_byte_range(text, range),
|
||||
format: TextFormat {
|
||||
font_id: egui::FontId::monospace(12.0),
|
||||
color: text_color,
|
||||
italics,
|
||||
underline,
|
||||
..Default::default()
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Some(job)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "syntect")]
|
||||
fn as_byte_range(whole: &str, range: &str) -> std::ops::Range<usize> {
|
||||
let whole_start = whole.as_ptr() as usize;
|
||||
let range_start = range.as_ptr() as usize;
|
||||
assert!(whole_start <= range_start);
|
||||
assert!(range_start + range.len() <= whole_start + whole.len());
|
||||
let offset = range_start - whole_start;
|
||||
offset..(offset + range.len())
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[cfg(not(feature = "syntect"))]
|
||||
#[derive(Default)]
|
||||
struct Highlighter {}
|
||||
|
||||
#[cfg(not(feature = "syntect"))]
|
||||
impl Highlighter {
|
||||
#[allow(clippy::unused_self, clippy::unnecessary_wraps)]
|
||||
fn highlight(&self, theme: &CodeTheme, mut text: &str, _language: &str) -> LayoutJob {
|
||||
// Extremely simple syntax highlighter for when we compile without syntect
|
||||
|
||||
let mut job = LayoutJob::default();
|
||||
|
||||
while !text.is_empty() {
|
||||
if text.starts_with("//") {
|
||||
let end = text.find('\n').unwrap_or(text.len());
|
||||
job.append(&text[..end], 0.0, theme.formats[TokenType::Comment].clone());
|
||||
text = &text[end..];
|
||||
} else if text.starts_with('"') {
|
||||
let end = text[1..]
|
||||
.find('"')
|
||||
.map(|i| i + 2)
|
||||
.or_else(|| text.find('\n'))
|
||||
.unwrap_or(text.len());
|
||||
job.append(
|
||||
&text[..end],
|
||||
0.0,
|
||||
theme.formats[TokenType::StringLiteral].clone(),
|
||||
);
|
||||
text = &text[end..];
|
||||
} else if text.starts_with(|c: char| c.is_ascii_alphanumeric()) {
|
||||
let end = text[1..]
|
||||
.find(|c: char| !c.is_ascii_alphanumeric())
|
||||
.map_or_else(|| text.len(), |i| i + 1);
|
||||
let word = &text[..end];
|
||||
let tt = if is_keyword(word) {
|
||||
TokenType::Keyword
|
||||
} else {
|
||||
TokenType::Literal
|
||||
};
|
||||
job.append(word, 0.0, theme.formats[tt].clone());
|
||||
text = &text[end..];
|
||||
} else if text.starts_with(|c: char| c.is_ascii_whitespace()) {
|
||||
let end = text[1..]
|
||||
.find(|c: char| !c.is_ascii_whitespace())
|
||||
.map_or_else(|| text.len(), |i| i + 1);
|
||||
job.append(
|
||||
&text[..end],
|
||||
0.0,
|
||||
theme.formats[TokenType::Whitespace].clone(),
|
||||
);
|
||||
text = &text[end..];
|
||||
} else {
|
||||
let mut it = text.char_indices();
|
||||
it.next();
|
||||
let end = it.next().map_or(text.len(), |(idx, _chr)| idx);
|
||||
job.append(
|
||||
&text[..end],
|
||||
0.0,
|
||||
theme.formats[TokenType::Punctuation].clone(),
|
||||
);
|
||||
text = &text[end..];
|
||||
}
|
||||
}
|
||||
|
||||
job
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "syntect"))]
|
||||
fn is_keyword(word: &str) -> bool {
|
||||
matches!(
|
||||
word,
|
||||
"as" | "async"
|
||||
| "await"
|
||||
| "break"
|
||||
| "const"
|
||||
| "continue"
|
||||
| "crate"
|
||||
| "dyn"
|
||||
| "else"
|
||||
| "enum"
|
||||
| "extern"
|
||||
| "false"
|
||||
| "fn"
|
||||
| "for"
|
||||
| "if"
|
||||
| "impl"
|
||||
| "in"
|
||||
| "let"
|
||||
| "loop"
|
||||
| "match"
|
||||
| "mod"
|
||||
| "move"
|
||||
| "mut"
|
||||
| "pub"
|
||||
| "ref"
|
||||
| "return"
|
||||
| "self"
|
||||
| "Self"
|
||||
| "static"
|
||||
| "struct"
|
||||
| "super"
|
||||
| "trait"
|
||||
| "true"
|
||||
| "type"
|
||||
| "unsafe"
|
||||
| "use"
|
||||
| "where"
|
||||
| "while"
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue