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

Add a fractal clock example to showcase painting performance

This commit is contained in:
Emil Ernerfeldt
2020-05-11 20:21:24 +02:00
parent 5a9e3d62bf
commit 71154edf9b
21 changed files with 425 additions and 37 deletions

View File

@@ -37,6 +37,16 @@ impl Frame {
outline: Some(Outline::new(1.0, color::white(128))),
}
}
pub fn fill_color(mut self, fill_color: Option<Color>) -> Self {
self.fill_color = fill_color;
self
}
pub fn outline(mut self, outline: Option<Outline>) -> Self {
self.outline = outline;
self
}
}
impl Frame {

View File

@@ -48,20 +48,27 @@ impl<'open> Window<'open> {
self
}
/// This is quite a crap idea
/// Usage: `Winmdow::new(...).mutate(|w| w.resize = w.resize.auto_expand_width(true))`
/// Not sure this is a good interface for this.
pub fn mutate(mut self, mutate: impl Fn(&mut Self)) -> Self {
mutate(&mut self);
self
}
/// This is quite a crap idea
/// Usage: `Winmdow::new(...).resize(|r| r.auto_expand_width(true))`
/// Not sure this is a good interface for this.
pub fn resize(mut self, mutate: impl Fn(Resize) -> Resize) -> Self {
self.resize = mutate(self.resize);
self
}
/// Usage: `Winmdow::new(...).frame(|f| f.fill_color(Some(BLUE)))`
/// Not sure this is a good interface for this.
pub fn frame(mut self, frame: Frame) -> Self {
self.frame = Some(frame);
self
}
pub fn default_pos(mut self, default_pos: Pos2) -> Self {
self.area = self.area.default_pos(default_pos);
self
@@ -72,6 +79,10 @@ impl<'open> Window<'open> {
self
}
pub fn default_rect(self, rect: Rect) -> Self {
self.default_pos(rect.min).default_size(rect.size())
}
pub fn min_size(mut self, min_size: Vec2) -> Self {
self.resize = self.resize.min_size(min_size);
self
@@ -100,6 +111,13 @@ impl<'open> Window<'open> {
self.scroll = None;
self
}
pub fn scroll(mut self, scroll: bool) -> Self {
if !scroll {
self.scroll = None;
}
self
}
}
impl<'open> Window<'open> {

View File

@@ -80,6 +80,10 @@ impl Context {
})
}
pub fn rect(&self) -> Rect {
Rect::from_min_size(pos2(0.0, 0.0), self.input.screen_size)
}
pub fn memory(&self) -> parking_lot::MutexGuard<'_, Memory> {
self.memory.lock()
}

7
emigui/src/examples.rs Normal file
View File

@@ -0,0 +1,7 @@
mod app;
mod fractal_clock;
pub use {
app::{ExampleApp, ExampleWindow},
fractal_clock::FractalClock,
};

View File

@@ -3,37 +3,62 @@ use std::sync::Arc;
use serde_derive::{Deserialize, Serialize};
use crate::{color::*, containers::*, widgets::*, *};
use crate::{color::*, containers::*, examples::FractalClock, widgets::*, *};
// ----------------------------------------------------------------------------
#[derive(Default, Deserialize, Serialize)]
pub struct ExampleApp {
has_initialized: bool,
example_window: ExampleWindow,
open_windows: OpenWindows,
fractal_clock: FractalClock,
}
impl ExampleApp {
pub fn ui(&mut self, ctx: &Arc<Context>) {
// TODO: Make it even simpler to show a window
// TODO: window manager for automatic positioning?
let ExampleApp {
has_initialized,
example_window,
open_windows,
fractal_clock,
} = self;
if !*has_initialized {
// #fragment end of URL:
let location_hash = ctx
.input()
.web
.as_ref()
.map(|web| web.location_hash.as_str());
if location_hash == Some("#clock") {
open_windows.fractal_clock = true;
}
*has_initialized = true;
}
Window::new("Examples")
.default_pos(pos2(32.0, 100.0))
.default_size(vec2(430.0, 600.0))
.show(ctx, |ui| {
show_menu_bar(ui, &mut self.open_windows);
self.example_window.ui(ui);
show_menu_bar(ui, open_windows);
example_window.ui(ui);
});
Window::new("Settings")
.open(&mut self.open_windows.settings)
.open(&mut open_windows.settings)
.default_pos(pos2(500.0, 100.0))
.default_size(vec2(350.0, 200.0))
.default_size(vec2(350.0, 400.0))
.show(ctx, |ui| {
ctx.settings_ui(ui);
});
Window::new("Inspection")
.open(&mut self.open_windows.inspection)
.open(&mut open_windows.inspection)
.default_pos(pos2(500.0, 400.0))
.default_size(vec2(400.0, 300.0))
.show(ctx, |ui| {
@@ -41,12 +66,14 @@ impl ExampleApp {
});
Window::new("Memory")
.open(&mut self.open_windows.memory)
.open(&mut open_windows.memory)
.default_pos(pos2(700.0, 350.0))
.auto_sized()
.show(ctx, |ui| {
ctx.memory_ui(ui);
});
fractal_clock.window(ctx, &mut open_windows.fractal_clock);
}
}
@@ -55,6 +82,7 @@ struct OpenWindows {
settings: bool,
inspection: bool,
memory: bool,
fractal_clock: bool,
}
impl Default for OpenWindows {
@@ -63,6 +91,7 @@ impl Default for OpenWindows {
settings: false,
inspection: true,
memory: false,
fractal_clock: false,
}
}
}
@@ -75,9 +104,13 @@ fn show_menu_bar(ui: &mut Ui, windows: &mut OpenWindows) {
ui.add(Button::new("Don't Quit"));
});
menu::menu(ui, "Windows", |ui| {
// TODO: open on top when clicking a new.
// Maybe an Window or Area can detect that: if wasn't open last frame, but is now,
// then automatically go to front?
ui.add(Checkbox::new(&mut windows.settings, "Settings"));
ui.add(Checkbox::new(&mut windows.inspection, "Inspection"));
ui.add(Checkbox::new(&mut windows.memory, "Memory"));
ui.add(Checkbox::new(&mut windows.fractal_clock, "Fractal Clock"));
});
menu::menu(ui, "About", |ui| {
ui.add(label!("This is Emigui, but you already knew that!"));
@@ -325,7 +358,7 @@ impl Painting {
for line in &self.lines {
if line.len() >= 2 {
ui.add_paint_cmd(PaintCmd::Line {
ui.add_paint_cmd(PaintCmd::LinePath {
points: line.iter().map(|p| canvas_corner + *p).collect(),
color: LIGHT_GRAY,
width: 2.0,

View File

@@ -0,0 +1,194 @@
use std::sync::Arc;
use serde_derive::{Deserialize, Serialize};
use crate::{containers::*, widgets::*, *};
#[derive(Deserialize, Serialize)]
#[serde(default)]
pub struct FractalClock {
paused: bool,
time: f64,
zoom: f32,
start_line_width: f32,
depth: usize,
length_factor: f32,
luminance_factor: f32,
width_factor: f32,
}
impl Default for FractalClock {
fn default() -> Self {
Self {
paused: false,
time: 0.0,
zoom: 0.25,
start_line_width: 2.5,
depth: 9,
length_factor: 0.8,
luminance_factor: 0.8,
width_factor: 0.9,
}
}
}
impl FractalClock {
pub fn window(&mut self, ctx: &Arc<Context>, open: &mut bool) {
Window::new("FractalClock")
.open(open)
.default_rect(ctx.rect().expand(-40.0))
.scroll(false)
// Dark background frame to make it pop:
.frame(Frame::window(&ctx.style()).fill_color(Some(color::black(250))))
.show(ctx, |ui| self.ui(ui));
}
pub fn ui(&mut self, ui: &mut Ui) {
self.fractal_ui(ui);
// TODO: background frame etc
Frame::popup(ui.style())
.fill_color(Some(color::gray(34, 160)))
.outline(None)
.show(&mut ui.left_column(320.0), |ui| self.options_ui(ui));
}
fn options_ui(&mut self, ui: &mut Ui) {
let time = if let Some(seconds_since_midnight) = ui.input().seconds_since_midnight {
ui.add(label!(
"Local time: {:02}:{:02}:{:02}.{:03}",
(seconds_since_midnight.rem_euclid(24.0 * 60.0 * 60.0) / 3600.0).floor(),
(seconds_since_midnight.rem_euclid(60.0 * 60.0) / 60.0).floor(),
(seconds_since_midnight.rem_euclid(60.0)).floor(),
(seconds_since_midnight.rem_euclid(1.0) * 1000.0).floor()
));
seconds_since_midnight
} else {
ui.add(label!(
"The fractal_clock clock is not showing the correct time"
));
ui.input().time
};
ui.add(Checkbox::new(&mut self.paused, "Paused"));
ui.add(Slider::f32(&mut self.zoom, 0.0..=1.0).text("zoom"));
ui.add(Slider::f32(&mut self.start_line_width, 0.0..=5.0).text("Start line width"));
ui.add(Slider::usize(&mut self.depth, 0..=14).text("depth"));
ui.add(Slider::f32(&mut self.length_factor, 0.0..=1.0).text("length factor"));
ui.add(Slider::f32(&mut self.luminance_factor, 0.0..=1.0).text("luminance factor"));
ui.add(Slider::f32(&mut self.width_factor, 0.0..=1.0).text("width factor"));
if ui.add(Button::new("Reset")).clicked {
*self = Default::default();
}
if !self.paused {
self.time = time;
}
ui.add(
Hyperlink::new("http://www.dqd.com/~mayoff/programs/FractalClock/")
.text("Inspired by a screensaver by Rob Mayoff"),
);
}
fn fractal_ui(&mut self, ui: &mut Ui) {
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 rect = ui.available_rect_min();
let scale = self.zoom * rect.width().min(rect.height());
let mut paint_line = |points: [Pos2; 2], color: Color, width: f32| {
let line = [
rect.center() + scale * points[0].to_vec2(),
rect.center() + scale * points[1].to_vec2(),
];
ui.add_paint_cmd(PaintCmd::line_segment([line[0], line[1]], color, width));
};
let hand_rotations = [
hands[0].angle - hands[2].angle,
hands[1].angle - hands[2].angle,
];
let hand_rotors = [
hands[0].length * Vec2::angled(hand_rotations[0]),
hands[1].length * Vec2::angled(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], color::additive_gray(255), width);
if i < 2 {
nodes.push(Node {
pos: end,
dir: hand.vec,
});
}
}
let mut luminance = 0.7; // Start dimmer than main hands
let mut new_nodes = Vec::new();
for _ in 0..self.depth {
new_nodes.clear();
new_nodes.reserve(nodes.len() * 2);
luminance *= self.luminance_factor;
width *= self.width_factor;
let luminance_u8 = (255.0 * luminance).round() as u8;
for rotor in &hand_rotors {
for a in &nodes {
let new_dir = rotor.rotate_other(a.dir);
let b = Node {
pos: a.pos + new_dir,
dir: new_dir,
};
paint_line([a.pos, b.pos], color::additive_gray(luminance_u8), width);
new_nodes.push(b);
}
}
std::mem::swap(&mut nodes, &mut new_nodes);
}
}
}

View File

@@ -1,8 +1,10 @@
use serde_derive::Deserialize;
use crate::math::*;
/// What the integration gives to the gui.
/// All coordinates in emigui is in point/logical coordinates.
#[derive(Clone, Debug, Default, serde_derive::Deserialize)]
#[derive(Clone, Debug, Default, Deserialize)]
#[serde(default)]
pub struct RawInput {
/// Is the button currently down?
@@ -15,6 +17,7 @@ pub struct RawInput {
pub scroll_delta: Vec2,
/// Size of the screen in points.
/// TODO: this should be screen_rect for easy sandboxing.
pub screen_size: Vec2,
/// Also known as device pixel ratio, > 1 for HDPI screens.
@@ -23,6 +26,9 @@ pub struct RawInput {
/// Time in seconds. Relative to whatever. Used for animation.
pub time: f64,
/// Local time. Only used for the clock in the example app.
pub seconds_since_midnight: Option<f64>,
/// Files has been dropped into the window.
pub dropped_files: Vec<std::path::PathBuf>,
@@ -31,6 +37,9 @@ pub struct RawInput {
/// In-order events received this frame
pub events: Vec<Event>,
/// Web-only input
pub web: Option<Web>,
}
/// What emigui maintains
@@ -74,6 +83,9 @@ pub struct GuiInput {
/// Time since last frame, in seconds.
pub dt: f32,
/// Local time. Only used for the clock in the example app.
pub seconds_since_midnight: Option<f64>,
/// Files has been dropped into the window.
pub dropped_files: Vec<std::path::PathBuf>,
@@ -82,9 +94,20 @@ pub struct GuiInput {
/// In-order events received this frame
pub events: Vec<Event>,
/// Web-only input
pub web: Option<Web>,
}
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, serde_derive::Deserialize)]
#[derive(Clone, Debug, Default, Eq, Ord, PartialEq, PartialOrd, Deserialize)]
#[serde(default)]
pub struct Web {
pub location: String,
/// i.e. "#fragment"
pub location_hash: String,
}
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Event {
Copy,
@@ -97,7 +120,7 @@ pub enum Event {
},
}
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, serde_derive::Deserialize)]
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Key {
Alt,
@@ -145,9 +168,11 @@ impl GuiInput {
pixels_per_point: new.pixels_per_point,
time: new.time,
dt,
seconds_since_midnight: new.seconds_since_midnight,
dropped_files: new.dropped_files.clone(),
hovered_files: new.hovered_files.clone(),
events: new.events.clone(),
web: new.web.clone(),
}
}
}
@@ -166,6 +191,9 @@ impl RawInput {
ui.add(label!("events: {:?}", self.events));
ui.add(label!("dropped_files: {:?}", self.dropped_files));
ui.add(label!("hovered_files: {:?}", self.hovered_files));
if let Some(web) = &self.web {
web.ui(ui);
}
}
}
@@ -189,5 +217,16 @@ impl GuiInput {
ui.add(label!("events: {:?}", self.events));
ui.add(label!("dropped_files: {:?}", self.dropped_files));
ui.add(label!("hovered_files: {:?}", self.hovered_files));
if let Some(web) = &self.web {
web.ui(ui);
}
}
}
impl Web {
pub fn ui(&self, ui: &mut crate::Ui) {
use crate::label;
ui.add(label!("location: '{}'", self.location));
ui.add(label!("location_hash: '{}'", self.location_hash));
}
}

View File

@@ -25,7 +25,7 @@
pub mod color;
pub mod containers;
mod context;
pub mod example_app;
pub mod examples;
mod font;
mod fonts;
mod id;

View File

@@ -65,6 +65,16 @@ impl Vec2 {
vec2(angle.cos(), angle.sin())
}
/// Use this vector as a rotor, rotating something else.
/// Example: Vec2::angled(angle).rotate_other(some_vec)
#[must_use]
pub fn rotate_other(self, v: Vec2) -> Self {
Self {
x: v.x * self.x + v.y * -self.y,
y: v.x * self.y + v.y * self.x,
}
}
#[must_use]
pub fn floor(self) -> Self {
vec2(self.x.floor(), self.y.floor())

View File

@@ -256,6 +256,10 @@ impl Ui {
self.finite_bottom_right() - self.cursor
}
pub fn available_rect_min(&self) -> Rect {
Rect::from_min_size(self.cursor, self.available_space_min())
}
pub fn direction(&self) -> Direction {
self.dir
}

View File

@@ -133,6 +133,12 @@ impl Hyperlink {
url,
}
}
/// Show some other text than the url
pub fn text(mut self, text: impl Into<String>) -> Self {
self.text = text.into();
self
}
}
impl Widget for Hyperlink {
@@ -154,6 +160,7 @@ impl Widget for Hyperlink {
if interact.hovered {
// Underline:
// TODO: underline spaces between words too.
for fragment in &text {
let pos = interact.rect.min;
let y = pos.y + fragment.y_offset + line_spacing;
@@ -260,7 +267,7 @@ impl<'a> Widget for Checkbox<'a> {
let id = ui.make_position_id();
let text_style = TextStyle::Button;
let font = &ui.fonts()[text_style];
let (text, text_size) = font.layout_multiline(&self.text, ui.available_width());
let (text, text_size) = font.layout_single_line(&self.text);
let interact = ui.reserve_space(
ui.style().button_padding
+ vec2(ui.style().start_icon_width, 0.0)