1
0
mirror of https://github.com/emilk/egui.git synced 2026-08-30 21:30:03 -04:00

Move code from egui_demo_lib to egui_demo_app (#1540)

Also clean up feature names and dependencies
This commit is contained in:
Emil Ernerfeldt
2022-04-28 11:23:34 +02:00
committed by GitHub
parent 39917bec26
commit 355d70d2b9
46 changed files with 213 additions and 174 deletions

View File

@@ -1,12 +0,0 @@
#[derive(Default)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "serde", serde(default))]
pub struct DemoApp {
demo_windows: super::DemoWindows,
}
impl epi::App for DemoApp {
fn update(&mut self, ctx: &egui::Context, _frame: &mut epi::Frame) {
self.demo_windows.ui(ctx);
}
}

View File

@@ -1,213 +0,0 @@
use egui::{containers::*, widgets::*, *};
use std::f32::consts::TAU;
#[derive(PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "serde", serde(default))]
pub struct FractalClock {
paused: bool,
time: f64,
zoom: f32,
start_line_width: f32,
depth: usize,
length_factor: f32,
luminance_factor: f32,
width_factor: f32,
line_count: usize,
}
impl Default for FractalClock {
fn default() -> Self {
Self {
paused: false,
time: 0.0,
zoom: 0.25,
start_line_width: 2.5,
depth: 9,
length_factor: 0.8,
luminance_factor: 0.8,
width_factor: 0.9,
line_count: 0,
}
}
}
impl epi::App for FractalClock {
fn update(&mut self, ctx: &egui::Context, _frame: &mut epi::Frame) {
egui::CentralPanel::default()
.frame(Frame::dark_canvas(&ctx.style()))
.show(ctx, |ui| self.ui(ui, crate::seconds_since_midnight()));
}
}
impl FractalClock {
pub fn ui(&mut self, ui: &mut Ui, seconds_since_midnight: Option<f64>) {
if !self.paused {
self.time = seconds_since_midnight.unwrap_or_else(|| ui.input().time);
ui.ctx().request_repaint();
}
let painter = Painter::new(
ui.ctx().clone(),
ui.layer_id(),
ui.available_rect_before_wrap(),
);
self.paint(&painter);
// Make sure we allocate what we used (everything)
ui.expand_to_include_rect(painter.clip_rect());
Frame::popup(ui.style())
.stroke(Stroke::none())
.show(ui, |ui| {
ui.set_max_width(270.0);
CollapsingHeader::new("Settings")
.show(ui, |ui| self.options_ui(ui, seconds_since_midnight));
});
}
fn options_ui(&mut self, ui: &mut Ui, seconds_since_midnight: Option<f64>) {
if seconds_since_midnight.is_some() {
ui.label(format!(
"Local time: {:02}:{:02}:{:02}.{:03}",
(self.time % (24.0 * 60.0 * 60.0) / 3600.0).floor(),
(self.time % (60.0 * 60.0) / 60.0).floor(),
(self.time % 60.0).floor(),
(self.time % 1.0 * 100.0).floor()
));
} else {
ui.label("The fractal_clock clock is not showing the correct time");
};
ui.label(format!("Painted line count: {}", self.line_count));
ui.checkbox(&mut self.paused, "Paused");
ui.add(Slider::new(&mut self.zoom, 0.0..=1.0).text("zoom"));
ui.add(Slider::new(&mut self.start_line_width, 0.0..=5.0).text("Start line width"));
ui.add(Slider::new(&mut self.depth, 0..=14).text("depth"));
ui.add(Slider::new(&mut self.length_factor, 0.0..=1.0).text("length factor"));
ui.add(Slider::new(&mut self.luminance_factor, 0.0..=1.0).text("luminance factor"));
ui.add(Slider::new(&mut self.width_factor, 0.0..=1.0).text("width factor"));
egui::reset_button(ui, self);
ui.hyperlink_to(
"Inspired by a screensaver by Rob Mayoff",
"http://www.dqd.com/~mayoff/programs/FractalClock/",
);
ui.add(crate::egui_github_link_file!());
}
fn paint(&mut self, painter: &Painter) {
struct Hand {
length: f32,
angle: f32,
vec: Vec2,
}
impl Hand {
fn from_length_angle(length: f32, angle: f32) -> Self {
Self {
length,
angle,
vec: length * Vec2::angled(angle),
}
}
}
let angle_from_period =
|period| TAU * (self.time.rem_euclid(period) / period) as f32 - TAU / 4.0;
let hands = [
// Second hand:
Hand::from_length_angle(self.length_factor, angle_from_period(60.0)),
// Minute hand:
Hand::from_length_angle(self.length_factor, angle_from_period(60.0 * 60.0)),
// Hour hand:
Hand::from_length_angle(0.5, angle_from_period(12.0 * 60.0 * 60.0)),
];
let mut shapes: Vec<Shape> = Vec::new();
let rect = painter.clip_rect();
let to_screen = emath::RectTransform::from_to(
Rect::from_center_size(Pos2::ZERO, rect.square_proportions() / self.zoom),
rect,
);
let mut paint_line = |points: [Pos2; 2], color: Color32, width: f32| {
let line = [to_screen * points[0], to_screen * points[1]];
// culling
if rect.intersects(Rect::from_two_pos(line[0], line[1])) {
shapes.push(Shape::line_segment(line, (width, color)));
}
};
let hand_rotations = [
hands[0].angle - hands[2].angle + TAU / 2.0,
hands[1].angle - hands[2].angle + TAU / 2.0,
];
let hand_rotors = [
hands[0].length * emath::Rot2::from_angle(hand_rotations[0]),
hands[1].length * emath::Rot2::from_angle(hand_rotations[1]),
];
#[derive(Clone, Copy)]
struct Node {
pos: Pos2,
dir: Vec2,
}
let mut nodes = Vec::new();
let mut width = self.start_line_width;
for (i, hand) in hands.iter().enumerate() {
let center = pos2(0.0, 0.0);
let end = center + hand.vec;
paint_line([center, end], Color32::from_additive_luminance(255), width);
if i < 2 {
nodes.push(Node {
pos: end,
dir: hand.vec,
});
}
}
let mut luminance = 0.7; // Start dimmer than main hands
let mut new_nodes = Vec::new();
for _ in 0..self.depth {
new_nodes.clear();
new_nodes.reserve(nodes.len() * 2);
luminance *= self.luminance_factor;
width *= self.width_factor;
let luminance_u8 = (255.0 * luminance).round() as u8;
if luminance_u8 == 0 {
break;
}
for &rotor in &hand_rotors {
for a in &nodes {
let new_dir = rotor * a.dir;
let b = Node {
pos: a.pos + new_dir,
dir: new_dir,
};
paint_line(
[a.pos, b.pos],
Color32::from_additive_luminance(luminance_u8),
width,
);
new_nodes.push(b);
}
}
std::mem::swap(&mut nodes, &mut new_nodes);
}
self.line_count = shapes.len();
painter.extend(shapes);
}
}

View File

@@ -1,266 +0,0 @@
use egui_extras::RetainedImage;
use poll_promise::Promise;
struct Resource {
/// HTTP response
response: ehttp::Response,
text: Option<String>,
/// If set, the response was an image.
image: Option<RetainedImage>,
/// If set, the response was text with some supported syntax highlighting (e.g. ".rs" or ".md").
colored_text: Option<ColoredText>,
}
impl Resource {
fn from_response(ctx: &egui::Context, response: ehttp::Response) -> Self {
let content_type = response.content_type().unwrap_or_default();
let image = if content_type.starts_with("image/") {
RetainedImage::from_image_bytes(&response.url, &response.bytes).ok()
} else {
None
};
let text = response.text();
let colored_text = text.and_then(|text| syntax_highlighting(ctx, &response, text));
let text = text.map(|text| text.to_owned());
Self {
response,
text,
image,
colored_text,
}
}
}
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct HttpApp {
url: String,
#[cfg_attr(feature = "serde", serde(skip))]
promise: Option<Promise<ehttp::Result<Resource>>>,
}
impl Default for HttpApp {
fn default() -> Self {
Self {
url: "https://raw.githubusercontent.com/emilk/egui/master/README.md".to_owned(),
promise: Default::default(),
}
}
}
impl epi::App for HttpApp {
fn update(&mut self, ctx: &egui::Context, frame: &mut epi::Frame) {
egui::TopBottomPanel::bottom("http_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| {
let trigger_fetch = ui_url(ui, frame, &mut self.url);
ui.horizontal_wrapped(|ui| {
ui.spacing_mut().item_spacing.x = 0.0;
ui.label("HTTP requests made using ");
ui.hyperlink_to("ehttp", "https://www.github.com/emilk/ehttp");
ui.label(".");
});
if trigger_fetch {
let ctx = ctx.clone();
let (sender, promise) = Promise::new();
let request = ehttp::Request::get(&self.url);
ehttp::fetch(request, move |response| {
ctx.request_repaint(); // wake up UI thread
let resource = response.map(|response| Resource::from_response(&ctx, response));
sender.send(resource);
});
self.promise = Some(promise);
}
ui.separator();
if let Some(promise) = &self.promise {
if let Some(result) = promise.ready() {
match result {
Ok(resource) => {
ui_resource(ui, resource);
}
Err(error) => {
// This should only happen if the fetch API isn't available or something similar.
ui.colored_label(
egui::Color32::RED,
if error.is_empty() { "Error" } else { error },
);
}
}
} else {
ui.spinner();
}
}
});
}
}
fn ui_url(ui: &mut egui::Ui, frame: &mut epi::Frame, url: &mut String) -> bool {
let mut trigger_fetch = false;
ui.horizontal(|ui| {
ui.label("URL:");
trigger_fetch |= ui
.add(egui::TextEdit::singleline(url).desired_width(f32::INFINITY))
.lost_focus();
});
if frame.is_web() {
ui.label("HINT: paste the url of this page into the field above!");
}
ui.horizontal(|ui| {
if ui.button("Source code for this example").clicked() {
*url = format!(
"https://raw.githubusercontent.com/emilk/egui/master/{}",
file!()
);
trigger_fetch = true;
}
if ui.button("Random image").clicked() {
let seed = ui.input().time;
let side = 640;
*url = format!("https://picsum.photos/seed/{}/{}", seed, side);
trigger_fetch = true;
}
});
trigger_fetch
}
fn ui_resource(ui: &mut egui::Ui, resource: &Resource) {
let Resource {
response,
text,
image,
colored_text,
} = resource;
ui.monospace(format!("url: {}", response.url));
ui.monospace(format!(
"status: {} ({})",
response.status, response.status_text
));
ui.monospace(format!(
"content-type: {}",
response.content_type().unwrap_or_default()
));
ui.monospace(format!(
"size: {:.1} kB",
response.bytes.len() as f32 / 1000.0
));
ui.separator();
egui::ScrollArea::vertical()
.auto_shrink([false; 2])
.show(ui, |ui| {
egui::CollapsingHeader::new("Response headers")
.default_open(false)
.show(ui, |ui| {
egui::Grid::new("response_headers")
.spacing(egui::vec2(ui.spacing().item_spacing.x * 2.0, 0.0))
.show(ui, |ui| {
for header in &response.headers {
ui.label(header.0);
ui.label(header.1);
ui.end_row();
}
})
});
ui.separator();
if let Some(text) = &text {
let tooltip = "Click to copy the response body";
if ui.button("📋").on_hover_text(tooltip).clicked() {
ui.output().copied_text = text.clone();
}
ui.separator();
}
if let Some(image) = image {
let mut size = image.size_vec2();
size *= (ui.available_width() / size.x).min(1.0);
image.show_size(ui, size);
} else if let Some(colored_text) = colored_text {
colored_text.ui(ui);
} else if let Some(text) = &text {
selectable_text(ui, text);
} else {
ui.monospace("[binary]");
}
});
}
fn selectable_text(ui: &mut egui::Ui, mut text: &str) {
ui.add(
egui::TextEdit::multiline(&mut text)
.desired_width(f32::INFINITY)
.font(egui::TextStyle::Monospace),
);
}
// ----------------------------------------------------------------------------
// Syntax highlighting:
#[cfg(feature = "syntect")]
fn syntax_highlighting(
ctx: &egui::Context,
response: &ehttp::Response,
text: &str,
) -> Option<ColoredText> {
let extension_and_rest: Vec<&str> = response.url.rsplitn(2, '.').collect();
let extension = extension_and_rest.get(0)?;
let theme = crate::syntax_highlighting::CodeTheme::from_style(&ctx.style());
Some(ColoredText(crate::syntax_highlighting::highlight(
ctx, &theme, text, extension,
)))
}
#[cfg(not(feature = "syntect"))]
fn syntax_highlighting(_ctx: &egui::Context, _: &ehttp::Response, _: &str) -> Option<ColoredText> {
None
}
struct ColoredText(egui::text::LayoutJob);
impl ColoredText {
pub fn ui(&self, ui: &mut egui::Ui) {
if true {
// Selectable text:
let mut layouter = |ui: &egui::Ui, _string: &str, wrap_width: f32| {
let mut layout_job = self.0.clone();
layout_job.wrap.max_width = wrap_width;
ui.fonts().layout_job(layout_job)
};
let mut text = self.0.text.as_str();
ui.add(
egui::TextEdit::multiline(&mut text)
.font(egui::TextStyle::Monospace)
.desired_width(f32::INFINITY)
.layouter(&mut layouter),
);
} else {
let mut job = self.0.clone();
job.wrap.max_width = ui.available_width();
let galley = ui.fonts().layout_job(job);
let (response, painter) = ui.allocate_painter(galley.size(), egui::Sense::hover());
painter.add(egui::Shape::galley(response.rect.min, galley));
}
}
}

View File

@@ -1,13 +0,0 @@
mod color_test;
mod demo;
mod fractal_clock;
#[cfg(feature = "http")]
mod http_app;
pub use color_test::ColorTest;
pub use demo::DemoApp;
pub use fractal_clock::FractalClock;
#[cfg(feature = "http")]
pub use http_app::HttpApp;
pub use demo::DemoWindows; // used for tests

View File

@@ -1,361 +0,0 @@
/// How often we repaint the demo app by default
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum RunMode {
/// This is the default for the demo.
///
/// If this is selected, egui is only updated if are input events
/// (like mouse movements) or there are some animations in the GUI.
///
/// Reactive mode saves CPU.
///
/// The downside is that the UI can become out-of-date if something it is supposed to monitor changes.
/// For instance, a GUI for a thermostat need to repaint each time the temperature changes.
/// To ensure the UI is up to date you need to call `egui::Context::request_repaint()` each
/// time such an event happens. You can also chose to call `request_repaint()` once every second
/// or after every single frame - this is called [`Continuous`](RunMode::Continuous) mode,
/// and for games and interactive tools that need repainting every frame anyway, this should be the default.
Reactive,
/// This will call `egui::Context::request_repaint()` at the end of each frame
/// to request the backend to repaint as soon as possible.
///
/// On most platforms this will mean that egui will run at the display refresh rate of e.g. 60 Hz.
///
/// For this demo it is not any reason to do so except to
/// demonstrate how quickly egui runs.
///
/// For games or other interactive apps, this is probably what you want to do.
/// It will guarantee that egui is always up-to-date.
Continuous,
}
/// Default for demo is Reactive since
/// 1) We want to use minimal CPU
/// 2) There are no external events that could invalidate the UI
/// so there are no events to miss.
impl Default for RunMode {
fn default() -> Self {
RunMode::Reactive
}
}
// ----------------------------------------------------------------------------
#[derive(Default)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "serde", serde(default))]
pub struct BackendPanel {
pub open: bool,
#[cfg_attr(feature = "serde", serde(skip))]
// go back to [`Reactive`] mode each time we start
run_mode: RunMode,
/// current slider value for current gui scale
#[cfg_attr(feature = "serde", serde(skip))]
pixels_per_point: Option<f32>,
#[cfg_attr(feature = "serde", serde(skip))]
frame_history: crate::frame_history::FrameHistory,
egui_windows: EguiWindows,
}
impl BackendPanel {
pub fn update(&mut self, ctx: &egui::Context, frame: &mut epi::Frame) {
self.frame_history
.on_new_frame(ctx.input().time, frame.info().cpu_usage);
if self.run_mode == RunMode::Continuous {
// Tell the backend to repaint as soon as possible
ctx.request_repaint();
}
}
pub fn end_of_frame(&mut self, ctx: &egui::Context) {
self.egui_windows.windows(ctx);
}
pub fn ui(&mut self, ui: &mut egui::Ui, frame: &mut epi::Frame) {
egui::trace!(ui);
ui.vertical_centered(|ui| {
ui.heading("💻 Backend");
});
ui.separator();
self.integration_ui(ui, frame);
ui.separator();
self.run_mode_ui(ui);
ui.separator();
self.frame_history.ui(ui);
ui.separator();
ui.label("egui windows:");
self.egui_windows.checkboxes(ui);
ui.separator();
{
let mut debug_on_hover = ui.ctx().debug_on_hover();
ui.checkbox(&mut debug_on_hover, "🐛 Debug on hover")
.on_hover_text("Show structure of the ui when you hover with the mouse");
ui.ctx().set_debug_on_hover(debug_on_hover);
}
ui.separator();
{
let mut screen_reader = ui.ctx().options().screen_reader;
ui.checkbox(&mut screen_reader, "🔈 Screen reader").on_hover_text("Experimental feature: checking this will turn on the screen reader on supported platforms");
ui.ctx().options().screen_reader = screen_reader;
}
if !frame.is_web() {
ui.separator();
if ui.button("Quit").clicked() {
frame.quit();
}
if ui
.button("Drag me to drag window")
.is_pointer_button_down_on()
{
frame.drag_window();
}
}
}
fn integration_ui(&mut self, ui: &mut egui::Ui, frame: &mut epi::Frame) {
if frame.is_web() {
ui.label("egui is an immediate mode GUI written in Rust, compiled to WebAssembly, rendered with WebGL.");
ui.label(
"Everything you see is rendered as textured triangles. There is no DOM and no HTML elements. \
This is the web page, reinvented with game tech.");
ui.hyperlink("https://github.com/emilk/egui");
ui.separator();
}
show_integration_name(ui, &frame.info());
if let Some(web_info) = &frame.info().web_info {
ui.collapsing("Web info (location)", |ui| {
ui.monospace(format!("{:#?}", web_info.location));
});
}
// For instance: `egui_web` sets `pixels_per_point` every frame to force
// egui to use the same scale as the web zoom factor.
let integration_controls_pixels_per_point = ui.input().raw.pixels_per_point.is_some();
if !integration_controls_pixels_per_point {
if let Some(new_pixels_per_point) = self.pixels_per_point_ui(ui, &frame.info()) {
ui.ctx().set_pixels_per_point(new_pixels_per_point);
}
}
if !frame.is_web()
&& ui
.button("📱 Phone Size")
.on_hover_text("Resize the window to be small like a phone.")
.clicked()
{
frame.set_window_size(egui::Vec2::new(375.0, 812.0)); // iPhone 12 mini
}
}
fn pixels_per_point_ui(
&mut self,
ui: &mut egui::Ui,
info: &epi::IntegrationInfo,
) -> Option<f32> {
let pixels_per_point = self.pixels_per_point.get_or_insert_with(|| {
info.native_pixels_per_point
.unwrap_or_else(|| ui.ctx().pixels_per_point())
});
ui.horizontal(|ui| {
ui.spacing_mut().slider_width = 90.0;
ui.add(
egui::Slider::new(pixels_per_point, 0.5..=5.0)
.logarithmic(true)
.clamp_to_range(true)
.text("Scale"),
)
.on_hover_text("Physical pixels per point.");
if let Some(native_pixels_per_point) = info.native_pixels_per_point {
let enabled = *pixels_per_point != native_pixels_per_point;
if ui
.add_enabled(enabled, egui::Button::new("Reset"))
.on_hover_text(format!(
"Reset scale to native value ({:.1})",
native_pixels_per_point
))
.clicked()
{
*pixels_per_point = native_pixels_per_point;
}
}
});
// We wait until mouse release to activate:
if ui.ctx().is_using_pointer() {
None
} else {
Some(*pixels_per_point)
}
}
fn run_mode_ui(&mut self, ui: &mut egui::Ui) {
ui.horizontal(|ui| {
let run_mode = &mut self.run_mode;
ui.label("Mode:");
ui.radio_value(run_mode, RunMode::Reactive, "Reactive")
.on_hover_text("Repaint when there are animations or input (e.g. mouse movement)");
ui.radio_value(run_mode, RunMode::Continuous, "Continuous")
.on_hover_text("Repaint everything each frame");
});
if self.run_mode == RunMode::Continuous {
ui.label(format!(
"Repainting the UI each frame. FPS: {:.1}",
self.frame_history.fps()
));
} else {
ui.label("Only running UI code when there are animations or input");
}
}
}
// ----------------------------------------------------------------------------
fn show_integration_name(ui: &mut egui::Ui, integration_info: &epi::IntegrationInfo) {
let name = integration_info.name;
ui.horizontal(|ui| {
ui.spacing_mut().item_spacing.x = 0.0;
ui.label("Integration: ");
match name {
"egui_glium" | "egui_glow" | "egui_web" => {
ui.hyperlink_to(
name,
format!("https://github.com/emilk/egui/tree/master/{}", name),
);
}
name => {
ui.label(name);
}
}
});
}
// ----------------------------------------------------------------------------
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
struct EguiWindows {
// egui stuff:
settings: bool,
inspection: bool,
memory: bool,
output_events: bool,
#[cfg_attr(feature = "serde", serde(skip))]
output_event_history: std::collections::VecDeque<egui::output::OutputEvent>,
}
impl Default for EguiWindows {
fn default() -> Self {
EguiWindows::none()
}
}
impl EguiWindows {
fn none() -> Self {
Self {
settings: false,
inspection: false,
memory: false,
output_events: false,
output_event_history: Default::default(),
}
}
fn checkboxes(&mut self, ui: &mut egui::Ui) {
let Self {
settings,
inspection,
memory,
output_events,
output_event_history: _,
} = self;
ui.checkbox(settings, "🔧 Settings");
ui.checkbox(inspection, "🔍 Inspection");
ui.checkbox(memory, "📝 Memory");
ui.checkbox(output_events, "📤 Output Events");
}
fn windows(&mut self, ctx: &egui::Context) {
let Self {
settings,
inspection,
memory,
output_events,
output_event_history,
} = self;
for event in &ctx.output().events {
output_event_history.push_back(event.clone());
}
while output_event_history.len() > 1000 {
output_event_history.pop_front();
}
egui::Window::new("🔧 Settings")
.open(settings)
.vscroll(true)
.show(ctx, |ui| {
ctx.settings_ui(ui);
});
egui::Window::new("🔍 Inspection")
.open(inspection)
.vscroll(true)
.show(ctx, |ui| {
ctx.inspection_ui(ui);
});
egui::Window::new("📝 Memory")
.open(memory)
.resizable(false)
.show(ctx, |ui| {
ctx.memory_ui(ui);
});
egui::Window::new("📤 Output Events")
.open(output_events)
.resizable(true)
.default_width(520.0)
.show(ctx, |ui| {
ui.label(
"Recent output events from egui. \
These are emitted when you interact with widgets, or move focus between them with TAB. \
They can be hooked up to a screen reader on supported platforms.",
);
ui.separator();
egui::ScrollArea::vertical()
.stick_to_bottom()
.show(ui, |ui| {
for event in output_event_history {
ui.label(format!("{:?}", event));
}
});
});
}
}

View File

@@ -9,6 +9,7 @@ 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))]
@@ -29,22 +30,6 @@ impl Default for ColorTest {
}
}
impl epi::App for ColorTest {
fn update(&mut self, ctx: &egui::Context, frame: &mut epi::Frame) {
egui::CentralPanel::default().show(ctx, |ui| {
if frame.is_web() {
ui.label(
"NOTE: Some old browsers stuck on WebGL1 without sRGB support will not pass the color test.",
);
ui.separator();
}
ScrollArea::both().auto_shrink([false; 2]).show(ui, |ui| {
self.ui(ui);
});
});
}
}
impl ColorTest {
pub fn ui(&mut self, ui: &mut Ui) {
ui.set_max_width(680.0);

View File

@@ -1,5 +1,5 @@
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
enum Plot {
Sin,
Bell,
@@ -15,7 +15,7 @@ fn sigmoid(x: f64) -> f64 {
}
#[derive(Clone, PartialEq)]
#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct ContextMenus {
plot: Plot,
show_axes: [bool; 2],

View File

@@ -76,7 +76,7 @@ pub fn drop_target<R>(
InnerResponse::new(ret, response)
}
#[derive(Clone, PartialEq)]
#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct DragAndDropDemo {
/// columns with items
columns: Vec<Vec<String>>,

View File

@@ -4,7 +4,6 @@
// ----------------------------------------------------------------------------
mod app;
pub mod code_editor;
pub mod code_example;
pub mod context_menu;
@@ -31,8 +30,7 @@ pub mod window_options;
pub mod window_with_panels;
pub use {
app::DemoApp, demo_app_windows::DemoWindows, misc_demo_window::MiscDemoWindow,
widget_gallery::WidgetGallery,
demo_app_windows::DemoWindows, misc_demo_window::MiscDemoWindow, widget_gallery::WidgetGallery,
};
// ----------------------------------------------------------------------------

View File

@@ -18,7 +18,7 @@ pub struct WidgetGallery {
color: egui::Color32,
animate_progress_bar: bool,
#[cfg(feature = "datetime")]
#[cfg(feature = "chrono")]
#[cfg_attr(feature = "serde", serde(skip))]
date: Option<chrono::Date<chrono::Utc>>,
@@ -37,7 +37,7 @@ impl Default for WidgetGallery {
string: Default::default(),
color: egui::Color32::LIGHT_BLUE.linear_multiply(0.5),
animate_progress_bar: false,
#[cfg(feature = "datetime")]
#[cfg(feature = "chrono")]
date: None,
texture: None,
}
@@ -109,7 +109,7 @@ impl WidgetGallery {
string,
color,
animate_progress_bar,
#[cfg(feature = "datetime")]
#[cfg(feature = "chrono")]
date,
texture,
} = self;
@@ -216,7 +216,7 @@ impl WidgetGallery {
}
ui.end_row();
#[cfg(feature = "datetime")]
#[cfg(feature = "chrono")]
{
let date = date.get_or_insert_with(|| chrono::offset::Utc::now().date());
ui.add(doc_link_label("DatePickerButton", "DatePickerButton"));

View File

@@ -29,8 +29,8 @@ impl Default for EasyMarkEditor {
}
}
impl epi::App for EasyMarkEditor {
fn update(&mut self, ctx: &egui::Context, _frame: &mut epi::Frame) {
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| {
@@ -42,10 +42,8 @@ impl epi::App for EasyMarkEditor {
self.ui(ui);
});
}
}
impl EasyMarkEditor {
fn ui(&mut self, ui: &mut egui::Ui) {
pub fn ui(&mut self, ui: &mut egui::Ui) {
egui::Grid::new("controls").show(ui, |ui| {
ui.checkbox(&mut self.highlight_editor, "Highlight editor");
egui::reset_button(ui, self);

View File

@@ -1,128 +0,0 @@
use egui::util::History;
pub struct FrameHistory {
frame_times: History<f32>,
}
impl Default for FrameHistory {
fn default() -> Self {
let max_age: f32 = 1.0;
let max_len = (max_age * 300.0).round() as usize;
Self {
frame_times: History::new(0..max_len, max_age),
}
}
}
impl FrameHistory {
// Called first
pub fn on_new_frame(&mut self, now: f64, previous_frame_time: Option<f32>) {
let previous_frame_time = previous_frame_time.unwrap_or_default();
if let Some(latest) = self.frame_times.latest_mut() {
*latest = previous_frame_time; // rewrite history now that we know
}
self.frame_times.add(now, previous_frame_time); // projected
}
pub fn mean_frame_time(&self) -> f32 {
self.frame_times.average().unwrap_or_default()
}
pub fn fps(&self) -> f32 {
1.0 / self.frame_times.mean_time_interval().unwrap_or_default()
}
pub fn ui(&mut self, ui: &mut egui::Ui) {
ui.label(format!(
"Total frames painted: {}",
self.frame_times.total_count()
))
.on_hover_text("Includes this frame.");
ui.label(format!(
"Mean CPU usage: {:.2} ms / frame",
1e3 * self.mean_frame_time()
))
.on_hover_text(
"Includes egui layout and tessellation time.\n\
Does not include GPU usage, nor overhead for sending data to GPU.",
);
egui::warn_if_debug_build(ui);
egui::CollapsingHeader::new("📊 CPU usage history")
.default_open(false)
.show(ui, |ui| {
self.graph(ui);
});
}
fn graph(&mut self, ui: &mut egui::Ui) -> egui::Response {
use egui::*;
ui.label("egui CPU usage history");
let history = &self.frame_times;
// TODO: we should not use `slider_width` as default graph width.
let height = ui.spacing().slider_width;
let size = vec2(ui.available_size_before_wrap().x, height);
let (rect, response) = ui.allocate_at_least(size, Sense::hover());
let style = ui.style().noninteractive();
let graph_top_cpu_usage = 0.010;
let graph_rect = Rect::from_x_y_ranges(history.max_age()..=0.0, graph_top_cpu_usage..=0.0);
let to_screen = emath::RectTransform::from_to(graph_rect, rect);
let mut shapes = Vec::with_capacity(3 + 2 * history.len());
shapes.push(Shape::Rect(epaint::RectShape {
rect,
rounding: style.rounding,
fill: ui.visuals().extreme_bg_color,
stroke: ui.style().noninteractive().bg_stroke,
}));
let rect = rect.shrink(4.0);
let color = ui.visuals().text_color();
let line_stroke = Stroke::new(1.0, color);
if let Some(pointer_pos) = response.hover_pos() {
let y = pointer_pos.y;
shapes.push(Shape::line_segment(
[pos2(rect.left(), y), pos2(rect.right(), y)],
line_stroke,
));
let cpu_usage = to_screen.inverse().transform_pos(pointer_pos).y;
let text = format!("{:.1} ms", 1e3 * cpu_usage);
shapes.push(Shape::text(
&*ui.fonts(),
pos2(rect.left(), y),
egui::Align2::LEFT_BOTTOM,
text,
TextStyle::Monospace.resolve(ui.style()),
color,
));
}
let circle_color = color;
let radius = 2.0;
let right_side_time = ui.input().time; // Time at right side of screen
for (time, cpu_usage) in history.iter() {
let age = (right_side_time - time) as f32;
let pos = to_screen.transform_pos_clamped(Pos2::new(age, cpu_usage));
shapes.push(Shape::line_segment(
[pos2(pos.x, rect.bottom()), pos],
line_stroke,
));
if cpu_usage < graph_top_cpu_usage {
shapes.push(Shape::circle_filled(pos, radius, circle_color));
}
}
ui.painter().extend(shapes);
response
}
}

View File

@@ -1,27 +1,27 @@
//! Demo-code for showing how egui is used.
//!
//! The demo-code is also used in benchmarks and tests.
//! 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.
#![allow(clippy::float_cmp)]
#![allow(clippy::manual_range_contains)]
mod apps;
mod backend_panel;
mod color_test;
mod demo;
pub mod easy_mark;
pub(crate) mod frame_history;
pub mod syntax_highlighting;
mod wrap_app;
pub use apps::ColorTest; // used for tests
pub use apps::DemoWindows; // used for tests
pub use wrap_app::WrapApp;
pub use color_test::ColorTest;
pub use demo::DemoWindows;
// ----------------------------------------------------------------------------
/// Create a [`Hyperlink`](crate::Hyperlink) to this egui source code file on github.
/// 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)")
$crate::egui_github_link_file!("(source code)")
};
($label: expr) => {
egui::github_link_file!(
@@ -30,12 +30,12 @@ macro_rules! egui_github_link_file {
)
};
}
pub(crate) use egui_github_link_file;
/// Create a [`Hyperlink`](crate::Hyperlink) to this egui source code file and line on github.
/// 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)")
$crate::egui_github_link_file_line!("(source code)")
};
($label: expr) => {
egui::github_link_file_line!(
@@ -44,7 +44,6 @@ macro_rules! egui_github_link_file_line {
)
};
}
pub(crate) use egui_github_link_file_line;
// ----------------------------------------------------------------------------
@@ -93,19 +92,3 @@ fn test_egui_zero_window_size() {
);
}
}
// ----------------------------------------------------------------------------
/// Time of day as seconds since midnight. Used for clock in demo app.
pub(crate) fn seconds_since_midnight() -> Option<f64> {
#[cfg(feature = "datetime")]
{
use chrono::Timelike;
let time = chrono::Local::now().time();
let seconds_since_midnight =
time.num_seconds_from_midnight() as f64 + 1e-9 * (time.nanosecond() as f64);
Some(seconds_since_midnight)
}
#[cfg(not(feature = "datetime"))]
None
}

View File

@@ -1,245 +0,0 @@
/// All the different demo apps.
#[derive(Default)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "serde", serde(default))]
pub struct Apps {
demo: crate::apps::DemoApp,
easy_mark_editor: crate::easy_mark::EasyMarkEditor,
#[cfg(feature = "http")]
http: crate::apps::HttpApp,
clock: crate::apps::FractalClock,
color_test: crate::apps::ColorTest,
}
impl Apps {
fn iter_mut(&mut self) -> impl Iterator<Item = (&str, &str, &mut dyn epi::App)> {
vec![
("✨ Demos", "demo", &mut self.demo as &mut dyn epi::App),
(
"🖹 EasyMark editor",
"easymark",
&mut self.easy_mark_editor as &mut dyn epi::App,
),
#[cfg(feature = "http")]
("⬇ HTTP", "http", &mut self.http as &mut dyn epi::App),
(
"🕑 Fractal Clock",
"clock",
&mut self.clock as &mut dyn epi::App,
),
(
"🎨 Color test",
"colors",
&mut self.color_test as &mut dyn epi::App,
),
]
.into_iter()
}
}
/// Wraps many demo/test apps into one.
#[derive(Default)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "serde", serde(default))]
pub struct WrapApp {
selected_anchor: String,
apps: Apps,
backend_panel: super::backend_panel::BackendPanel,
#[cfg_attr(feature = "serde", serde(skip))]
dropped_files: Vec<egui::DroppedFile>,
}
impl WrapApp {
pub fn new(_cc: &epi::CreationContext<'_>) -> Self {
#[cfg(feature = "persistence")]
if let Some(storage) = _cc.storage {
return epi::get_value(storage, epi::APP_KEY).unwrap_or_default();
}
Self::default()
}
}
impl epi::App for WrapApp {
#[cfg(feature = "persistence")]
fn save(&mut self, storage: &mut dyn epi::Storage) {
epi::set_value(storage, epi::APP_KEY, self);
}
fn clear_color(&self) -> egui::Rgba {
egui::Rgba::TRANSPARENT // we set a [`CentralPanel`] fill color in `demo_windows.rs`
}
fn update(&mut self, ctx: &egui::Context, frame: &mut epi::Frame) {
if let Some(web_info) = frame.info().web_info.as_ref() {
if let Some(anchor) = web_info.location.hash.strip_prefix('#') {
self.selected_anchor = anchor.to_owned();
}
}
if self.selected_anchor.is_empty() {
self.selected_anchor = self.apps.iter_mut().next().unwrap().0.to_owned();
}
egui::TopBottomPanel::top("wrap_app_top_bar").show(ctx, |ui| {
egui::trace!(ui);
self.bar_contents(ui, frame);
});
self.backend_panel.update(ctx, frame);
if self.backend_panel.open || ctx.memory().everything_is_visible() {
egui::SidePanel::left("backend_panel").show(ctx, |ui| {
self.backend_panel.ui(ui, frame);
ui.separator();
ui.horizontal(|ui| {
if ui
.button("Reset egui")
.on_hover_text("Forget scroll, positions, sizes etc")
.clicked()
{
*ui.ctx().memory() = Default::default();
}
if ui.button("Reset everything").clicked() {
*self = Default::default();
*ui.ctx().memory() = Default::default();
}
});
});
}
let mut found_anchor = false;
for (_name, anchor, app) in self.apps.iter_mut() {
if anchor == self.selected_anchor || ctx.memory().everything_is_visible() {
app.update(ctx, frame);
found_anchor = true;
}
}
if !found_anchor {
self.selected_anchor = "demo".into();
}
self.backend_panel.end_of_frame(ctx);
self.ui_file_drag_and_drop(ctx);
}
}
impl WrapApp {
fn bar_contents(&mut self, ui: &mut egui::Ui, frame: &mut epi::Frame) {
// A menu-bar is a horizontal layout with some special styles applied.
// egui::menu::bar(ui, |ui| {
ui.horizontal_wrapped(|ui| {
egui::widgets::global_dark_light_mode_switch(ui);
ui.checkbox(&mut self.backend_panel.open, "💻 Backend");
ui.separator();
for (name, anchor, _app) in self.apps.iter_mut() {
if ui
.selectable_label(self.selected_anchor == anchor, name)
.clicked()
{
self.selected_anchor = anchor.to_owned();
if frame.is_web() {
ui.output().open_url(format!("#{}", anchor));
}
}
}
ui.with_layout(egui::Layout::right_to_left(), |ui| {
if false {
// TODO: fix the overlap on small screens
if let Some(seconds_since_midnight) = crate::seconds_since_midnight() {
if clock_button(ui, seconds_since_midnight).clicked() {
self.selected_anchor = "clock".to_owned();
if frame.is_web() {
ui.output().open_url("#clock");
}
}
}
}
egui::warn_if_debug_build(ui);
});
});
}
fn ui_file_drag_and_drop(&mut self, ctx: &egui::Context) {
use egui::*;
// Preview hovering files:
if !ctx.input().raw.hovered_files.is_empty() {
let mut text = "Dropping files:\n".to_owned();
for file in &ctx.input().raw.hovered_files {
if let Some(path) = &file.path {
text += &format!("\n{}", path.display());
} else if !file.mime.is_empty() {
text += &format!("\n{}", file.mime);
} else {
text += "\n???";
}
}
let painter =
ctx.layer_painter(LayerId::new(Order::Foreground, Id::new("file_drop_target")));
let screen_rect = ctx.input().screen_rect();
painter.rect_filled(screen_rect, 0.0, Color32::from_black_alpha(192));
painter.text(
screen_rect.center(),
Align2::CENTER_CENTER,
text,
TextStyle::Heading.resolve(&ctx.style()),
Color32::WHITE,
);
}
// Collect dropped files:
if !ctx.input().raw.dropped_files.is_empty() {
self.dropped_files = ctx.input().raw.dropped_files.clone();
}
// Show dropped files (if any):
if !self.dropped_files.is_empty() {
let mut open = true;
egui::Window::new("Dropped files")
.open(&mut open)
.show(ctx, |ui| {
for file in &self.dropped_files {
let mut info = if let Some(path) = &file.path {
path.display().to_string()
} else if !file.name.is_empty() {
file.name.clone()
} else {
"???".to_owned()
};
if let Some(bytes) = &file.bytes {
info += &format!(" ({} bytes)", bytes.len());
}
ui.label(info);
}
});
if !open {
self.dropped_files.clear();
}
}
}
}
fn clock_button(ui: &mut egui::Ui, seconds_since_midnight: f64) -> egui::Response {
let time = seconds_since_midnight;
let time = format!(
"{:02}:{:02}:{:02}.{:02}",
(time % (24.0 * 60.0 * 60.0) / 3600.0).floor(),
(time % (60.0 * 60.0) / 60.0).floor(),
(time % 60.0).floor(),
(time % 1.0 * 100.0).floor()
);
ui.button(egui::RichText::new(time).monospace())
}