From 73cea29f7dfbbc3a6136ecafe65e4e8a49a6f1f9 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Fri, 21 Aug 2020 18:53:43 +0200 Subject: [PATCH] [docs] Improve README.md and documentation --- README.md | 149 ++++++++++++------ egui/src/app.rs | 6 +- egui/src/containers.rs | 2 +- egui/src/containers/window.rs | 44 ++++-- egui/src/context.rs | 2 +- egui/src/demos.rs | 3 + egui/src/demos/app.rs | 9 +- egui/src/input.rs | 7 +- egui/src/lib.rs | 24 ++- egui/src/paint/tessellator.rs | 274 +++++++++++++++++++--------------- egui/src/ui.rs | 4 + egui_glium/src/painter.rs | 2 + egui_web/src/webgl.rs | 1 + example_glium/src/main.rs | 28 +++- media/demo-2020-08-21.png | Bin 0 -> 29152 bytes 15 files changed, 366 insertions(+), 189 deletions(-) create mode 100644 media/demo-2020-08-21.png diff --git a/README.md b/README.md index d920803d..dbb510fa 100644 --- a/README.md +++ b/README.md @@ -1,68 +1,133 @@ +# Egui +An immediate mode GUI library for Rust. Egui works anywhere you can draw textured triangles. + [![Latest version](https://img.shields.io/crates/v/egui.svg)](https://crates.io/crates/egui) [![Documentation](https://docs.rs/egui/badge.svg)](https://docs.rs/egui) ![MIT](https://img.shields.io/badge/license-MIT-blue.svg) ![Apache](https://img.shields.io/badge/license-Apache-blue.svg) -# Egui -An immediate mode GUI library written in Rust. Works anywhere you can draw textured triangles. +Sections: +* [Demo](#demos) +* [Goals](#goals) +* [State / features](#state) +* [How it works](#how-it-works) +* [Backends](#backends) +* [Other](#other) -## Goals: -* Lightweight -* Short, conveniant syntax -* Responsive (60 Hz without breaking a sweat) -* Portable -* Platform independent (the same code works on the web and as a native app) -## How it works: -Loop: -* Gather input: mouse, touches, screen size, ... -* Run application code (Immediate Mode GUI) -* Output is a triangle mesh -* Render with e.g. OpenGL +## Demo +[Click to run Egui web demo](https://emilk.github.io/egui/index.html). Partial demo source: https://github.com/emilk/egui/blob/master/egui/src/demos/app.rs -## Available backends: -Wherever you can render textured triangles you can use Egui. +[Hobogo](https://emilk.github.io/hobogo/index.html): A small game I made using Egui. Source: https://github.com/emilk/hobogo -* WebAssembly (`egui_web`) for making a web app. [Click to run](https://emilk.github.io/egui/index.html). -* [Glium](https://github.com/glium/glium) for native apps (see example_glium). -* [miniquad](https://github.com/not-fl3/emigui-miniquad) [web demo](https://not-fl3.github.io/miniquad-samples/emigui.html) [demo source](https://github.com/not-fl3/good-web-game/blob/master/examples/emigui.rs) +#### Example: -The same application code can thus be compiled to either into a native app or a web app. +``` rust +Window::new("Debug").show(ui.ctx(), |ui| { + ui.label(format!("Hello, world {}", 123)); + if ui.button("Save").clicked { + my_save_function(); + } + ui.text_edit(&mut my_string); + ui.add(Slider::f32(&mut value, 0.0..=1.0).text("float")); +}); +``` -## Demos -[Egui feature demo](https://emilk.github.io/egui/index.html), (partial) source: https://github.com/emilk/egui/blob/master/egui/src/demos/app.rs + + + +## Goals +* Short and convenient syntax +* Responsive: 60 Hz without breaking a sweat +* Portable: the same code works on the web and as a native app +* Safe: difficult to make a mistake +* Easy to integrate into a any environment +* Provide a simple 2D graphics API for custom painting +* Simple: no callbacks, minimal dependencies, avoid unnecessary monomorphization + +Egui is *not* a framework. Egui is a library you call into, not an environment you program for. -[Hobogo: A small game using Egui](https://emilk.github.io/hobogo/index.html), source: https://github.com/emilk/hobogo ## State -Alpha state. It works, but is somewhat incomplete. +Alpha state. It works well for what it does, but it lacks many features and the interfaces are still in flux. New releases will have breaking changes. -Features: +### Features: -* Labels -* Buttons, checkboxes, radio buttons and sliders -* Horizontal or vertical layout -* Column layout -* Collapsible headers (sections) -* Windows -* Resizable regions -* Vertical scolling -* Simple text input -* Anti-aliased rendering of circles, rounded rectangles and lines. +* Widgets: label, text button, hyperlink, checkbox, radio button, slider, draggable value, text editing +* Layouts: horizontal, vertical, columns +* Text input: very basic, multiline, copy/paste +* Windows: move, resize, name, minimize and close +* Regions: resizing, vertical scrolling, collapsing headers (sections) +* Rendering: Anti-aliased rendering of lines, circles, text and convex polygons. +* Tooltips on hover -## Conventions -* All coordinates are screen space coordinates, in locial "points" (which may consist of many physical pixels). + +## How it works +Loop: +* Gather input (mouse, touches, keyboard, screen size, etc) and give it to Egui +* Run application code (Immediate Mode GUI) +* Tell Egui to tesselate the frame graphics to a triangle mesh +* Render the triangle mesh with your favorite graphics API (see OpenGL examples) + + +## Backends +Wherever you can render textured triangles you can use Egui. + +### Official +I maintain two official Egui backends: + +* [egui_web](crates.io/crates/egui_web) for making a web app. Compiles to WASM, renders with WebGL. [Click to run the Egui demo](https://emilk.github.io/egui/index.html). +* [egui_glium](crates.io/crates/egui_glium) for compiling native apps with [Glium](https://github.com/glium/glium) backend. + +The same code can be compiled to a native app or a web app. + +### 3rd party +* [emigui-miniquad](https://github.com/not-fl3/emigui-miniquad): backend for [Miniquad](https://github.com/not-fl3/miniquad). [Web demo](https://not-fl3.github.io/miniquad-samples/emigui.html) and [demo source](https://github.com/not-fl3/good-web-game/blob/master/examples/emigui.rs). + +### Writing your own Egui backend +You need to collect `egui::RawInput`, paint `egui::PaintJobs` and handle `egui::Output`. The basic structure is this: + +``` rust +let mut egui_ctx = egui::Context::new(); + +// game loop: +loop { + let raw_input: egui::RawInput = my_backend.gather_input(); + let mut ui = egui_ctx.begin_frame(raw_input); + my_app.ui(&mut ui); // add windows and widgets to `ui` here + let (output, paint_jobs) = egui_ctx.end_frame(); + my_backend.paint(paint_jobs); + my_backend.set_cursor_icon(output.cursor_icon); + // Also see `egui::Output` for more +} +``` + + +## Other +### Conventions +* All coordinates are screen space coordinates, in logical "points" (which may consist of many physical pixels). Origin (0, 0) is top left. * All colors have premultiplied alpha -## Inspiration -The one and only [Dear ImGui](https://github.com/ocornut/imgui) is a great Immediate Mode GUI for C++ which works with many backends. That library revolutionized how I think about GUI code from something I hated to do to something I now like to do. +### Inspiration +The one and only [Dear ImGui](https://github.com/ocornut/imgui) is a great Immediate Mode GUI for C++ which works with many backends. That library revolutionized how I think about GUI code and turned GUI programming from something I hated to do to something I now enjoy. -## Name -The name of the gui library is "Egui", written like that in text and as `egui` in code and pronounced as "e-gooey". +#### Differences between Egui and Dear ImGui +Dear ImGui has has many years of development and so of course has more features. It has also been heavily optimized for speed, which Egui has not yet been. + +Where Dear ImGui uses matching `Begin/End` style function calls, which can be error prone. Egui prefers to use lambdas passed to a wrapping function. Lambdas are a bit ugly though, so I'd like to find a nicer solution to this. + +Egui uses the builder pattern for construction widgets. For instance: `ui.add(Label::new("Hello").text_color(RED));` I am not a big fan of the builder pattern (it is quite verbose both in implementation and in use) but until we have named, default arguments it is the best we can do. To alleviate some of the verbosity there are common case helper functions, like `ui.label("Hello");`. + +### Name +The name of the library and the project is "Egui" and pronounced as "e-gooey". The library was originally called "Emigui", but was renamed to Egui in 2020. -## Credits / Licenses +### Credits / Licenses +Egui author: Emil Ernerfeldt + +Egui is under MIT OR Apache-2.0 license. + Fonts: * Comfortaa: Open Font License, see OFT.txt * ProggyClean.ttf, Copyright (c) 2004, 2005 Tristan Grimmer. MIT License. http://www.proggyfonts.net/ diff --git a/egui/src/app.rs b/egui/src/app.rs index ee5d7887..2877fe48 100644 --- a/egui/src/app.rs +++ b/egui/src/app.rs @@ -5,8 +5,8 @@ use crate::Ui; -/// Implement this trait to write apps that can be compiled both natively using the `egui_glium` crate, -/// and deployed as a web site using the `egui_web` crate. +/// Implement this trait to write apps that can be compiled both natively using the [`egui_glium`](https://crates.io/crates/egui_glium) crate, +/// and deployed as a web site using the [`egui_web`](https://crates.io/crates/egui_web) crate. pub trait App { /// Called each time the UI needs repainting, which may be many times per second. fn ui(&mut self, ui: &mut Ui, backend: &mut dyn Backend); @@ -19,7 +19,7 @@ pub trait App { /// How the backend runs the app #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum RunMode { - /// Rapint the UI all the time (at the display refresh rate of e.g. 60 Hz). + /// Repint the UI all the time (at the display refresh rate of e.g. 60 Hz). /// This is good for games where things are constantly moving. /// This can also be achieved with `RunMode::Reactive` combined with calling `egui::Context::request_repaint()` each frame. Continuous, diff --git a/egui/src/containers.rs b/egui/src/containers.rs index 934abc5b..00d21075 100644 --- a/egui/src/containers.rs +++ b/egui/src/containers.rs @@ -1,4 +1,4 @@ -//! Containers are pieces of the UI which wraps other pieces of UI. +//! Containers are pieces of the UI which wraps other pieces of UI. Examples: `Window`, `ScrollArea`, `Resize`, etc. //! //! For instance, a `Frame` adds a frame and background to some contained UI. diff --git a/egui/src/containers/window.rs b/egui/src/containers/window.rs index b71af946..bedfce38 100644 --- a/egui/src/containers/window.rs +++ b/egui/src/containers/window.rs @@ -4,7 +4,14 @@ use crate::{paint::*, widgets::*, *}; use super::*; -/// A floating window which can be moved, closed, collapsed, resized and scrolled. +/// Builder for a floating window which can be dragged, closed, collapsed, resized and scrolled. +/// +/// You can customize: +/// * title +/// * default, minimum, maximum and/or fixed size +/// * if the window has a scroll area +/// * if the window can be collapsed (minimized) to just the title bar +/// * if there should be a close button pub struct Window<'open> { pub title_label: Label, open: Option<&'open mut bool>, @@ -41,71 +48,79 @@ impl<'open> Window<'open> { } } - /// If the given bool is false, the window will not be visible. - /// If the given bool is true, the window will have a close button that sets this bool to false. + /// Call this to add a close-button to the window title bar. + /// + /// * If `*open == false`, the window will not be visible. + /// * If `*open == true`, the window will have a close button. + /// * If the close button is pressed, `*open` will be set to `false`. pub fn open(mut self, open: &'open mut bool) -> Self { self.open = Some(open); self } - /// Usage: `Winmdow::new(...).mutate(|w| w.resize = w.resize.auto_expand_width(true))` + /// Usage: `Window::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 } - /// Usage: `Winmdow::new(...).resize(|r| r.auto_expand_width(true))` + /// Usage: `Window::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(Some(BLUE)))` + /// Usage: `Window::new(...).frame(|f| f.fill(Some(BLUE)))` /// Not sure this is a good interface for this. pub fn frame(mut self, frame: Frame) -> Self { self.frame = Some(frame); self } + /// Set initial position of the window. pub fn default_pos(mut self, default_pos: impl Into) -> Self { self.area = self.area.default_pos(default_pos); self } + /// Set initial size of the window. pub fn default_size(mut self, default_size: impl Into) -> Self { self.resize = self.resize.default_size(default_size); self } + /// Set initial width of the window. pub fn default_width(mut self, default_width: f32) -> Self { self.resize = self.resize.default_width(default_width); self } - + /// Set initial height of the window. pub fn default_height(mut self, default_height: f32) -> Self { self.resize = self.resize.default_height(default_height); self } + /// Set initial position and size of the window. pub fn default_rect(self, rect: Rect) -> Self { self.default_pos(rect.min).default_size(rect.size()) } - /// Positions the window and prevents it from being moved + /// Sets the window position and prevents it from being dragged around. pub fn fixed_pos(mut self, pos: impl Into) -> Self { self.area = self.area.fixed_pos(pos); self } + /// Sets the window size and prevents it from being resized by dragging its edges. pub fn fixed_size(mut self, size: impl Into) -> Self { self.resize = self.resize.fixed_size(size); self } - /// Can you resize it with the mouse? - /// Note that a window can still auto-resize + /// Can the user resize the window by dragging its edges? + /// Note that even if you set this to `false` the window may still auto-resize. pub fn resizable(mut self, resizable: bool) -> Self { self.resize = self.resize.resizable(resizable); self @@ -118,14 +133,21 @@ impl<'open> Window<'open> { } /// Not resizable, just takes the size of its contents. + /// Also disabled scrolling. pub fn auto_sized(mut self) -> Self { self.resize = self.resize.auto_sized(); self.scroll = None; self } + /// Enable/disable scrolling. True by default. pub fn scroll(mut self, scroll: bool) -> Self { - if !scroll { + if scroll { + debug_assert!( + self.scroll.is_some(), + "Window::scroll called multiple times" + ); + } else { self.scroll = None; } self diff --git a/egui/src/context.rs b/egui/src/context.rs index 93e3b2f1..30c288cd 100644 --- a/egui/src/context.rs +++ b/egui/src/context.rs @@ -194,7 +194,7 @@ impl Context { let paint_commands = self.drain_paint_lists(); let num_primitives = paint_commands.len(); let paint_jobs = - tessellator::tessellate_paint_commands(paint_options, self.fonts(), paint_commands); + tessellator::tessellate_paint_commands(paint_commands, paint_options, self.fonts()); { let mut stats = PaintStats::default(); diff --git a/egui/src/demos.rs b/egui/src/demos.rs index a6e2a3fe..a21f7347 100644 --- a/egui/src/demos.rs +++ b/egui/src/demos.rs @@ -1,3 +1,6 @@ +//! Demo-code for showing how Egui is used. +//! +//! The demo-code is also used in benchmarks and tests. mod app; mod fractal_clock; diff --git a/egui/src/demos/app.rs b/egui/src/demos/app.rs index ea703d29..bda1c408 100644 --- a/egui/src/demos/app.rs +++ b/egui/src/demos/app.rs @@ -4,6 +4,10 @@ use crate::{app, color::*, containers::*, demos::FractalClock, paint::*, widgets // ---------------------------------------------------------------------------- +/// Demonstrates how to make an app using Egui. +/// +/// Implements `App` so it can be used with +/// [`egui_glium`](https://crates.io/crates/egui_glium) and [`egui_web`](https://crates.io/crates/egui_web). #[derive(Default)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", serde(default))] @@ -16,7 +20,9 @@ pub struct DemoApp { } impl DemoApp { - /// `web_location_hash`: for web demo only. e.g. "#fragmet". + /// Show the app ui (menu bar and windows). + /// + /// * `web_location_hash`: for web demo only. e.g. "#fragment". Set to "". pub fn ui(&mut self, ui: &mut Ui, web_location_hash: &str) { if self.previous_web_location_hash != web_location_hash { // #fragment end of URL: @@ -34,6 +40,7 @@ impl DemoApp { self.windows(ui.ctx()); } + /// Show the open windows. pub fn windows(&mut self, ctx: &Arc) { let DemoApp { open_windows, diff --git a/egui/src/input.rs b/egui/src/input.rs index 2a12a31e..081414f1 100644 --- a/egui/src/input.rs +++ b/egui/src/input.rs @@ -6,7 +6,8 @@ const MAX_CLICK_DIST: f32 = 6.0; const MAX_CLICK_DELAY: f64 = 0.3; /// What the integration gives to the gui. -/// All coordinates in egui is in point/logical coordinates. +/// All coordinates in egui is in point/logical coordinates +/// with origin (0, 0) in the top left corner. #[derive(Clone, Debug, Default)] pub struct RawInput { /// Is the button currently down? @@ -19,13 +20,13 @@ pub struct RawInput { pub scroll_delta: Vec2, /// Size of the screen in points. - /// TODO: this should be screen_rect for easy sandboxing. + // TODO: this should be screen_rect for easy sandboxing. pub screen_size: Vec2, /// Also known as device pixel ratio, > 1 for HDPI screens. pub pixels_per_point: Option, - /// Time in seconds. Relative to whatever. Used for animation. + /// Time in seconds. Relative to whatever. Used for animations. pub time: f64, /// Local time. Only used for the clock in the demo app. diff --git a/egui/src/lib.rs b/egui/src/lib.rs index 956edc05..6cdfb356 100644 --- a/egui/src/lib.rs +++ b/egui/src/lib.rs @@ -1,4 +1,26 @@ -#![deny(warnings)] +//! Egui core library +//! +//! To get started with Egui, you can use one of the available backends +//! such as [`egui_web`](https://crates.io/crates/egui_web) or [`egui_glium`](https://crates.io/crates/egui_glium). +//! +//! To write your own backend for Egui you need to do this: +//! +//! ``` ignore +//! let mut egui_ctx = egui::Context::new(); +//! +//! // game loop: +//! loop { +//! let raw_input: egui::RawInput = my_backend.gather_input(); +//! let mut ui = egui_ctx.begin_frame(raw_input); +//! my_app.ui(&mut ui); // add windows and widgets to `ui` here +//! let (output, paint_jobs) = egui_ctx.end_frame(); +//! my_backend.paint(paint_jobs); +//! my_backend.set_cursor_icon(output.cursor_icon); +//! // Also see `egui::Output` for more +//! } +//! ``` + +// #![deny(warnings)] // TODO: remove #![warn( clippy::all, clippy::dbg_macro, diff --git a/egui/src/paint/tessellator.rs b/egui/src/paint/tessellator.rs index fa18088e..1c9fc276 100644 --- a/egui/src/paint/tessellator.rs +++ b/egui/src/paint/tessellator.rs @@ -1,3 +1,8 @@ +//! Converts graphics primitives into textured triangles. +//! +//! This module converts lines, circles, text and more represented by `PaintCmd` +//! into textured triangles represented by `Triangles`. + #![allow(clippy::identity_op)] use { @@ -12,17 +17,22 @@ use { /// The UV coordinate of a white region of the texture mesh. const WHITE_UV: (u16, u16) = (1, 1); +/// The vertex type. +/// +/// Should be friendly to send to GPU as is. +#[repr(C)] #[derive(Clone, Copy, Debug, Default)] pub struct Vertex { - /// Logical pixel coordinates (points) - pub pos: Pos2, + /// Logical pixel coordinates (points). + /// (0,0) is the top left corner of the screen. + pub pos: Pos2, // 64 bit /// Texel coordinates in the texture - pub uv: (u16, u16), + pub uv: (u16, u16), // 32 bit /// sRGBA with premultiplied alpha - pub color: Color, + pub color: Color, // 32 bit } -/// Textured triangles +/// Textured triangles. #[derive(Clone, Debug, Default)] pub struct Triangles { /// Draw as triangles (i.e. the length is always multiple of three). @@ -39,36 +49,49 @@ pub type PaintJobs = Vec; // ---------------------------------------------------------------------------- +/// ## Helpers for adding impl Triangles { - pub fn append(&mut self, triangles: &Triangles) { - let index_offset = self.vertices.len() as u32; - for index in &triangles.indices { - self.indices.push(index_offset + index); - } - self.vertices.extend(triangles.vertices.iter()); + /// Are all indices within the bounds of the contained vertices? + pub fn is_valid(&self) -> bool { + let n = self.vertices.len() as u32; + self.indices.iter().all(|&i| i < n) } - fn triangle(&mut self, a: u32, b: u32, c: u32) { + /// Append all the indices and vertices of `other` to `self`. + pub fn append(&mut self, other: &Triangles) { + let index_offset = self.vertices.len() as u32; + for index in &other.indices { + self.indices.push(index_offset + index); + } + self.vertices.extend(other.vertices.iter()); + } + + /// Add a triangle. + pub fn add_triangle(&mut self, a: u32, b: u32, c: u32) { self.indices.push(a); self.indices.push(b); self.indices.push(c); } + /// Make room for this many additional triangles (will reserve 3x as many indices). + /// See also `reserve_vertices`. pub fn reserve_triangles(&mut self, additional_triangles: usize) { self.indices.reserve(3 * additional_triangles); } + /// Make room for this many additional vertices. + /// See also `reserve_triangles`. pub fn reserve_vertices(&mut self, additional: usize) { self.vertices.reserve(additional); } - /// Uniformly colored rectangle + /// Uniformly colored rectangle. pub fn add_rect(&mut self, top_left: Vertex, bottom_right: Vertex) { debug_assert_eq!(top_left.color, bottom_right.color); let idx = self.vertices.len() as u32; - self.triangle(idx + 0, idx + 1, idx + 2); - self.triangle(idx + 2, idx + 1, idx + 3); + self.add_triangle(idx + 0, idx + 1, idx + 2); + self.add_triangle(idx + 2, idx + 1, idx + 3); let top_right = Vertex { pos: pos2(bottom_right.pos.x, top_left.pos.y), @@ -87,7 +110,8 @@ impl Triangles { } /// This is for platforms that only support 16-bit index buffers. - /// Splits this mesh into many small if needed. + /// + /// Splits this mesh into many smaller meshes (if needed). /// All the returned meshes will have indices that fit into a `u16`. pub fn split_to_u16(self) -> Vec { const MAX_SIZE: u32 = 1 << 16; @@ -146,15 +170,20 @@ impl Triangles { pub struct PathPoint { pos: Pos2, - /// For filled paths the normal is used for antialiasing. - /// For outlines the normal is used for figuring out how to make the line wide + /// For filled paths the normal is used for anti-aliasing (both outlines and filled areas). + /// + /// For outlines the normal is also used for giving thickness to the path /// (i.e. in what direction to expand). + /// /// The normal could be estimated by differences between successive points, /// but that would be less accurate (and in some cases slower). + /// + /// Normals are normally unit-length. normal: Vec2, } -/// A 2D path that can be tesselated into triangles. +/// A connected line (without thickness or gaps) which can be tesselated +/// to either to an outline (with thickness) or a filled convex area. #[derive(Clone, Debug, Default)] pub struct Path(Vec); @@ -277,17 +306,24 @@ impl Path { } } - /// with x right, and y down (GUI coords) we have: - /// angle = dir - /// 0 * TAU / 4 = right - /// quadrant 0, right bottom - /// 1 * TAU / 4 = bottom - /// quadrant 1, left bottom - /// 2 * TAU / 4 = left - /// quadrant 2 left top - /// 3 * TAU / 4 = top - /// quadrant 3 right top - /// 4 * TAU / 4 = right + /// Add one quadrant of a circle + /// + /// * quadrant 0: right bottom + /// * quadrant 1: left bottom + /// * quadrant 2: left top + /// * quadrant 3: right top + // + // Derivation: + // + // * angle 0 * TAU / 4 = right + // - quadrant 0: right bottom + // * angle 1 * TAU / 4 = bottom + // - quadrant 1: left bottom + // * angle 2 * TAU / 4 = left + // - quadrant 2: left top + // * angle 3 * TAU / 4 = top + // - quadrant 3: right top + // * angle 4 * TAU / 4 = right pub fn add_circle_quadrant(&mut self, center: Pos2, radius: f32, quadrant: f32) { // TODO: optimize with precalculated vertices for some radii ranges @@ -316,6 +352,7 @@ pub enum PathType { } use self::PathType::{Closed, Open}; +/// Tesselation quality options #[derive(Clone, Copy)] pub struct PaintOptions { /// Anti-aliasing makes shapes appear smoother, but requires more triangles and is therefore slower. @@ -336,11 +373,12 @@ impl Default for PaintOptions { } } +/// Tesselate the given convex area into a polygon. pub fn fill_closed_path( - triangles: &mut Triangles, - options: PaintOptions, path: &[PathPoint], color: Color, + options: PaintOptions, + out: &mut Triangles, ) { if color == color::TRANSPARENT { return; @@ -353,49 +391,48 @@ pub fn fill_closed_path( color, }; if options.anti_alias { - triangles.reserve_triangles(3 * n as usize); - triangles.reserve_vertices(2 * n as usize); + out.reserve_triangles(3 * n as usize); + out.reserve_vertices(2 * n as usize); let color_outer = color::TRANSPARENT; - let idx_inner = triangles.vertices.len() as u32; + let idx_inner = out.vertices.len() as u32; let idx_outer = idx_inner + 1; for i in 2..n { - triangles.triangle(idx_inner + 2 * (i - 1), idx_inner, idx_inner + 2 * i); + out.add_triangle(idx_inner + 2 * (i - 1), idx_inner, idx_inner + 2 * i); } let mut i0 = n - 1; for i1 in 0..n { let p1 = &path[i1 as usize]; let dm = p1.normal * options.aa_size * 0.5; - triangles.vertices.push(vert(p1.pos - dm, color)); - triangles.vertices.push(vert(p1.pos + dm, color_outer)); - triangles.triangle(idx_inner + i1 * 2, idx_inner + i0 * 2, idx_outer + 2 * i0); - triangles.triangle(idx_outer + i0 * 2, idx_outer + i1 * 2, idx_inner + 2 * i1); + out.vertices.push(vert(p1.pos - dm, color)); + out.vertices.push(vert(p1.pos + dm, color_outer)); + out.add_triangle(idx_inner + i1 * 2, idx_inner + i0 * 2, idx_outer + 2 * i0); + out.add_triangle(idx_outer + i0 * 2, idx_outer + i1 * 2, idx_inner + 2 * i1); i0 = i1; } } else { - triangles.reserve_triangles(n as usize); - let idx = triangles.vertices.len() as u32; - triangles - .vertices - .extend(path.iter().map(|p| vert(p.pos, color))); + out.reserve_triangles(n as usize); + let idx = out.vertices.len() as u32; + out.vertices.extend(path.iter().map(|p| vert(p.pos, color))); for i in 2..n { - triangles.triangle(idx, idx + i - 1, idx + i); + out.add_triangle(idx, idx + i - 1, idx + i); } } } +/// Tesselate the given path as an outline with thickness. pub fn paint_path_outline( - triangles: &mut Triangles, - options: PaintOptions, - path_type: PathType, path: &[PathPoint], + path_type: PathType, style: LineStyle, + options: PaintOptions, + out: &mut Triangles, ) { if style.color == color::TRANSPARENT { return; } let n = path.len() as u32; - let idx = triangles.vertices.len() as u32; + let idx = out.vertices.len() as u32; let vert = |pos, color| Vertex { pos, @@ -422,8 +459,8 @@ pub fn paint_path_outline( return; } - triangles.reserve_triangles(4 * n as usize); - triangles.reserve_vertices(3 * n as usize); + out.reserve_triangles(4 * n as usize); + out.reserve_vertices(3 * n as usize); let mut i0 = n - 1; for i1 in 0..n { @@ -431,20 +468,18 @@ pub fn paint_path_outline( let p1 = &path[i1 as usize]; let p = p1.pos; let n = p1.normal; - triangles - .vertices + out.vertices .push(vert(p + n * options.aa_size, color_outer)); - triangles.vertices.push(vert(p, color_inner)); - triangles - .vertices + out.vertices.push(vert(p, color_inner)); + out.vertices .push(vert(p - n * options.aa_size, color_outer)); if connect_with_previous { - triangles.triangle(idx + 3 * i0 + 0, idx + 3 * i0 + 1, idx + 3 * i1 + 0); - triangles.triangle(idx + 3 * i0 + 1, idx + 3 * i1 + 0, idx + 3 * i1 + 1); + out.add_triangle(idx + 3 * i0 + 0, idx + 3 * i0 + 1, idx + 3 * i1 + 0); + out.add_triangle(idx + 3 * i0 + 1, idx + 3 * i1 + 0, idx + 3 * i1 + 1); - triangles.triangle(idx + 3 * i0 + 1, idx + 3 * i0 + 2, idx + 3 * i1 + 1); - triangles.triangle(idx + 3 * i0 + 2, idx + 3 * i1 + 1, idx + 3 * i1 + 2); + out.add_triangle(idx + 3 * i0 + 1, idx + 3 * i0 + 2, idx + 3 * i1 + 1); + out.add_triangle(idx + 3 * i0 + 2, idx + 3 * i1 + 1, idx + 3 * i1 + 2); } i0 = i1; } @@ -462,8 +497,8 @@ pub fn paint_path_outline( . |-----| inner_rad */ - triangles.reserve_triangles(6 * n as usize); - triangles.reserve_vertices(4 * n as usize); + out.reserve_triangles(6 * n as usize); + out.reserve_vertices(4 * n as usize); let mut i0 = n - 1; for i1 in 0..n { @@ -473,44 +508,36 @@ pub fn paint_path_outline( let p1 = &path[i1 as usize]; let p = p1.pos; let n = p1.normal; - triangles - .vertices - .push(vert(p + n * outer_rad, color_outer)); - triangles - .vertices - .push(vert(p + n * inner_rad, color_inner)); - triangles - .vertices - .push(vert(p - n * inner_rad, color_inner)); - triangles - .vertices - .push(vert(p - n * outer_rad, color_outer)); + out.vertices.push(vert(p + n * outer_rad, color_outer)); + out.vertices.push(vert(p + n * inner_rad, color_inner)); + out.vertices.push(vert(p - n * inner_rad, color_inner)); + out.vertices.push(vert(p - n * outer_rad, color_outer)); if connect_with_previous { - triangles.triangle(idx + 4 * i0 + 0, idx + 4 * i0 + 1, idx + 4 * i1 + 0); - triangles.triangle(idx + 4 * i0 + 1, idx + 4 * i1 + 0, idx + 4 * i1 + 1); + out.add_triangle(idx + 4 * i0 + 0, idx + 4 * i0 + 1, idx + 4 * i1 + 0); + out.add_triangle(idx + 4 * i0 + 1, idx + 4 * i1 + 0, idx + 4 * i1 + 1); - triangles.triangle(idx + 4 * i0 + 1, idx + 4 * i0 + 2, idx + 4 * i1 + 1); - triangles.triangle(idx + 4 * i0 + 2, idx + 4 * i1 + 1, idx + 4 * i1 + 2); + out.add_triangle(idx + 4 * i0 + 1, idx + 4 * i0 + 2, idx + 4 * i1 + 1); + out.add_triangle(idx + 4 * i0 + 2, idx + 4 * i1 + 1, idx + 4 * i1 + 2); - triangles.triangle(idx + 4 * i0 + 2, idx + 4 * i0 + 3, idx + 4 * i1 + 2); - triangles.triangle(idx + 4 * i0 + 3, idx + 4 * i1 + 2, idx + 4 * i1 + 3); + out.add_triangle(idx + 4 * i0 + 2, idx + 4 * i0 + 3, idx + 4 * i1 + 2); + out.add_triangle(idx + 4 * i0 + 3, idx + 4 * i1 + 2, idx + 4 * i1 + 3); } i0 = i1; } } } else { - triangles.reserve_triangles(2 * n as usize); - triangles.reserve_vertices(2 * n as usize); + out.reserve_triangles(2 * n as usize); + out.reserve_vertices(2 * n as usize); let last_index = if path_type == Closed { n } else { n - 1 }; for i in 0..last_index { - triangles.triangle( + out.add_triangle( idx + (2 * i + 0) % (2 * n), idx + (2 * i + 1) % (2 * n), idx + (2 * i + 2) % (2 * n), ); - triangles.triangle( + out.add_triangle( idx + (2 * i + 2) % (2 * n), idx + (2 * i + 1) % (2 * n), idx + (2 * i + 3) % (2 * n), @@ -526,21 +553,15 @@ pub fn paint_path_outline( return; } for p in path { - triangles - .vertices - .push(vert(p.pos + radius * p.normal, color)); - triangles - .vertices - .push(vert(p.pos - radius * p.normal, color)); + out.vertices.push(vert(p.pos + radius * p.normal, color)); + out.vertices.push(vert(p.pos - radius * p.normal, color)); } } else { let radius = style.width / 2.0; for p in path { - triangles - .vertices + out.vertices .push(vert(p.pos + radius * p.normal, style.color)); - triangles - .vertices + out.vertices .push(vert(p.pos - radius * p.normal, style.color)); } } @@ -560,32 +581,39 @@ fn mul_color(color: Color, factor: f32) -> Color { // ---------------------------------------------------------------------------- -/// `reused_path`: only used to reuse memory +/// Tesselate a single `PaintCmd` into a `Triangles`. +/// +/// * `command`: the command to tesselate +/// * `options`: tesselation quality +/// * `fonts`: font source when tesselating text +/// * `out`: where the triangles are put +/// * `scratchpad_path`: if you plan to run `tessellate_paint_command` +/// many times, pass it a reference to the same `Path` to avoid excessive allocations. pub fn tessellate_paint_command( - reused_path: &mut Path, + command: PaintCmd, options: PaintOptions, fonts: &Fonts, - command: PaintCmd, out: &mut Triangles, + scratchpad_path: &mut Path, ) { - let path = reused_path; + let path = scratchpad_path; path.clear(); match command { PaintCmd::Noop => {} PaintCmd::Circle { center, + radius, fill, outline, - radius, } => { if radius > 0.0 { path.add_circle(center, radius); if let Some(fill) = fill { - fill_closed_path(out, options, &path.0, fill); + fill_closed_path(&path.0, fill, options, out); } - if let Some(outline) = outline { - paint_path_outline(out, options, Closed, &path.0, outline); + if let Some(line_style) = outline { + paint_path_outline(&path.0, Closed, line_style, options, out); } } } @@ -594,7 +622,7 @@ pub fn tessellate_paint_command( } PaintCmd::LineSegment { points, style } => { path.add_line_segment(points); - paint_path_outline(out, options, Open, &path.0, style); + paint_path_outline(&path.0, Open, style, options, out); } PaintCmd::Path { path, @@ -608,19 +636,19 @@ pub fn tessellate_paint_command( closed, "You asked to fill a path that is not closed. That makes no sense." ); - fill_closed_path(out, options, &path.0, fill); + fill_closed_path(&path.0, fill, options, out); } - if let Some(outline) = outline { + if let Some(line_style) = outline { let typ = if closed { Closed } else { Open }; - paint_path_outline(out, options, typ, &path.0, outline); + paint_path_outline(&path.0, typ, line_style, options, out); } } } PaintCmd::Rect { + mut rect, corner_radius, fill, outline, - mut rect, } => { if !rect.is_empty() { // It is common to (sometimes accidentally) create an infinitely sized ractangle. @@ -630,10 +658,10 @@ pub fn tessellate_paint_command( path.add_rounded_rectangle(rect, corner_radius); if let Some(fill) = fill { - fill_closed_path(out, options, &path.0, fill); + fill_closed_path(&path.0, fill, options, out); } - if let Some(outline) = outline { - paint_path_outline(out, options, Closed, &path.0, outline); + if let Some(line_style) = outline { + paint_path_outline(&path.0, Closed, line_style, options, out); } } } @@ -678,13 +706,23 @@ pub fn tessellate_paint_command( } } -/// Turns `PaintCmd`:s into sets of triangles +/// Turns `PaintCmd`:s into sets of triangles. +/// +/// The given commands will be painted back-to-front (painters algorithm). +/// They will be batched together by clip rectangle. +/// +/// * `commands`: the command to tesselate +/// * `options`: tesselation quality +/// * `fonts`: font source when tesselating text +/// +/// ## Returns +/// A list of clip rectangles with matching `Triangles`. pub fn tessellate_paint_commands( + commands: Vec<(Rect, PaintCmd)>, options: PaintOptions, fonts: &Fonts, - commands: Vec<(Rect, PaintCmd)>, ) -> Vec<(Rect, Triangles)> { - let mut reused_path = Path::default(); + let mut scratchpad_path = Path::default(); let mut jobs = PaintJobs::default(); for (clip_rect, cmd) in commands { @@ -695,22 +733,22 @@ pub fn tessellate_paint_commands( } let out = &mut jobs.last_mut().unwrap().1; - tessellate_paint_command(&mut reused_path, options, fonts, cmd, out); + tessellate_paint_command(cmd, options, fonts, out, &mut scratchpad_path); } if options.debug_paint_clip_rects { for (clip_rect, triangles) in &mut jobs { tessellate_paint_command( - &mut reused_path, - options, - fonts, PaintCmd::Rect { rect: *clip_rect, corner_radius: 0.0, fill: None, outline: Some(LineStyle::new(2.0, srgba(150, 255, 150, 255))), }, + options, + fonts, triangles, + &mut scratchpad_path, ) } } diff --git a/egui/src/ui.rs b/egui/src/ui.rs index 136b2e23..0a4106a7 100644 --- a/egui/src/ui.rs +++ b/egui/src/ui.rs @@ -472,6 +472,10 @@ impl Ui { self.add(RadioButton::new(checked, text)) } + pub fn text_edit(&mut self, text: &mut String) -> GuiResponse { + self.add(TextEdit::new(text)) + } + /// Show a radio button. It is selected if `*curr_value == radio_value`. /// If clicked, `radio_value` is assigned to `*curr_value`; pub fn radio_value( diff --git a/egui_glium/src/painter.rs b/egui_glium/src/painter.rs index bcb3d457..56f746e1 100644 --- a/egui_glium/src/painter.rs +++ b/egui_glium/src/painter.rs @@ -206,6 +206,8 @@ impl Painter { triangles: &Triangles, texture: &egui::Texture, ) { + debug_assert!(triangles.is_valid()); + let vertex_buffer = { #[derive(Copy, Clone)] struct Vertex { diff --git a/egui_web/src/webgl.rs b/egui_web/src/webgl.rs index 59b83af0..8686b87b 100644 --- a/egui_web/src/webgl.rs +++ b/egui_web/src/webgl.rs @@ -235,6 +235,7 @@ impl Painter { } fn paint_triangles(&self, triangles: &Triangles) -> Result<(), JsValue> { + debug_assert!(triangles.is_valid()); let indices: Vec = triangles.indices.iter().map(|idx| *idx as u16).collect(); let mut positions: Vec = Vec::with_capacity(2 * triangles.vertices.len()); diff --git a/example_glium/src/main.rs b/example_glium/src/main.rs index a0129b31..e2efc1fd 100644 --- a/example_glium/src/main.rs +++ b/example_glium/src/main.rs @@ -1,25 +1,33 @@ +//! Example of how to use Egui + #![deny(warnings)] #![warn(clippy::all)] +use egui::{Slider, Window}; use egui_glium::{storage::FileStorage, RunMode}; /// We derive Deserialize/Serialize so we can persist app state on shutdown. #[derive(Default, serde::Deserialize, serde::Serialize)] struct MyApp { - counter: u64, + my_string: String, + value: f32, } impl egui::app::App for MyApp { /// This function will be called whenever the Ui needs to be shown, /// which may be many times per second. fn ui(&mut self, ui: &mut egui::Ui, _: &mut dyn egui::app::Backend) { - if ui.button("Increment").clicked { - self.counter += 1; - } - if ui.button("Reset").clicked { - self.counter = 0; - } - ui.label(format!("Counter: {}", self.counter)); + let MyApp { my_string, value } = self; + + // Example used in `README.md`. + Window::new("Debug").show(ui.ctx(), |ui| { + ui.label(format!("Hello, world {}", 123)); + if ui.button("Save").clicked { + my_save_function(); + } + ui.text_edit(my_string); + ui.add(Slider::f32(value, 0.0..=1.0).text("float")); + }); } fn on_exit(&mut self, storage: &mut dyn egui::app::Storage) { @@ -33,3 +41,7 @@ fn main() { let app: MyApp = egui::app::get_value(&storage, egui::app::APP_KEY).unwrap_or_default(); // Restore `MyApp` from file, or create new `MyApp`. egui_glium::run(title, RunMode::Reactive, storage, app); } + +fn my_save_function() { + // dummy +} diff --git a/media/demo-2020-08-21.png b/media/demo-2020-08-21.png new file mode 100644 index 0000000000000000000000000000000000000000..00101a6d5107d72c4d8d28740a913540895707bf GIT binary patch literal 29152 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

fWO;5~47@B{j*&flko729itg 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 I=@|b1085wp3IG5A literal 0 HcmV?d00001