mirror of
https://github.com/emilk/egui.git
synced 2026-09-01 06:10:06 -04:00
Rename from "Emigui" to "Egui"
Shorter to type (especially in code).
This commit is contained in:
22
egui/Cargo.toml
Normal file
22
egui/Cargo.toml
Normal file
@@ -0,0 +1,22 @@
|
||||
[package]
|
||||
name = "egui"
|
||||
version = "0.1.0"
|
||||
authors = ["Emil Ernerfeldt <emilernerfeldt@gmail.com>"]
|
||||
license = "MIT OR Apache-2.0"
|
||||
edition = "2018"
|
||||
|
||||
[lib]
|
||||
|
||||
[dependencies]
|
||||
ahash = "0.3"
|
||||
parking_lot = "0.10"
|
||||
rusttype = "0.9"
|
||||
serde = "1"
|
||||
serde_derive = "1"
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = { version = "0.3", default-features = false }
|
||||
|
||||
[[bench]]
|
||||
name = "benchmark"
|
||||
harness = false
|
||||
106
egui/README.md
Normal file
106
egui/README.md
Normal file
@@ -0,0 +1,106 @@
|
||||
# GUI implementation
|
||||
This is the core library crate Egui. It is fully platform independent without any backend. You give the Egui library input each frame (mouse pos etc), and it outputs a triangle mesh for you to paint.
|
||||
|
||||
## TODO:
|
||||
### Widgets
|
||||
* [x] Label
|
||||
* [x] Button
|
||||
* [x] Checkbox
|
||||
* [x] Radiobutton
|
||||
* [x] Horizontal slider
|
||||
* [ ] Vertical slider
|
||||
* [x] Collapsing header region
|
||||
* [x] Tooltip
|
||||
* [x] Movable/resizable windows
|
||||
* [x] Kinetic windows
|
||||
* [ ] Windows should open from `UI`s and be boxed by parent ui.
|
||||
* Then we could open the example app inside a window in the example app, recursively.
|
||||
* [x] Resize any side and corner on windows
|
||||
* [ ] Fix autoshrink
|
||||
* [ ] Scroll areas
|
||||
* [x] Vertical scrolling
|
||||
* [ ] Horizontal scrolling
|
||||
* [x] Scroll-wheel input
|
||||
* [x] Drag background to scroll
|
||||
* [ ] Kinetic scrolling
|
||||
* [x] Add support for clicking hyperlinks
|
||||
* [x] Menu bar (File, Edit, etc)
|
||||
* [ ] Sub-menus
|
||||
* [ ] Keyboard shortcuts
|
||||
* [ ] Text input
|
||||
* [x] Input events (key presses)
|
||||
* [x] Text focus
|
||||
* [x] Cursor movement
|
||||
* [ ] Text selection
|
||||
* [ ] Clipboard copy/paste
|
||||
* [ ] Move focus with tab
|
||||
* [x] Handle leading/trailing space
|
||||
* [ ] Color picker
|
||||
* [ ] Style editor
|
||||
* [ ] Table with resizable columns
|
||||
* [ ] Layout
|
||||
* [ ] Generalize Layout (separate from Ui)
|
||||
* [ ] Cascading layout: same lite if it fits, else next line. Like text.
|
||||
* [ ] Grid layout
|
||||
* [ ] Point list
|
||||
* [ ] Image support
|
||||
|
||||
### Web version:
|
||||
* [x] Scroll input
|
||||
* [x] Change to resize cursor on hover
|
||||
* [ ] Make it a JS library for easily creating your own stuff
|
||||
* [x] Read url fragment and redirect to a subpage (e.g. different examples apps)
|
||||
|
||||
### Visuals
|
||||
* [x] Simplify button style to make for nicer collapsible headers. Maybe weak outline? Or just subtle different text color?
|
||||
* [/] Pixel-perfect painting (round positions to nearest pixel).
|
||||
* [ ] Make sure alpha blending is correct (different between web and glium)
|
||||
* [ ] Color picker widgets
|
||||
* [ ] Fix thin rounded corners rendering bug (too bright)
|
||||
|
||||
### Animations
|
||||
Add extremely quick animations for some things, maybe 2-3 frames. For instance:
|
||||
* [x] Animate collapsing headers with clip_rect
|
||||
|
||||
### Clip rects
|
||||
* [x] Separate Ui::clip_rect from Ui::rect
|
||||
* [x] Use clip rectangles when painting
|
||||
* [x] Use clip rectangles when interacting
|
||||
* [x] Adjust clip rects so edges of child widgets aren't clipped
|
||||
* [ ] Use HW clip rects
|
||||
|
||||
### Modularity
|
||||
* [x] `trait Widget` (`Label`, `Slider`, `Checkbox`, ...)
|
||||
* [ ] `trait Container` (`Frame`, `Resize`, `ScrollArea`, ...)
|
||||
* [ ] `widget::TextButton` implemented as a `container::Button` which contains a `widget::Label`.
|
||||
* [ ] Easily chain `Container`s without nested closures.
|
||||
* e.g. `ui.containers((Frame::new(), Resize::new(), ScrollArea::new()), |ui| ...)`
|
||||
|
||||
### Input
|
||||
* [x] Distinguish between clicks and drags
|
||||
* [x] Double-click
|
||||
* [x] Text
|
||||
* [ ] Support all mouse buttons
|
||||
|
||||
### Debugability / Inspection
|
||||
* [x] Widget debug rectangles
|
||||
* [x] Easily debug why something keeps expanding
|
||||
|
||||
### Other
|
||||
* [x] Persist UI state in external storage
|
||||
* [ ] Persist Example App state
|
||||
* [ ] Build in a profiler which tracks which `Ui` in which window takes up CPU.
|
||||
* [ ] Draw as flame graph
|
||||
* [ ] Draw as hotmap
|
||||
* [ ] Change `width.min(max_width)` to `width.at_most(max_width)`
|
||||
|
||||
### Names and structure
|
||||
* [ ] Rename things to be more consistent with Dear ImGui
|
||||
* [x] Combine Egui and Context?
|
||||
* [x] Solve which parts of Context are behind a mutex
|
||||
* [x] Rename Region to Ui
|
||||
* [ ] Move Path and Triangles to own crate
|
||||
* [ ] Maybe find a shorter name for the library like `egui`?
|
||||
|
||||
### Global widget search
|
||||
Ability to do a search for any widget. The search works even for collapsed regions and closed windows and menus. This is implemented like this: while searching, all region are layed out and their add_content functions are run. If none of the contents matches the search, the layout is reverted and nothing is shown. So windows will get temporarily opened and run, but if the search is not a match in the window it is closed again. This means then when searching your whole GUI is being run, which may be a bit slower, but it would be a really awesome feature.
|
||||
23
egui/benches/benchmark.rs
Normal file
23
egui/benches/benchmark.rs
Normal file
@@ -0,0 +1,23 @@
|
||||
use criterion::{criterion_group, criterion_main, Criterion};
|
||||
|
||||
pub fn criterion_benchmark(c: &mut Criterion) {
|
||||
let mut example_app = egui::examples::ExampleApp::default();
|
||||
let mut ctx = egui::Context::new(1.0);
|
||||
|
||||
let raw_input = egui::RawInput {
|
||||
screen_size: egui::vec2(1280.0, 1024.0),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
c.bench_function("example_app", |b| {
|
||||
b.iter(|| {
|
||||
ctx.begin_frame(raw_input.clone());
|
||||
let mut ui = ctx.fullscreen_ui();
|
||||
example_app.ui(&mut ui, "");
|
||||
ctx.end_frame()
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
criterion_group!(benches, criterion_benchmark);
|
||||
criterion_main!(benches);
|
||||
BIN
egui/fonts/Comfortaa-Regular.ttf
Executable file
BIN
egui/fonts/Comfortaa-Regular.ttf
Executable file
Binary file not shown.
BIN
egui/fonts/DejaVuSans.ttf
Normal file
BIN
egui/fonts/DejaVuSans.ttf
Normal file
Binary file not shown.
BIN
egui/fonts/DejaVuSansMono.ttf
Normal file
BIN
egui/fonts/DejaVuSansMono.ttf
Normal file
Binary file not shown.
BIN
egui/fonts/ProggyClean.ttf
Normal file
BIN
egui/fonts/ProggyClean.ttf
Normal file
Binary file not shown.
BIN
egui/fonts/Roboto-Regular.ttf
Normal file
BIN
egui/fonts/Roboto-Regular.ttf
Normal file
Binary file not shown.
13
egui/src/containers.rs
Normal file
13
egui/src/containers.rs
Normal file
@@ -0,0 +1,13 @@
|
||||
pub mod area;
|
||||
pub mod collapsing_header;
|
||||
pub mod frame;
|
||||
pub mod menu;
|
||||
pub mod popup;
|
||||
pub mod resize;
|
||||
pub mod scroll_area;
|
||||
pub mod window;
|
||||
|
||||
pub use {
|
||||
area::Area, collapsing_header::CollapsingHeader, frame::Frame, popup::*, resize::Resize,
|
||||
scroll_area::ScrollArea, window::Window,
|
||||
};
|
||||
233
egui/src/containers/area.rs
Normal file
233
egui/src/containers/area.rs
Normal file
@@ -0,0 +1,233 @@
|
||||
//! Area is a `Ui` that has no parent, it floats on the background.
|
||||
//! It has no frame or own size. It is potentioally movable.
|
||||
//! It is the foundation for windows and popups.
|
||||
|
||||
use std::{fmt::Debug, hash::Hash, sync::Arc};
|
||||
|
||||
use crate::*;
|
||||
|
||||
#[derive(Clone, Copy, Debug, serde_derive::Deserialize, serde_derive::Serialize)]
|
||||
pub(crate) struct State {
|
||||
/// Last known pos
|
||||
pub pos: Pos2,
|
||||
|
||||
/// Last know size. Used for catching clicks.
|
||||
pub size: Vec2,
|
||||
|
||||
/// If false, clicks goes stright throught to what is behind us.
|
||||
/// Good for tooltips etc.
|
||||
pub interactable: bool,
|
||||
|
||||
/// You can throw a moveable Area. It's fun.
|
||||
/// TODO: separate out moveable to container?
|
||||
#[serde(skip)]
|
||||
pub vel: Vec2,
|
||||
}
|
||||
|
||||
impl State {
|
||||
pub fn rect(&self) -> Rect {
|
||||
Rect::from_min_size(self.pos, self.size)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Area {
|
||||
id: Id,
|
||||
movable: bool,
|
||||
interactable: bool,
|
||||
order: Order,
|
||||
default_pos: Option<Pos2>,
|
||||
fixed_pos: Option<Pos2>,
|
||||
}
|
||||
|
||||
impl Area {
|
||||
pub fn new(id_source: impl Hash) -> Self {
|
||||
Self {
|
||||
id: Id::new(id_source),
|
||||
movable: true,
|
||||
interactable: true,
|
||||
order: Order::Middle,
|
||||
default_pos: None,
|
||||
fixed_pos: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn layer(&self) -> Layer {
|
||||
Layer {
|
||||
order: self.order,
|
||||
id: self.id,
|
||||
}
|
||||
}
|
||||
|
||||
/// moveable by draggin the area?
|
||||
pub fn movable(mut self, movable: bool) -> Self {
|
||||
self.movable = movable;
|
||||
self.interactable |= movable;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn is_movable(&self) -> bool {
|
||||
self.movable
|
||||
}
|
||||
|
||||
/// If false, clicks goes stright throught to what is behind us.
|
||||
/// Good for tooltips etc.
|
||||
pub fn interactable(mut self, interactable: bool) -> Self {
|
||||
self.interactable = interactable;
|
||||
self.movable &= interactable;
|
||||
self
|
||||
}
|
||||
|
||||
/// `order(Order::Foreground)` for an Area that should always be on top
|
||||
pub fn order(mut self, order: Order) -> Self {
|
||||
self.order = order;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn default_pos(mut self, default_pos: Pos2) -> Self {
|
||||
self.default_pos = Some(default_pos);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn fixed_pos(mut self, fixed_pos: Pos2) -> Self {
|
||||
self.default_pos = Some(fixed_pos);
|
||||
self.fixed_pos = Some(fixed_pos);
|
||||
self.movable = false;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct Prepared {
|
||||
layer: Layer,
|
||||
state: State,
|
||||
movable: bool,
|
||||
}
|
||||
|
||||
impl Area {
|
||||
pub(crate) fn begin(self, ctx: &Arc<Context>) -> Prepared {
|
||||
let Area {
|
||||
id,
|
||||
movable,
|
||||
order,
|
||||
interactable,
|
||||
default_pos,
|
||||
fixed_pos,
|
||||
} = self;
|
||||
|
||||
let default_pos = default_pos.unwrap_or_else(|| pos2(100.0, 100.0)); // TODO
|
||||
let id = ctx.register_unique_id(id, "Area", default_pos);
|
||||
let layer = Layer { order, id };
|
||||
|
||||
let mut state = ctx.memory().areas.get(id).unwrap_or_else(|| State {
|
||||
pos: default_pos,
|
||||
size: Vec2::zero(),
|
||||
interactable,
|
||||
vel: Vec2::zero(),
|
||||
});
|
||||
state.pos = fixed_pos.unwrap_or(state.pos);
|
||||
state.pos = state.pos.round();
|
||||
|
||||
Prepared {
|
||||
layer,
|
||||
state,
|
||||
movable,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn show(self, ctx: &Arc<Context>, add_contents: impl FnOnce(&mut Ui)) -> InteractInfo {
|
||||
let prepared = self.begin(ctx);
|
||||
let mut content_ui = prepared.content_ui(ctx);
|
||||
add_contents(&mut content_ui);
|
||||
prepared.end(ctx, content_ui)
|
||||
}
|
||||
}
|
||||
|
||||
impl Prepared {
|
||||
pub(crate) fn state(&self) -> &State {
|
||||
&self.state
|
||||
}
|
||||
|
||||
pub(crate) fn state_mut(&mut self) -> &mut State {
|
||||
&mut self.state
|
||||
}
|
||||
|
||||
pub(crate) fn content_ui(&self, ctx: &Arc<Context>) -> Ui {
|
||||
Ui::new(
|
||||
ctx.clone(),
|
||||
self.layer,
|
||||
self.layer.id,
|
||||
Rect::from_min_size(self.state.pos, Vec2::infinity()),
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn end(self, ctx: &Arc<Context>, content_ui: Ui) -> InteractInfo {
|
||||
let Prepared {
|
||||
layer,
|
||||
mut state,
|
||||
movable,
|
||||
} = self;
|
||||
|
||||
state.size = (content_ui.child_bounds().max - state.pos).ceil();
|
||||
|
||||
let rect = Rect::from_min_size(state.pos, state.size);
|
||||
let clip_rect = Rect::everything(); // TODO: get from context
|
||||
|
||||
let interact_id = if movable {
|
||||
Some(layer.id.with("move"))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let move_interact =
|
||||
ctx.interact(layer, clip_rect, rect, interact_id, Sense::click_and_drag());
|
||||
|
||||
let input = ctx.input();
|
||||
if move_interact.active {
|
||||
state.pos += input.mouse.delta;
|
||||
state.vel = input.mouse.velocity;
|
||||
} else {
|
||||
let stop_speed = 20.0; // Pixels per second.
|
||||
let friction_coeff = 1000.0; // Pixels per second squared.
|
||||
|
||||
let friction = friction_coeff * input.dt;
|
||||
if friction > state.vel.length() || state.vel.length() < stop_speed {
|
||||
state.vel = Vec2::zero();
|
||||
} else {
|
||||
state.vel -= friction * state.vel.normalized();
|
||||
state.pos += state.vel * input.dt;
|
||||
}
|
||||
}
|
||||
|
||||
// Constrain to screen:
|
||||
let margin = 32.0;
|
||||
state.pos = state.pos.max(pos2(margin - state.size.x, 0.0));
|
||||
state.pos = state.pos.min(pos2(
|
||||
ctx.input().screen_size.x - margin,
|
||||
ctx.input().screen_size.y - margin,
|
||||
));
|
||||
|
||||
state.pos = state.pos.round();
|
||||
|
||||
// ctx.debug_rect(
|
||||
// Rect::from_min_size(state.pos, state.size),
|
||||
// &format!("Area size: {:?}", state.size),
|
||||
// );
|
||||
|
||||
if move_interact.active
|
||||
|| mouse_pressed_on_area(ctx, layer)
|
||||
|| !ctx.memory().areas.visible_last_frame(&layer)
|
||||
{
|
||||
ctx.memory().areas.move_to_top(layer);
|
||||
}
|
||||
ctx.memory().areas.set_state(layer, state);
|
||||
|
||||
move_interact
|
||||
}
|
||||
}
|
||||
|
||||
fn mouse_pressed_on_area(ctx: &Context, layer: Layer) -> bool {
|
||||
if let Some(mouse_pos) = ctx.input().mouse.pos {
|
||||
ctx.input().mouse.pressed && ctx.memory().layer_at(mouse_pos) == Some(layer)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
249
egui/src/containers/collapsing_header.rs
Normal file
249
egui/src/containers/collapsing_header.rs
Normal file
@@ -0,0 +1,249 @@
|
||||
use crate::{
|
||||
layout::Direction,
|
||||
paint::{LineStyle, PaintCmd, Path, TextStyle},
|
||||
widgets::Label,
|
||||
*,
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy, Debug, serde_derive::Deserialize, serde_derive::Serialize)]
|
||||
#[serde(default)]
|
||||
pub(crate) struct State {
|
||||
open: bool,
|
||||
|
||||
#[serde(skip)] // Times are relative, and we don't want to continue animations anyway
|
||||
toggle_time: f64,
|
||||
|
||||
/// Height of the region when open. Used for animations
|
||||
open_height: Option<f32>,
|
||||
}
|
||||
|
||||
impl Default for State {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
open: false,
|
||||
toggle_time: -f64::INFINITY,
|
||||
open_height: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl State {
|
||||
pub fn from_memory_with_default_open(ui: &Ui, id: Id, default_open: bool) -> Self {
|
||||
ui.memory()
|
||||
.collapsing_headers
|
||||
.entry(id)
|
||||
.or_insert(State {
|
||||
open: default_open,
|
||||
..Default::default()
|
||||
})
|
||||
.clone()
|
||||
}
|
||||
|
||||
// Helper
|
||||
pub fn is_open(ctx: &Context, id: Id) -> Option<bool> {
|
||||
ctx.memory()
|
||||
.collapsing_headers
|
||||
.get(&id)
|
||||
.map(|state| state.open)
|
||||
}
|
||||
|
||||
pub fn toggle(&mut self, ui: &Ui) {
|
||||
self.open = !self.open;
|
||||
self.toggle_time = ui.input().time;
|
||||
}
|
||||
|
||||
/// 0 for closed, 1 for open, with tweening
|
||||
pub fn openness(&self, ui: &Ui) -> f32 {
|
||||
let animation_time = ui.style().animation_time;
|
||||
let time_since_toggle = (ui.input().time - self.toggle_time) as f32;
|
||||
let time_since_toggle = time_since_toggle + ui.input().dt; // Instant feedback
|
||||
if self.open {
|
||||
remap_clamp(time_since_toggle, 0.0..=animation_time, 0.0..=1.0)
|
||||
} else {
|
||||
remap_clamp(time_since_toggle, 0.0..=animation_time, 1.0..=0.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Paint the arrow icon that indicated if the region is open or not
|
||||
pub fn paint_icon(&self, ui: &mut Ui, interact: &InteractInfo) {
|
||||
let stroke_color = ui.style().interact(interact).stroke_color;
|
||||
let stroke_width = ui.style().interact(interact).stroke_width;
|
||||
|
||||
let rect = interact.rect;
|
||||
|
||||
let openness = self.openness(ui);
|
||||
|
||||
// Draw a pointy triangle arrow:
|
||||
let rect = Rect::from_center_size(rect.center(), vec2(rect.width(), rect.height()) * 0.75);
|
||||
let mut points = [rect.left_top(), rect.right_top(), rect.center_bottom()];
|
||||
let rotation = Vec2::angled(remap(openness, 0.0..=1.0, -TAU / 4.0..=0.0));
|
||||
for p in &mut points {
|
||||
let v = *p - rect.center();
|
||||
let v = rotation.rotate_other(v);
|
||||
*p = rect.center() + v;
|
||||
}
|
||||
|
||||
ui.add_paint_cmd(PaintCmd::Path {
|
||||
path: Path::from_point_loop(&points),
|
||||
closed: true,
|
||||
fill: None,
|
||||
outline: Some(LineStyle::new(stroke_width, stroke_color)),
|
||||
});
|
||||
}
|
||||
|
||||
/// Show contents if we are open, with a nice animation between closed and open
|
||||
pub fn add_contents<R>(
|
||||
&mut self,
|
||||
ui: &mut Ui,
|
||||
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||
) -> Option<(R, Rect)> {
|
||||
let openness = self.openness(ui);
|
||||
let animate = 0.0 < openness && openness < 1.0;
|
||||
if animate {
|
||||
Some(ui.add_custom(|child_ui| {
|
||||
let max_height = if self.open {
|
||||
if let Some(full_height) = self.open_height {
|
||||
remap_clamp(openness, 0.0..=1.0, 0.0..=full_height)
|
||||
} else {
|
||||
// First frame of expansion.
|
||||
// We don't know full height yet, but we will next frame.
|
||||
// Just use a placehodler value that shows some movement:
|
||||
10.0
|
||||
}
|
||||
} else {
|
||||
let full_height = self.open_height.unwrap_or_default();
|
||||
remap_clamp(openness, 0.0..=1.0, 0.0..=full_height)
|
||||
};
|
||||
|
||||
let mut clip_rect = child_ui.clip_rect();
|
||||
clip_rect.max.y = clip_rect.max.y.min(child_ui.rect().top() + max_height);
|
||||
child_ui.set_clip_rect(clip_rect);
|
||||
|
||||
let top_left = child_ui.top_left();
|
||||
let r = add_contents(child_ui);
|
||||
|
||||
self.open_height = Some(child_ui.bounding_size().y);
|
||||
|
||||
// Pretend children took up less space:
|
||||
let mut child_bounds = child_ui.child_bounds();
|
||||
child_bounds.max.y = child_bounds.max.y.min(top_left.y + max_height);
|
||||
child_ui.force_set_child_bounds(child_bounds);
|
||||
r
|
||||
}))
|
||||
} else if self.open {
|
||||
let r_interact = ui.add_custom(add_contents);
|
||||
let full_size = r_interact.1.size();
|
||||
self.open_height = Some(full_size.y);
|
||||
Some(r_interact)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CollapsingHeader {
|
||||
label: Label,
|
||||
default_open: bool,
|
||||
}
|
||||
|
||||
impl CollapsingHeader {
|
||||
pub fn new(label: impl Into<String>) -> Self {
|
||||
Self {
|
||||
label: Label::new(label)
|
||||
.text_style(TextStyle::Button)
|
||||
.multiline(false),
|
||||
default_open: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn default_open(mut self, open: bool) -> Self {
|
||||
self.default_open = open;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
struct Prepared {
|
||||
id: Id,
|
||||
state: State,
|
||||
}
|
||||
|
||||
impl CollapsingHeader {
|
||||
fn begin(self, ui: &mut Ui) -> Prepared {
|
||||
assert!(
|
||||
ui.layout().dir() == Direction::Vertical,
|
||||
"Horizontal collapsing is unimplemented"
|
||||
);
|
||||
let Self {
|
||||
label,
|
||||
default_open,
|
||||
} = self;
|
||||
|
||||
// TODO: horizontal layout, with icon and text as labels. Insert background behind using Frame.
|
||||
|
||||
let title = label.text();
|
||||
let id = ui.make_unique_id(title);
|
||||
|
||||
let available = ui.available_finite();
|
||||
let text_pos = available.min + vec2(ui.style().indent, 0.0);
|
||||
let galley = label.layout_width(ui, available.width() - ui.style().indent);
|
||||
let text_max_x = text_pos.x + galley.size.x;
|
||||
let desired_width = text_max_x - available.left();
|
||||
let desired_width = desired_width.max(available.width());
|
||||
|
||||
let size = vec2(
|
||||
desired_width,
|
||||
galley.size.y + 2.0 * ui.style().button_padding.y,
|
||||
);
|
||||
|
||||
let rect = ui.allocate_space(size);
|
||||
let interact = ui.interact(rect, id, Sense::click());
|
||||
let text_pos = pos2(text_pos.x, interact.rect.center().y - galley.size.y / 2.0);
|
||||
|
||||
let mut state = State::from_memory_with_default_open(ui, id, default_open);
|
||||
if interact.clicked {
|
||||
state.toggle(ui);
|
||||
}
|
||||
|
||||
let where_to_put_background = ui.paint_list_len();
|
||||
|
||||
{
|
||||
let (mut icon_rect, _) = ui.style().icon_rectangles(interact.rect);
|
||||
icon_rect.set_center(pos2(
|
||||
interact.rect.left() + ui.style().indent / 2.0,
|
||||
interact.rect.center().y,
|
||||
));
|
||||
let icon_interact = InteractInfo {
|
||||
rect: icon_rect,
|
||||
..interact
|
||||
};
|
||||
state.paint_icon(ui, &icon_interact);
|
||||
}
|
||||
|
||||
ui.add_galley(
|
||||
text_pos,
|
||||
galley,
|
||||
label.text_style,
|
||||
Some(ui.style().interact(&interact).stroke_color),
|
||||
);
|
||||
|
||||
ui.insert_paint_cmd(
|
||||
where_to_put_background,
|
||||
PaintCmd::Rect {
|
||||
corner_radius: ui.style().interact(&interact).corner_radius,
|
||||
fill: ui.style().interact(&interact).bg_fill,
|
||||
outline: None,
|
||||
rect: interact.rect,
|
||||
},
|
||||
);
|
||||
|
||||
Prepared { id, state }
|
||||
}
|
||||
|
||||
pub fn show<R>(self, ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) -> Option<R> {
|
||||
let Prepared { id, mut state } = self.begin(ui);
|
||||
let r_interact = state.add_contents(ui, |ui| ui.indent(id, add_contents).0);
|
||||
let ret = r_interact.map(|ri| ri.0);
|
||||
ui.memory().collapsing_headers.insert(id, state);
|
||||
ret
|
||||
}
|
||||
}
|
||||
123
egui/src/containers/frame.rs
Normal file
123
egui/src/containers/frame.rs
Normal file
@@ -0,0 +1,123 @@
|
||||
//! Frame container
|
||||
|
||||
use crate::{paint::*, *};
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Frame {
|
||||
// On each side
|
||||
pub margin: Vec2,
|
||||
pub corner_radius: f32,
|
||||
pub fill: Option<Color>,
|
||||
pub outline: Option<LineStyle>,
|
||||
}
|
||||
|
||||
impl Frame {
|
||||
pub fn window(style: &Style) -> Self {
|
||||
Self {
|
||||
margin: style.window_padding,
|
||||
corner_radius: style.window.corner_radius,
|
||||
fill: Some(style.background_fill),
|
||||
outline: style.interact.inactive.rect_outline, // becauce we can resize windows
|
||||
}
|
||||
}
|
||||
|
||||
pub fn menu_bar(_style: &Style) -> Self {
|
||||
Self {
|
||||
margin: Vec2::splat(1.0),
|
||||
corner_radius: 0.0,
|
||||
fill: None,
|
||||
outline: Some(LineStyle::new(0.5, color::white(128))),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn menu(style: &Style) -> Self {
|
||||
Self {
|
||||
margin: Vec2::splat(1.0),
|
||||
corner_radius: 2.0,
|
||||
fill: Some(style.background_fill),
|
||||
outline: Some(LineStyle::new(1.0, color::white(128))),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn popup(style: &Style) -> Self {
|
||||
Self {
|
||||
margin: style.window_padding,
|
||||
corner_radius: 5.0,
|
||||
fill: Some(style.background_fill),
|
||||
outline: Some(LineStyle::new(1.0, color::white(128))),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fill(mut self, fill: Option<Color>) -> Self {
|
||||
self.fill = fill;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn outline(mut self, outline: Option<LineStyle>) -> Self {
|
||||
self.outline = outline;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Prepared {
|
||||
pub frame: Frame,
|
||||
outer_rect_bounds: Rect,
|
||||
where_to_put_background: usize,
|
||||
pub content_ui: Ui,
|
||||
}
|
||||
|
||||
impl Frame {
|
||||
pub fn begin(self, ui: &mut Ui) -> Prepared {
|
||||
let outer_rect_bounds = ui.available();
|
||||
let inner_rect = outer_rect_bounds.shrink2(self.margin);
|
||||
let where_to_put_background = ui.paint_list_len();
|
||||
let content_ui = ui.child_ui(inner_rect);
|
||||
Prepared {
|
||||
frame: self,
|
||||
outer_rect_bounds,
|
||||
where_to_put_background,
|
||||
content_ui,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn show<R>(self, ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) -> R {
|
||||
let mut prepared = self.begin(ui);
|
||||
let ret = add_contents(&mut prepared.content_ui);
|
||||
prepared.end(ui);
|
||||
ret
|
||||
}
|
||||
}
|
||||
|
||||
impl Prepared {
|
||||
pub fn outer_rect(&self) -> Rect {
|
||||
Rect::from_min_max(
|
||||
self.outer_rect_bounds.min,
|
||||
self.content_ui.child_bounds().max + self.frame.margin,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn end(self, ui: &mut Ui) -> Rect {
|
||||
let outer_rect = self.outer_rect();
|
||||
|
||||
let Prepared {
|
||||
frame,
|
||||
where_to_put_background,
|
||||
..
|
||||
} = self;
|
||||
|
||||
ui.insert_paint_cmd(
|
||||
where_to_put_background,
|
||||
PaintCmd::Rect {
|
||||
corner_radius: frame.corner_radius,
|
||||
fill: frame.fill,
|
||||
outline: frame.outline,
|
||||
rect: outer_rect,
|
||||
},
|
||||
);
|
||||
|
||||
ui.expand_to_include_child(outer_rect);
|
||||
// TODO: move cursor in parent ui
|
||||
|
||||
outer_rect
|
||||
}
|
||||
}
|
||||
153
egui/src/containers/menu.rs
Normal file
153
egui/src/containers/menu.rs
Normal file
@@ -0,0 +1,153 @@
|
||||
use crate::{widgets::*, *};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[derive(Clone, Copy, Debug, serde_derive::Deserialize, serde_derive::Serialize)]
|
||||
pub struct BarState {
|
||||
#[serde(skip)]
|
||||
open_menu: Option<Id>,
|
||||
#[serde(skip)]
|
||||
/// When did we open a menu?
|
||||
open_time: f64,
|
||||
}
|
||||
|
||||
impl Default for BarState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
open_menu: None,
|
||||
open_time: f64::NEG_INFINITY,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn bar<R>(ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) -> (R, Rect) {
|
||||
ui.inner_layout(Layout::horizontal(Align::Center), |ui| {
|
||||
Frame::menu_bar(ui.style()).show(ui, |ui| {
|
||||
let mut style = ui.style().clone();
|
||||
style.button_padding = vec2(2.0, 0.0);
|
||||
// style.interact.active.bg_fill = None;
|
||||
style.interact.active.rect_outline = None;
|
||||
// style.interact.hovered.bg_fill = None;
|
||||
style.interact.hovered.rect_outline = None;
|
||||
style.interact.inactive.bg_fill = None;
|
||||
style.interact.inactive.rect_outline = None;
|
||||
ui.set_style(style);
|
||||
|
||||
// Take full width and fixed height:
|
||||
let height = ui.style().menu_bar.height;
|
||||
ui.set_desired_height(height);
|
||||
ui.expand_to_size(vec2(ui.available().width(), height));
|
||||
add_contents(ui)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Construct a top level menu in a menu bar. This would be e.g. "File", "Edit" etc.
|
||||
pub fn menu(ui: &mut Ui, title: impl Into<String>, add_contents: impl FnOnce(&mut Ui)) {
|
||||
menu_impl(ui, title, Box::new(add_contents))
|
||||
}
|
||||
|
||||
fn menu_impl<'c>(
|
||||
ui: &mut Ui,
|
||||
title: impl Into<String>,
|
||||
add_contents: Box<dyn FnOnce(&mut Ui) + 'c>,
|
||||
) {
|
||||
let title = title.into();
|
||||
let bar_id = ui.id();
|
||||
let menu_id = Id::new(&title);
|
||||
|
||||
let mut bar_state = ui
|
||||
.memory()
|
||||
.menu_bar
|
||||
.get(&bar_id)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
|
||||
let mut button = Button::new(title);
|
||||
|
||||
if bar_state.open_menu == Some(menu_id) {
|
||||
button = button.fill(Some(ui.style().interact.active.fill));
|
||||
}
|
||||
|
||||
let button_interact = ui.add(button);
|
||||
|
||||
interact_with_menu_button(&mut bar_state, ui.input(), menu_id, &button_interact);
|
||||
|
||||
if bar_state.open_menu == Some(menu_id) {
|
||||
let area = Area::new(menu_id)
|
||||
.order(Order::Foreground)
|
||||
.fixed_pos(button_interact.rect.left_bottom());
|
||||
let frame = Frame::menu(ui.style());
|
||||
|
||||
let resize = Resize::default().auto_sized();
|
||||
|
||||
let menu_interact = area.show(ui.ctx(), |ui| {
|
||||
frame.show(ui, |ui| {
|
||||
resize.show(ui, |ui| {
|
||||
let mut style = ui.style().clone();
|
||||
style.button_padding = vec2(2.0, 0.0);
|
||||
// style.interact.active.bg_fill = None;
|
||||
style.interact.active.rect_outline = None;
|
||||
// style.interact.hovered.bg_fill = None;
|
||||
style.interact.hovered.rect_outline = None;
|
||||
style.interact.inactive.bg_fill = None;
|
||||
style.interact.inactive.rect_outline = None;
|
||||
ui.set_style(style);
|
||||
ui.set_layout(Layout::justified(Direction::Vertical));
|
||||
add_contents(ui)
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
if menu_interact.hovered && ui.input().mouse.released {
|
||||
bar_state.open_menu = None;
|
||||
}
|
||||
}
|
||||
|
||||
ui.memory().menu_bar.insert(bar_id, bar_state);
|
||||
}
|
||||
|
||||
fn interact_with_menu_button(
|
||||
bar_state: &mut BarState,
|
||||
input: &InputState,
|
||||
menu_id: Id,
|
||||
button_interact: &GuiResponse,
|
||||
) {
|
||||
if button_interact.hovered && input.mouse.pressed {
|
||||
if bar_state.open_menu.is_some() {
|
||||
bar_state.open_menu = None;
|
||||
} else {
|
||||
bar_state.open_menu = Some(menu_id);
|
||||
bar_state.open_time = input.time;
|
||||
}
|
||||
}
|
||||
|
||||
if button_interact.hovered && input.mouse.released && bar_state.open_menu.is_some() {
|
||||
let time_since_open = input.time - bar_state.open_time;
|
||||
if time_since_open < 0.4 {
|
||||
// A quick click
|
||||
bar_state.open_menu = Some(menu_id);
|
||||
bar_state.open_time = input.time;
|
||||
} else {
|
||||
// A long hold, then release
|
||||
bar_state.open_menu = None;
|
||||
}
|
||||
}
|
||||
|
||||
if button_interact.hovered && bar_state.open_menu.is_some() {
|
||||
bar_state.open_menu = Some(menu_id);
|
||||
}
|
||||
|
||||
let pressed_escape = input.events.iter().any(|event| {
|
||||
matches!(
|
||||
event,
|
||||
Event::Key {
|
||||
key: Key::Escape,
|
||||
pressed: true
|
||||
}
|
||||
)
|
||||
});
|
||||
if pressed_escape {
|
||||
bar_state.open_menu = None;
|
||||
}
|
||||
}
|
||||
27
egui/src/containers/popup.rs
Normal file
27
egui/src/containers/popup.rs
Normal file
@@ -0,0 +1,27 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::*;
|
||||
|
||||
pub fn show_tooltip(ctx: &Arc<Context>, add_contents: impl FnOnce(&mut Ui)) {
|
||||
if let Some(mouse_pos) = ctx.input().mouse.pos {
|
||||
// TODO: default size
|
||||
let id = Id::tooltip();
|
||||
let window_pos = mouse_pos + vec2(16.0, 16.0);
|
||||
show_popup(ctx, id, window_pos, add_contents);
|
||||
}
|
||||
}
|
||||
|
||||
/// Show a pop-over window
|
||||
pub fn show_popup(
|
||||
ctx: &Arc<Context>,
|
||||
id: Id,
|
||||
window_pos: Pos2,
|
||||
add_contents: impl FnOnce(&mut Ui),
|
||||
) -> InteractInfo {
|
||||
use containers::*;
|
||||
Area::new(id)
|
||||
.order(Order::Foreground)
|
||||
.fixed_pos(window_pos)
|
||||
.interactable(false)
|
||||
.show(ctx, |ui| Frame::popup(&ctx.style()).show(ui, add_contents))
|
||||
}
|
||||
349
egui/src/containers/resize.rs
Normal file
349
egui/src/containers/resize.rs
Normal file
@@ -0,0 +1,349 @@
|
||||
#![allow(unused_variables)] // TODO
|
||||
|
||||
use crate::*;
|
||||
|
||||
#[derive(Clone, Copy, Debug, serde_derive::Deserialize, serde_derive::Serialize)]
|
||||
pub(crate) struct State {
|
||||
pub(crate) size: Vec2,
|
||||
|
||||
/// Externally requested size (e.g. by Window) for the next frame
|
||||
pub(crate) requested_size: Option<Vec2>,
|
||||
}
|
||||
|
||||
// TODO: auto-shink/grow should be part of another container!
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Resize {
|
||||
id: Option<Id>,
|
||||
|
||||
/// If false, we are no enabled
|
||||
resizable: bool,
|
||||
|
||||
// Will still try to stay within parent ui bounds
|
||||
min_size: Vec2,
|
||||
max_size: Vec2,
|
||||
|
||||
default_size: Vec2,
|
||||
|
||||
// If true, won't allow you to make window so big that it creates spacing
|
||||
auto_shrink_width: bool,
|
||||
auto_shrink_height: bool,
|
||||
|
||||
// If true, won't allow you to resize smaller than that everything fits.
|
||||
expand_width_to_fit_content: bool,
|
||||
expand_height_to_fit_content: bool,
|
||||
|
||||
outline: bool,
|
||||
handle_offset: Vec2,
|
||||
}
|
||||
|
||||
impl Default for Resize {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
id: None,
|
||||
resizable: true,
|
||||
min_size: Vec2::splat(16.0),
|
||||
max_size: Vec2::infinity(),
|
||||
default_size: vec2(f32::INFINITY, 200.0), // TODO
|
||||
auto_shrink_width: false,
|
||||
auto_shrink_height: false,
|
||||
expand_width_to_fit_content: true,
|
||||
expand_height_to_fit_content: true,
|
||||
outline: true,
|
||||
handle_offset: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Resize {
|
||||
/// Assign an explicit and globablly unique id.
|
||||
pub fn id(mut self, id: Id) -> Self {
|
||||
self.id = Some(id);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn default_width(mut self, width: f32) -> Self {
|
||||
self.default_size.x = width;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn default_height(mut self, height: f32) -> Self {
|
||||
self.default_size.y = height;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn default_size(mut self, default_size: Vec2) -> Self {
|
||||
self.default_size = default_size;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn min_size(mut self, min_size: Vec2) -> Self {
|
||||
self.min_size = min_size;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn max_size(mut self, max_size: Vec2) -> Self {
|
||||
self.max_size = max_size;
|
||||
self
|
||||
}
|
||||
|
||||
/// Can you resize it with the mouse?
|
||||
/// Note that a window can still auto-resize
|
||||
pub fn resizable(mut self, resizable: bool) -> Self {
|
||||
self.resizable = resizable;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn is_resizable(&self) -> bool {
|
||||
self.resizable
|
||||
}
|
||||
|
||||
/// Not resizable, just takes the size of its contents.
|
||||
pub fn auto_sized(self) -> Self {
|
||||
self.default_size(Vec2::splat(f32::INFINITY))
|
||||
.resizable(false)
|
||||
.auto_shrink_width(true)
|
||||
.auto_expand_width(true)
|
||||
.auto_shrink_height(true)
|
||||
.auto_expand_height(true)
|
||||
}
|
||||
|
||||
pub fn fixed_size(mut self, size: Vec2) -> Self {
|
||||
self.auto_shrink_width = false;
|
||||
self.auto_shrink_height = false;
|
||||
self.expand_width_to_fit_content = false;
|
||||
self.expand_height_to_fit_content = false;
|
||||
self.default_size = size;
|
||||
self.min_size = size;
|
||||
self.max_size = size;
|
||||
self.resizable = false;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn as_wide_as_possible(mut self) -> Self {
|
||||
self.min_size.x = f32::INFINITY;
|
||||
self
|
||||
}
|
||||
|
||||
/// true: prevent from resizing to smaller than contents.
|
||||
/// false: allow shrinking to smaller than contents.
|
||||
pub fn auto_expand(mut self, auto_expand: bool) -> Self {
|
||||
self.expand_width_to_fit_content = auto_expand;
|
||||
self.expand_height_to_fit_content = auto_expand;
|
||||
self
|
||||
}
|
||||
|
||||
/// true: prevent from resizing to smaller than contents.
|
||||
/// false: allow shrinking to smaller than contents.
|
||||
pub fn auto_expand_width(mut self, auto_expand: bool) -> Self {
|
||||
self.expand_width_to_fit_content = auto_expand;
|
||||
self
|
||||
}
|
||||
|
||||
/// true: prevent from resizing to smaller than contents.
|
||||
/// false: allow shrinking to smaller than contents.
|
||||
pub fn auto_expand_height(mut self, auto_expand: bool) -> Self {
|
||||
self.expand_height_to_fit_content = auto_expand;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn auto_shrink_width(mut self, auto_shrink_width: bool) -> Self {
|
||||
self.auto_shrink_width = auto_shrink_width;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn auto_shrink_height(mut self, auto_shrink_height: bool) -> Self {
|
||||
self.auto_shrink_height = auto_shrink_height;
|
||||
self
|
||||
}
|
||||
|
||||
/// Offset the position of the resize handle by this much
|
||||
pub fn handle_offset(mut self, handle_offset: Vec2) -> Self {
|
||||
self.handle_offset = handle_offset;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn outline(mut self, outline: bool) -> Self {
|
||||
self.outline = outline;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
struct Prepared {
|
||||
id: Id,
|
||||
state: State,
|
||||
is_new: bool,
|
||||
corner_interact: Option<InteractInfo>,
|
||||
content_ui: Ui,
|
||||
}
|
||||
|
||||
impl Resize {
|
||||
fn begin(&mut self, ui: &mut Ui) -> Prepared {
|
||||
let id = self.id.unwrap_or_else(|| ui.make_child_id("resize"));
|
||||
self.min_size = self.min_size.min(ui.available().size());
|
||||
self.max_size = self.max_size.min(ui.available().size());
|
||||
self.max_size = self.max_size.max(self.min_size);
|
||||
|
||||
let (is_new, mut state) = match ui.memory().resize.get(&id) {
|
||||
Some(state) => (false, *state),
|
||||
None => {
|
||||
let default_size = self.default_size.clamp(self.min_size..=self.max_size);
|
||||
(
|
||||
true,
|
||||
State {
|
||||
size: default_size,
|
||||
requested_size: None,
|
||||
},
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
state.size = state.size.clamp(self.min_size..=self.max_size);
|
||||
let last_frame_size = state.size;
|
||||
|
||||
let position = ui.available().min;
|
||||
|
||||
let corner_interact = if self.resizable {
|
||||
// Resize-corner:
|
||||
let corner_size = Vec2::splat(16.0); // TODO: style
|
||||
let corner_rect = Rect::from_min_size(
|
||||
position + state.size + self.handle_offset - corner_size,
|
||||
corner_size,
|
||||
);
|
||||
let corner_interact = ui.interact(corner_rect, id.with("corner"), Sense::drag());
|
||||
|
||||
if corner_interact.active {
|
||||
if let Some(mouse_pos) = ui.input().mouse.pos {
|
||||
// This is the desired size. We may not be able to achieve it.
|
||||
|
||||
state.size = mouse_pos - position + 0.5 * corner_interact.rect.size()
|
||||
- self.handle_offset;
|
||||
// We don't clamp to max size, because we want to be able to push against outer bounds.
|
||||
// For instance, if we are inside a bigger Resize region, we want to expand that.
|
||||
// state.size = state.size.clamp(self.min_size..=self.max_size);
|
||||
state.size = state.size.max(self.min_size);
|
||||
}
|
||||
}
|
||||
Some(corner_interact)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(requested_size) = state.requested_size.take() {
|
||||
state.size = requested_size;
|
||||
// We don't clamp to max size, because we want to be able to push against outer bounds.
|
||||
// For instance, if we are inside a bigger Resize region, we want to expand that.
|
||||
// state.size = state.size.clamp(self.min_size..=self.max_size);
|
||||
state.size = state.size.max(self.min_size);
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
|
||||
let inner_rect = Rect::from_min_size(position, state.size);
|
||||
|
||||
let mut content_clip_rect = ui
|
||||
.clip_rect()
|
||||
.intersect(inner_rect.expand(ui.style().clip_rect_margin));
|
||||
|
||||
// If we pull the resize handle to shrink, we want to TRY to shink it.
|
||||
// After laying out the contents, we might be much bigger.
|
||||
// In those cases we don't want the clip_rect to be smaller, because
|
||||
// then we will clip the contents of the region even thought the result gets larger. This is simply ugly!
|
||||
// So we use the memory of last_frame_size to make the clip rect large enough.
|
||||
content_clip_rect.max = content_clip_rect
|
||||
.max
|
||||
.max(content_clip_rect.min + last_frame_size)
|
||||
.min(ui.clip_rect().max); // Respect parent region
|
||||
|
||||
let mut content_ui = ui.child_ui(inner_rect);
|
||||
content_ui.set_clip_rect(content_clip_rect);
|
||||
|
||||
Prepared {
|
||||
id,
|
||||
state,
|
||||
is_new,
|
||||
corner_interact,
|
||||
content_ui,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn show<R>(mut self, ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) -> R {
|
||||
let mut prepared = self.begin(ui);
|
||||
let ret = add_contents(&mut prepared.content_ui);
|
||||
self.end(ui, prepared);
|
||||
ret
|
||||
}
|
||||
|
||||
fn end(self, ui: &mut Ui, prepared: Prepared) {
|
||||
let Prepared {
|
||||
id,
|
||||
mut state,
|
||||
is_new,
|
||||
corner_interact,
|
||||
content_ui,
|
||||
} = prepared;
|
||||
|
||||
let desired_size = content_ui.bounding_size();
|
||||
let desired_size = desired_size.ceil(); // Avoid rounding errors in math
|
||||
|
||||
// ------------------------------
|
||||
|
||||
if self.auto_shrink_width {
|
||||
state.size.x = state.size.x.min(desired_size.x);
|
||||
}
|
||||
if self.auto_shrink_height {
|
||||
state.size.y = state.size.y.min(desired_size.y);
|
||||
}
|
||||
if self.expand_width_to_fit_content || is_new {
|
||||
state.size.x = state.size.x.max(desired_size.x);
|
||||
}
|
||||
if self.expand_height_to_fit_content || is_new {
|
||||
state.size.y = state.size.y.max(desired_size.y);
|
||||
}
|
||||
|
||||
state.size = state.size.max(self.min_size);
|
||||
// state.size = state.size.clamp(self.min_size..=self.max_size);
|
||||
state.size = state.size.round(); // TODO: round to pixels
|
||||
|
||||
ui.allocate_space(state.size);
|
||||
|
||||
// ------------------------------
|
||||
|
||||
if self.outline && corner_interact.is_some() {
|
||||
let rect = Rect::from_min_size(content_ui.top_left(), state.size);
|
||||
let rect = rect.expand(2.0); // breathing room for content
|
||||
ui.add_paint_cmd(paint::PaintCmd::Rect {
|
||||
rect,
|
||||
corner_radius: 3.0,
|
||||
fill: None,
|
||||
outline: Some(ui.style().thin_outline),
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(corner_interact) = corner_interact {
|
||||
paint_resize_corner(ui, &corner_interact);
|
||||
|
||||
if corner_interact.hovered || corner_interact.active {
|
||||
ui.ctx().output().cursor_icon = CursorIcon::ResizeNwSe;
|
||||
}
|
||||
}
|
||||
|
||||
ui.memory().resize.insert(id, state);
|
||||
}
|
||||
}
|
||||
|
||||
fn paint_resize_corner(ui: &mut Ui, interact: &InteractInfo) {
|
||||
let color = ui.style().interact(interact).stroke_color;
|
||||
let width = ui.style().interact(interact).stroke_width;
|
||||
|
||||
let corner = ui.round_pos_to_pixels(interact.rect.right_bottom());
|
||||
let mut w = 2.0;
|
||||
|
||||
while w < 12.0 {
|
||||
ui.add_paint_cmd(paint::PaintCmd::line_segment(
|
||||
[pos2(corner.x - w, corner.y), pos2(corner.x, corner.y - w)],
|
||||
color,
|
||||
width,
|
||||
));
|
||||
w += 4.0;
|
||||
}
|
||||
}
|
||||
249
egui/src/containers/scroll_area.rs
Normal file
249
egui/src/containers/scroll_area.rs
Normal file
@@ -0,0 +1,249 @@
|
||||
use crate::*;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, serde_derive::Deserialize, serde_derive::Serialize)]
|
||||
#[serde(default)]
|
||||
pub(crate) struct State {
|
||||
/// Positive offset means scrolling down/right
|
||||
offset: Vec2,
|
||||
|
||||
show_scroll: bool, // TODO: default value?
|
||||
}
|
||||
|
||||
// TODO: rename VScroll
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ScrollArea {
|
||||
max_height: f32,
|
||||
always_show_scroll: bool,
|
||||
auto_hide_scroll: bool,
|
||||
}
|
||||
|
||||
impl Default for ScrollArea {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
max_height: 200.0,
|
||||
always_show_scroll: false,
|
||||
auto_hide_scroll: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ScrollArea {
|
||||
pub fn max_height(mut self, max_height: f32) -> Self {
|
||||
self.max_height = max_height;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn always_show_scroll(mut self, always_show_scroll: bool) -> Self {
|
||||
self.always_show_scroll = always_show_scroll;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn auto_hide_scroll(mut self, auto_hide_scroll: bool) -> Self {
|
||||
self.auto_hide_scroll = auto_hide_scroll;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
struct Prepared {
|
||||
id: Id,
|
||||
state: State,
|
||||
current_scroll_bar_width: f32,
|
||||
always_show_scroll: bool,
|
||||
inner_rect: Rect,
|
||||
content_ui: Ui,
|
||||
}
|
||||
|
||||
impl ScrollArea {
|
||||
fn begin(self, ui: &mut Ui) -> Prepared {
|
||||
let Self {
|
||||
max_height,
|
||||
always_show_scroll,
|
||||
auto_hide_scroll,
|
||||
} = self;
|
||||
|
||||
let ctx = ui.ctx().clone();
|
||||
|
||||
let id = ui.make_child_id("scroll_area");
|
||||
let state = ctx
|
||||
.memory()
|
||||
.scroll_areas
|
||||
.get(&id)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
|
||||
// content: size of contents (generally large)
|
||||
// outer: size of scroll area including scroll bar(s)
|
||||
// inner: excluding scroll bar(s). The area we clip the contents to.
|
||||
|
||||
let max_scroll_bar_width = 16.0;
|
||||
|
||||
let current_scroll_bar_width = if state.show_scroll || !auto_hide_scroll {
|
||||
max_scroll_bar_width // TODO: animate?
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
let outer_size = vec2(
|
||||
ui.available().width(),
|
||||
ui.available().height().min(max_height),
|
||||
);
|
||||
|
||||
let inner_size = outer_size - vec2(current_scroll_bar_width, 0.0);
|
||||
let inner_rect = Rect::from_min_size(ui.available().min, inner_size);
|
||||
|
||||
let mut content_ui = ui.child_ui(Rect::from_min_size(
|
||||
inner_rect.min - state.offset,
|
||||
vec2(inner_size.x, f32::INFINITY),
|
||||
));
|
||||
let mut content_clip_rect = ui.clip_rect().intersect(inner_rect);
|
||||
content_clip_rect.max.x = ui.clip_rect().max.x - current_scroll_bar_width; // Nice handling of forced resizing beyond the possible
|
||||
content_ui.set_clip_rect(content_clip_rect);
|
||||
|
||||
Prepared {
|
||||
id,
|
||||
state,
|
||||
always_show_scroll,
|
||||
inner_rect,
|
||||
current_scroll_bar_width,
|
||||
content_ui,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn show<R>(self, ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) -> R {
|
||||
let mut prepared = self.begin(ui);
|
||||
let ret = add_contents(&mut prepared.content_ui);
|
||||
prepared.end(ui);
|
||||
ret
|
||||
}
|
||||
}
|
||||
|
||||
impl Prepared {
|
||||
fn end(self, ui: &mut Ui) {
|
||||
let Prepared {
|
||||
id,
|
||||
mut state,
|
||||
inner_rect,
|
||||
always_show_scroll,
|
||||
current_scroll_bar_width,
|
||||
content_ui,
|
||||
} = self;
|
||||
|
||||
let content_size = content_ui.bounding_size();
|
||||
|
||||
let inner_rect = Rect::from_min_size(
|
||||
inner_rect.min,
|
||||
vec2(
|
||||
inner_rect.width().max(content_size.x), // Expand width to fit content
|
||||
inner_rect.height(),
|
||||
),
|
||||
);
|
||||
|
||||
let outer_rect = Rect::from_min_size(
|
||||
inner_rect.min,
|
||||
inner_rect.size() + vec2(current_scroll_bar_width, 0.0),
|
||||
);
|
||||
|
||||
let content_is_too_small = content_size.y > inner_rect.height();
|
||||
|
||||
if content_is_too_small {
|
||||
// Drag contents to scroll (for touch screens mostly):
|
||||
let content_interact = ui.interact(inner_rect, id.with("area"), Sense::drag());
|
||||
if content_interact.active {
|
||||
state.offset.y -= ui.input().mouse.delta.y;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: check that nothing else is being inteacted with
|
||||
if ui.contains_mouse(outer_rect) {
|
||||
state.offset.y -= ui.input().scroll_delta.y;
|
||||
}
|
||||
|
||||
let show_scroll_this_frame = content_is_too_small || always_show_scroll;
|
||||
if show_scroll_this_frame || state.show_scroll {
|
||||
let left = inner_rect.right() + 2.0;
|
||||
let right = outer_rect.right();
|
||||
let corner_radius = (right - left) / 2.0;
|
||||
let top = inner_rect.top();
|
||||
let bottom = inner_rect.bottom();
|
||||
|
||||
let outer_scroll_rect = Rect::from_min_max(
|
||||
pos2(left, inner_rect.top()),
|
||||
pos2(right, inner_rect.bottom()),
|
||||
);
|
||||
|
||||
let from_content =
|
||||
|content_y| remap_clamp(content_y, 0.0..=content_size.y, top..=bottom);
|
||||
|
||||
let handle_rect = Rect::from_min_max(
|
||||
pos2(left, from_content(state.offset.y)),
|
||||
pos2(right, from_content(state.offset.y + inner_rect.height())),
|
||||
);
|
||||
|
||||
// intentionally use same id for inside and outside of handle
|
||||
let interact_id = id.with("vertical");
|
||||
let mut interact = ui.interact(handle_rect, interact_id, Sense::click_and_drag());
|
||||
|
||||
if let Some(mouse_pos) = ui.input().mouse.pos {
|
||||
if interact.active {
|
||||
if inner_rect.top() <= mouse_pos.y && mouse_pos.y <= inner_rect.bottom() {
|
||||
state.offset.y +=
|
||||
ui.input().mouse.delta.y * content_size.y / inner_rect.height();
|
||||
}
|
||||
} else {
|
||||
// Check for mouse down outside handle:
|
||||
let scroll_bg_interact =
|
||||
ui.interact(outer_scroll_rect, interact_id, Sense::click_and_drag());
|
||||
|
||||
if scroll_bg_interact.active {
|
||||
// Center scroll at mouse pos:
|
||||
let mpos_top = mouse_pos.y - handle_rect.height() / 2.0;
|
||||
state.offset.y = remap(mpos_top, top..=bottom, 0.0..=content_size.y);
|
||||
}
|
||||
|
||||
interact = interact.union(scroll_bg_interact);
|
||||
}
|
||||
}
|
||||
|
||||
state.offset.y = state.offset.y.max(0.0);
|
||||
state.offset.y = state.offset.y.min(content_size.y - inner_rect.height());
|
||||
|
||||
// Avoid frame-delay by calculating a new handle rect:
|
||||
let handle_rect = Rect::from_min_max(
|
||||
pos2(left, from_content(state.offset.y)),
|
||||
pos2(right, from_content(state.offset.y + inner_rect.height())),
|
||||
);
|
||||
|
||||
let style = ui.style();
|
||||
let handle_fill = style.interact(&interact).fill;
|
||||
let handle_outline = style.interact(&interact).rect_outline;
|
||||
|
||||
ui.add_paint_cmd(paint::PaintCmd::Rect {
|
||||
rect: outer_scroll_rect,
|
||||
corner_radius,
|
||||
fill: Some(ui.style().dark_bg_color),
|
||||
outline: None,
|
||||
});
|
||||
|
||||
ui.add_paint_cmd(paint::PaintCmd::Rect {
|
||||
rect: handle_rect.expand(-2.0),
|
||||
corner_radius,
|
||||
fill: Some(handle_fill),
|
||||
outline: handle_outline,
|
||||
});
|
||||
}
|
||||
|
||||
// let size = content_size.min(inner_rect.size());
|
||||
// let size = vec2(
|
||||
// content_size.x, // ignore inner_rect, i.e. try to expand horizontally if necessary
|
||||
// content_size.y.min(inner_rect.size().y), // respect vertical height.
|
||||
// );
|
||||
let size = outer_rect.size();
|
||||
ui.allocate_space(size);
|
||||
|
||||
state.offset.y = state.offset.y.min(content_size.y - inner_rect.height());
|
||||
state.offset.y = state.offset.y.max(0.0);
|
||||
state.show_scroll = show_scroll_this_frame;
|
||||
|
||||
ui.memory().scroll_areas.insert(id, state);
|
||||
}
|
||||
}
|
||||
666
egui/src/containers/window.rs
Normal file
666
egui/src/containers/window.rs
Normal file
@@ -0,0 +1,666 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::{widgets::*, *};
|
||||
|
||||
use super::*;
|
||||
|
||||
/// A wrapper around other containers for things you often want in a window
|
||||
pub struct Window<'open> {
|
||||
pub title_label: Label,
|
||||
open: Option<&'open mut bool>,
|
||||
pub area: Area,
|
||||
pub frame: Option<Frame>,
|
||||
pub resize: Resize,
|
||||
pub scroll: Option<ScrollArea>,
|
||||
}
|
||||
|
||||
impl<'open> Window<'open> {
|
||||
// TODO: Into<Label>
|
||||
pub fn new(title: impl Into<String>) -> Self {
|
||||
let title = title.into();
|
||||
let area = Area::new(&title);
|
||||
let title_label = Label::new(title)
|
||||
.text_style(TextStyle::Heading)
|
||||
.multiline(false);
|
||||
Self {
|
||||
title_label,
|
||||
open: None,
|
||||
area,
|
||||
frame: None,
|
||||
resize: Resize::default()
|
||||
.auto_expand_height(false)
|
||||
.auto_expand_width(true)
|
||||
.auto_shrink_height(false)
|
||||
.auto_shrink_width(true)
|
||||
.handle_offset(Vec2::splat(4.0))
|
||||
.outline(false),
|
||||
scroll: Some(
|
||||
ScrollArea::default()
|
||||
.always_show_scroll(false)
|
||||
.max_height(f32::INFINITY),
|
||||
), // As large as we can be
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.
|
||||
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))`
|
||||
/// 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))`
|
||||
/// 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)))`
|
||||
/// 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
|
||||
}
|
||||
|
||||
pub fn default_size(mut self, default_size: Vec2) -> Self {
|
||||
self.resize = self.resize.default_size(default_size);
|
||||
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
|
||||
}
|
||||
|
||||
pub fn max_size(mut self, max_size: Vec2) -> Self {
|
||||
self.resize = self.resize.max_size(max_size);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn fixed_size(mut self, size: Vec2) -> Self {
|
||||
self.resize = self.resize.fixed_size(size);
|
||||
self
|
||||
}
|
||||
|
||||
/// Can you resize it with the mouse?
|
||||
/// Note that a window can still auto-resize
|
||||
pub fn resizable(mut self, resizable: bool) -> Self {
|
||||
self.resize = self.resize.resizable(resizable);
|
||||
self
|
||||
}
|
||||
|
||||
/// Not resizable, just takes the size of its contents.
|
||||
pub fn auto_sized(mut self) -> Self {
|
||||
self.resize = self.resize.auto_sized();
|
||||
self.scroll = None;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn scroll(mut self, scroll: bool) -> Self {
|
||||
if !scroll {
|
||||
self.scroll = None;
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'open> Window<'open> {
|
||||
pub fn show(
|
||||
self,
|
||||
ctx: &Arc<Context>,
|
||||
add_contents: impl FnOnce(&mut Ui),
|
||||
) -> Option<InteractInfo> {
|
||||
self.show_impl(ctx, Box::new(add_contents))
|
||||
}
|
||||
|
||||
fn show_impl<'c>(
|
||||
self,
|
||||
ctx: &Arc<Context>,
|
||||
add_contents: Box<dyn FnOnce(&mut Ui) + 'c>,
|
||||
) -> Option<InteractInfo> {
|
||||
let Window {
|
||||
title_label,
|
||||
open,
|
||||
area,
|
||||
frame,
|
||||
resize,
|
||||
scroll,
|
||||
} = self;
|
||||
|
||||
if matches!(open, Some(false)) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let window_id = Id::new(title_label.text());
|
||||
let area_layer = area.layer();
|
||||
let resize_id = window_id.with("resize");
|
||||
let collapsing_id = window_id.with("collapsing");
|
||||
|
||||
let possible = PossibleInteractions {
|
||||
movable: area.is_movable(),
|
||||
resizable: resize.is_resizable()
|
||||
&& collapsing_header::State::is_open(ctx, collapsing_id).unwrap_or_default(),
|
||||
};
|
||||
|
||||
let area = area.movable(false); // We move it manually
|
||||
let resize = resize.resizable(false); // We move it manually
|
||||
|
||||
let resize = resize.id(resize_id);
|
||||
|
||||
let frame = frame.unwrap_or_else(|| Frame::window(&ctx.style()));
|
||||
|
||||
let mut area = area.begin(ctx);
|
||||
|
||||
// First interact (move etc) to avoid frame delay:
|
||||
let last_frame_outer_rect = area.state().rect();
|
||||
let interaction = if possible.movable || possible.resizable {
|
||||
interact(
|
||||
ctx,
|
||||
possible,
|
||||
area_layer,
|
||||
area.state_mut(),
|
||||
window_id,
|
||||
resize_id,
|
||||
last_frame_outer_rect,
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let hover_interaction = resize_hover(ctx, possible, area_layer, last_frame_outer_rect);
|
||||
|
||||
let mut area_content_ui = area.content_ui(ctx);
|
||||
|
||||
{
|
||||
// BEGIN FRAME --------------------------------
|
||||
let mut frame = frame.begin(&mut area_content_ui);
|
||||
|
||||
let default_expanded = true;
|
||||
let mut collapsing = collapsing_header::State::from_memory_with_default_open(
|
||||
&mut frame.content_ui,
|
||||
collapsing_id,
|
||||
default_expanded,
|
||||
);
|
||||
let show_close_button = open.is_some();
|
||||
let title_bar = show_title_bar(
|
||||
&mut frame.content_ui,
|
||||
title_label,
|
||||
show_close_button,
|
||||
collapsing_id,
|
||||
&mut collapsing,
|
||||
);
|
||||
|
||||
let content_rect = collapsing
|
||||
.add_contents(&mut frame.content_ui, |ui| {
|
||||
resize.show(ui, |ui| {
|
||||
// Add some spacing (item_spacing) between title and content:
|
||||
ui.allocate_space(Vec2::zero());
|
||||
|
||||
if let Some(scroll) = scroll {
|
||||
scroll.show(ui, add_contents)
|
||||
} else {
|
||||
add_contents(ui)
|
||||
}
|
||||
})
|
||||
})
|
||||
.map(|ri| ri.1);
|
||||
|
||||
let outer_rect = frame.end(&mut area_content_ui);
|
||||
// END FRAME --------------------------------
|
||||
|
||||
title_bar.ui(
|
||||
&mut area_content_ui,
|
||||
outer_rect,
|
||||
content_rect,
|
||||
open,
|
||||
&mut collapsing,
|
||||
);
|
||||
|
||||
area_content_ui
|
||||
.memory()
|
||||
.collapsing_headers
|
||||
.insert(collapsing_id, collapsing);
|
||||
|
||||
if let Some(interaction) = interaction {
|
||||
paint_frame_interaction(
|
||||
&mut area_content_ui,
|
||||
outer_rect,
|
||||
interaction,
|
||||
ctx.style().interact.active,
|
||||
);
|
||||
} else {
|
||||
if let Some(hover_interaction) = hover_interaction {
|
||||
paint_frame_interaction(
|
||||
&mut area_content_ui,
|
||||
outer_rect,
|
||||
hover_interaction,
|
||||
ctx.style().interact.hovered,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
let full_interact = area.end(ctx, area_content_ui);
|
||||
|
||||
Some(full_interact)
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
struct PossibleInteractions {
|
||||
movable: bool,
|
||||
resizable: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub(crate) struct WindowInteraction {
|
||||
pub(crate) area_layer: Layer,
|
||||
pub(crate) start_rect: Rect,
|
||||
pub(crate) left: bool,
|
||||
pub(crate) right: bool,
|
||||
pub(crate) top: bool,
|
||||
pub(crate) bottom: bool,
|
||||
}
|
||||
|
||||
impl WindowInteraction {
|
||||
pub fn set_cursor(&self, ctx: &Context) {
|
||||
if (self.left && self.top) || (self.right && self.bottom) {
|
||||
ctx.output().cursor_icon = CursorIcon::ResizeNwSe;
|
||||
} else if (self.right && self.top) || (self.left && self.bottom) {
|
||||
ctx.output().cursor_icon = CursorIcon::ResizeNeSw;
|
||||
} else if self.left || self.right {
|
||||
ctx.output().cursor_icon = CursorIcon::ResizeHorizontal;
|
||||
} else if self.bottom || self.top {
|
||||
ctx.output().cursor_icon = CursorIcon::ResizeVertical;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_resize(&self) -> bool {
|
||||
self.left || self.right || self.top || self.bottom
|
||||
}
|
||||
|
||||
pub fn is_pure_move(&self) -> bool {
|
||||
!self.is_resize()
|
||||
}
|
||||
}
|
||||
|
||||
fn interact(
|
||||
ctx: &Context,
|
||||
possible: PossibleInteractions,
|
||||
area_layer: Layer,
|
||||
area_state: &mut area::State,
|
||||
window_id: Id,
|
||||
resize_id: Id,
|
||||
rect: Rect,
|
||||
) -> Option<WindowInteraction> {
|
||||
let pre_resize = ctx.round_rect_to_pixels(rect);
|
||||
let window_interaction = window_interaction(
|
||||
ctx,
|
||||
possible,
|
||||
area_layer,
|
||||
window_id.with("frame_resize"),
|
||||
rect,
|
||||
)?;
|
||||
let new_rect = resize_window(ctx, &window_interaction)?;
|
||||
|
||||
let new_rect = ctx.round_rect_to_pixels(new_rect);
|
||||
// TODO: add this to a Window state instead as a command "move here next frame"
|
||||
|
||||
area_state.pos = new_rect.min;
|
||||
|
||||
let mut resize_state = ctx.memory().resize.get(&resize_id).cloned().unwrap();
|
||||
// resize_state.size += new_rect.size() - pre_resize.size();
|
||||
// resize_state.size = new_rect.size() - some margin;
|
||||
resize_state.requested_size = Some(resize_state.size + new_rect.size() - pre_resize.size());
|
||||
ctx.memory().resize.insert(resize_id, resize_state);
|
||||
|
||||
ctx.memory().areas.move_to_top(area_layer);
|
||||
Some(window_interaction)
|
||||
}
|
||||
|
||||
fn resize_window(ctx: &Context, window_interaction: &WindowInteraction) -> Option<Rect> {
|
||||
window_interaction.set_cursor(ctx);
|
||||
let mouse_pos = ctx.input().mouse.pos?;
|
||||
let mut rect = window_interaction.start_rect; // prevent drift
|
||||
|
||||
if window_interaction.is_resize() {
|
||||
if window_interaction.left {
|
||||
rect.min.x = ctx.round_to_pixel(mouse_pos.x);
|
||||
} else if window_interaction.right {
|
||||
rect.max.x = ctx.round_to_pixel(mouse_pos.x);
|
||||
}
|
||||
|
||||
if window_interaction.top {
|
||||
rect.min.y = ctx.round_to_pixel(mouse_pos.y);
|
||||
} else if window_interaction.bottom {
|
||||
rect.max.y = ctx.round_to_pixel(mouse_pos.y);
|
||||
}
|
||||
} else {
|
||||
// movevement
|
||||
rect = rect.translate(mouse_pos - ctx.input().mouse.press_origin?);
|
||||
}
|
||||
|
||||
return Some(rect);
|
||||
}
|
||||
|
||||
fn window_interaction(
|
||||
ctx: &Context,
|
||||
possible: PossibleInteractions,
|
||||
area_layer: Layer,
|
||||
id: Id,
|
||||
rect: Rect,
|
||||
) -> Option<WindowInteraction> {
|
||||
{
|
||||
let drag_id = ctx.memory().interaction.drag_id;
|
||||
|
||||
if drag_id.is_some() && drag_id != Some(id) {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
let mut window_interaction = { ctx.memory().window_interaction.clone() };
|
||||
|
||||
if window_interaction.is_none() {
|
||||
if let Some(hover_window_interaction) = resize_hover(ctx, possible, area_layer, rect) {
|
||||
hover_window_interaction.set_cursor(ctx);
|
||||
if ctx.input().mouse.pressed {
|
||||
ctx.memory().interaction.drag_id = Some(id);
|
||||
ctx.memory().interaction.drag_is_window = true;
|
||||
window_interaction = Some(hover_window_interaction);
|
||||
ctx.memory().window_interaction = window_interaction;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(window_interaction) = window_interaction {
|
||||
let is_active = ctx.memory().interaction.drag_id == Some(id);
|
||||
|
||||
if is_active && window_interaction.area_layer == area_layer {
|
||||
return Some(window_interaction);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn resize_hover(
|
||||
ctx: &Context,
|
||||
possible: PossibleInteractions,
|
||||
area_layer: Layer,
|
||||
rect: Rect,
|
||||
) -> Option<WindowInteraction> {
|
||||
if let Some(mouse_pos) = ctx.input().mouse.pos {
|
||||
if let Some(top_layer) = ctx.memory().layer_at(mouse_pos) {
|
||||
if top_layer != area_layer && top_layer.order != Order::Background {
|
||||
return None; // Another window is on top here
|
||||
}
|
||||
}
|
||||
|
||||
if ctx.memory().interaction.drag_interest {
|
||||
// Another widget will become active if we drag here
|
||||
return None;
|
||||
}
|
||||
|
||||
let side_interact_radius = 5.0; // TODO: from style
|
||||
let corner_interact_radius = 10.0; // TODO
|
||||
if rect.expand(side_interact_radius).contains(mouse_pos) {
|
||||
let (mut left, mut right, mut top, mut bottom) = Default::default();
|
||||
if possible.resizable {
|
||||
right = (rect.right() - mouse_pos.x).abs() <= side_interact_radius;
|
||||
bottom = (rect.bottom() - mouse_pos.y).abs() <= side_interact_radius;
|
||||
|
||||
if rect.right_bottom().distance(mouse_pos) < corner_interact_radius {
|
||||
right = true;
|
||||
bottom = true;
|
||||
}
|
||||
|
||||
if possible.movable {
|
||||
left = (rect.left() - mouse_pos.x).abs() <= side_interact_radius;
|
||||
top = (rect.top() - mouse_pos.y).abs() <= side_interact_radius;
|
||||
|
||||
if rect.right_top().distance(mouse_pos) < corner_interact_radius {
|
||||
right = true;
|
||||
top = true;
|
||||
}
|
||||
if rect.left_top().distance(mouse_pos) < corner_interact_radius {
|
||||
left = true;
|
||||
top = true;
|
||||
}
|
||||
if rect.left_bottom().distance(mouse_pos) < corner_interact_radius {
|
||||
left = true;
|
||||
bottom = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
let any_resize = left || right || top || bottom;
|
||||
|
||||
if !any_resize && !possible.movable {
|
||||
return None;
|
||||
}
|
||||
|
||||
if any_resize || possible.movable {
|
||||
Some(WindowInteraction {
|
||||
area_layer,
|
||||
start_rect: rect,
|
||||
left,
|
||||
right,
|
||||
top,
|
||||
bottom,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Fill in parts of the window frame when we resize by dragging that part
|
||||
fn paint_frame_interaction(
|
||||
ui: &mut Ui,
|
||||
rect: Rect,
|
||||
interaction: WindowInteraction,
|
||||
style: style::WidgetStyle,
|
||||
) {
|
||||
let cr = ui.style().window.corner_radius;
|
||||
let Rect { min, max } = rect;
|
||||
|
||||
let mut path = Path::default();
|
||||
|
||||
if interaction.right && !interaction.bottom && !interaction.top {
|
||||
path.add_line_segment([pos2(max.x, min.y + cr), pos2(max.x, max.y - cr)]);
|
||||
}
|
||||
if interaction.right && interaction.bottom {
|
||||
path.add_line_segment([pos2(max.x, min.y + cr), pos2(max.x, max.y - cr)]);
|
||||
path.add_circle_quadrant(pos2(max.x - cr, max.y - cr), cr, 0.0);
|
||||
}
|
||||
if interaction.bottom {
|
||||
path.add_line_segment([pos2(max.x - cr, max.y), pos2(min.x + cr, max.y)]);
|
||||
}
|
||||
if interaction.left && interaction.bottom {
|
||||
path.add_circle_quadrant(pos2(min.x + cr, max.y - cr), cr, 1.0);
|
||||
}
|
||||
if interaction.left {
|
||||
path.add_line_segment([pos2(min.x, max.y - cr), pos2(min.x, min.y + cr)]);
|
||||
}
|
||||
if interaction.left && interaction.top {
|
||||
path.add_circle_quadrant(pos2(min.x + cr, min.y + cr), cr, 2.0);
|
||||
}
|
||||
if interaction.top {
|
||||
path.add_line_segment([pos2(min.x + cr, min.y), pos2(max.x - cr, min.y)]);
|
||||
}
|
||||
if interaction.right && interaction.top {
|
||||
path.add_circle_quadrant(pos2(max.x - cr, min.y + cr), cr, 3.0);
|
||||
path.add_line_segment([pos2(max.x, min.y + cr), pos2(max.x, max.y - cr)]);
|
||||
}
|
||||
ui.add_paint_cmd(PaintCmd::Path {
|
||||
path,
|
||||
closed: false,
|
||||
fill: None,
|
||||
outline: style.rect_outline,
|
||||
});
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
struct TitleBar {
|
||||
title_label: Label,
|
||||
title_galley: font::Galley,
|
||||
title_rect: Rect,
|
||||
rect: Rect,
|
||||
}
|
||||
|
||||
fn show_title_bar(
|
||||
ui: &mut Ui,
|
||||
title_label: Label,
|
||||
show_close_button: bool,
|
||||
collapsing_id: Id,
|
||||
collapsing: &mut collapsing_header::State,
|
||||
) -> TitleBar {
|
||||
let title_bar_and_rect = ui.inner_layout(Layout::horizontal(Align::Center), |ui| {
|
||||
ui.set_desired_height(title_label.font_height(ui));
|
||||
|
||||
let item_spacing = ui.style().item_spacing;
|
||||
let button_size = ui.style().start_icon_width;
|
||||
|
||||
{
|
||||
// TODO: make clickable radius larger
|
||||
ui.allocate_space(vec2(0.0, 0.0)); // HACK: will add left spacing
|
||||
|
||||
let rect = ui.allocate_space(Vec2::splat(button_size));
|
||||
let collapse_button_interact = ui.interact(rect, collapsing_id, Sense::click());
|
||||
if collapse_button_interact.clicked {
|
||||
collapsing.toggle(ui);
|
||||
}
|
||||
collapsing.paint_icon(ui, &collapse_button_interact);
|
||||
}
|
||||
|
||||
let title_galley = title_label.layout(ui);
|
||||
let title_rect = ui.allocate_space(title_galley.size);
|
||||
|
||||
if show_close_button {
|
||||
// Reserve space for close button which will be added later:
|
||||
let close_max_x = title_rect.right() + item_spacing.x + button_size + item_spacing.x;
|
||||
let close_max_x = close_max_x.max(ui.rect_finite().right());
|
||||
let close_rect = Rect::from_min_size(
|
||||
pos2(
|
||||
close_max_x - button_size,
|
||||
title_rect.center().y - 0.5 * button_size,
|
||||
),
|
||||
Vec2::splat(button_size),
|
||||
);
|
||||
ui.expand_to_include_child(close_rect);
|
||||
}
|
||||
|
||||
TitleBar {
|
||||
title_label,
|
||||
title_galley,
|
||||
title_rect,
|
||||
rect: Default::default(), // Will be filled in later
|
||||
}
|
||||
});
|
||||
|
||||
TitleBar {
|
||||
rect: title_bar_and_rect.1,
|
||||
..title_bar_and_rect.0
|
||||
}
|
||||
}
|
||||
|
||||
impl TitleBar {
|
||||
fn ui(
|
||||
mut self,
|
||||
ui: &mut Ui,
|
||||
outer_rect: Rect,
|
||||
content_rect: Option<Rect>,
|
||||
open: Option<&mut bool>,
|
||||
collapsing: &mut collapsing_header::State,
|
||||
) {
|
||||
if let Some(content_rect) = content_rect {
|
||||
// Now we know how large we got to be:
|
||||
self.rect.max.x = content_rect.max.x;
|
||||
}
|
||||
|
||||
if let Some(open) = open {
|
||||
// Add close button now that we know our full width:
|
||||
if self.close_button_ui(ui).clicked {
|
||||
*open = false;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: pick style for title based on move interaction
|
||||
self.title_label
|
||||
.paint_galley(ui, self.title_rect.min, self.title_galley);
|
||||
|
||||
if let Some(content_rect) = content_rect {
|
||||
// paint separator between title and content:
|
||||
let left = outer_rect.left();
|
||||
let right = outer_rect.right();
|
||||
let y = content_rect.top() + ui.style().item_spacing.y * 0.5;
|
||||
ui.add_paint_cmd(PaintCmd::LineSegment {
|
||||
points: [pos2(left, y), pos2(right, y)],
|
||||
style: ui.style().interact.inactive.rect_outline.unwrap(),
|
||||
});
|
||||
}
|
||||
|
||||
let title_bar_id = ui.make_child_id("title_bar");
|
||||
if ui
|
||||
.interact(self.rect, title_bar_id, Sense::click())
|
||||
.double_clicked
|
||||
{
|
||||
collapsing.toggle(ui);
|
||||
}
|
||||
}
|
||||
|
||||
fn close_button_ui(&self, ui: &mut Ui) -> InteractInfo {
|
||||
let button_size = ui.style().start_icon_width;
|
||||
let button_rect = Rect::from_min_size(
|
||||
pos2(
|
||||
self.rect.right() - ui.style().item_spacing.x - button_size,
|
||||
self.rect.center().y - 0.5 * button_size,
|
||||
),
|
||||
Vec2::splat(button_size),
|
||||
);
|
||||
|
||||
close_button(ui, button_rect)
|
||||
}
|
||||
}
|
||||
|
||||
fn close_button(ui: &mut Ui, rect: Rect) -> InteractInfo {
|
||||
let close_id = ui.make_child_id("window_close_button");
|
||||
let interact = ui.interact(rect, close_id, Sense::click());
|
||||
ui.expand_to_include_child(interact.rect);
|
||||
|
||||
let stroke_color = ui.style().interact(&interact).stroke_color;
|
||||
let stroke_width = ui.style().interact(&interact).stroke_width;
|
||||
ui.add_paint_cmd(PaintCmd::line_segment(
|
||||
[rect.left_top(), rect.right_bottom()],
|
||||
stroke_color,
|
||||
stroke_width,
|
||||
));
|
||||
ui.add_paint_cmd(PaintCmd::line_segment(
|
||||
[rect.right_top(), rect.left_bottom()],
|
||||
stroke_color,
|
||||
stroke_width,
|
||||
));
|
||||
interact
|
||||
}
|
||||
610
egui/src/context.rs
Normal file
610
egui/src/context.rs
Normal file
@@ -0,0 +1,610 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use {ahash::AHashMap, parking_lot::Mutex};
|
||||
|
||||
use crate::{layout::align_rect, paint::*, *};
|
||||
|
||||
#[derive(Clone, Copy, Default)]
|
||||
struct PaintStats {
|
||||
num_batches: usize,
|
||||
num_primitives: usize,
|
||||
num_vertices: usize,
|
||||
num_triangles: usize,
|
||||
}
|
||||
|
||||
/// Contains the input, style and output of all GUI commands.
|
||||
/// `Ui`:s keep an Arc pointer to this.
|
||||
/// This allows us to create several child `Ui`:s at once,
|
||||
/// all working against the same shared Context.
|
||||
pub struct Context {
|
||||
/// The default style for new `Ui`:s
|
||||
style: Mutex<Style>,
|
||||
paint_options: Mutex<paint::PaintOptions>,
|
||||
fonts: Arc<Fonts>,
|
||||
/// HACK: set a new font next frame
|
||||
new_fonts: Mutex<Option<Arc<Fonts>>>,
|
||||
memory: Arc<Mutex<Memory>>,
|
||||
|
||||
input: InputState,
|
||||
|
||||
// The output of a frame:
|
||||
graphics: Mutex<GraphicLayers>,
|
||||
output: Mutex<Output>,
|
||||
/// Used to debug name clashes of e.g. windows
|
||||
used_ids: Mutex<AHashMap<Id, Pos2>>,
|
||||
|
||||
paint_stats: Mutex<PaintStats>,
|
||||
}
|
||||
|
||||
impl Clone for Context {
|
||||
fn clone(&self) -> Self {
|
||||
Context {
|
||||
style: Mutex::new(self.style()),
|
||||
paint_options: Mutex::new(*self.paint_options.lock()),
|
||||
fonts: self.fonts.clone(),
|
||||
new_fonts: Mutex::new(self.new_fonts.lock().clone()),
|
||||
memory: self.memory.clone(),
|
||||
input: self.input.clone(),
|
||||
graphics: Mutex::new(self.graphics.lock().clone()),
|
||||
output: Mutex::new(self.output.lock().clone()),
|
||||
used_ids: Mutex::new(self.used_ids.lock().clone()),
|
||||
paint_stats: Mutex::new(*self.paint_stats.lock()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Context {
|
||||
pub fn new(pixels_per_point: f32) -> Arc<Context> {
|
||||
Arc::new(Context {
|
||||
style: Default::default(),
|
||||
paint_options: Default::default(),
|
||||
fonts: Arc::new(Fonts::new(pixels_per_point)),
|
||||
new_fonts: Default::default(),
|
||||
memory: Default::default(),
|
||||
|
||||
input: Default::default(),
|
||||
|
||||
graphics: Default::default(),
|
||||
output: Default::default(),
|
||||
used_ids: Default::default(),
|
||||
paint_stats: Default::default(),
|
||||
})
|
||||
}
|
||||
|
||||
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.try_lock().expect("memory already locked")
|
||||
}
|
||||
|
||||
pub fn graphics(&self) -> parking_lot::MutexGuard<'_, GraphicLayers> {
|
||||
self.graphics.try_lock().expect("graphics already locked")
|
||||
}
|
||||
|
||||
pub fn output(&self) -> parking_lot::MutexGuard<'_, Output> {
|
||||
self.output.try_lock().expect("output already locked")
|
||||
}
|
||||
|
||||
pub fn input(&self) -> &InputState {
|
||||
&self.input
|
||||
}
|
||||
|
||||
pub fn fonts(&self) -> &Fonts {
|
||||
&*self.fonts
|
||||
}
|
||||
|
||||
pub fn texture(&self) -> &paint::Texture {
|
||||
self.fonts().texture()
|
||||
}
|
||||
|
||||
/// Will become active next frame
|
||||
pub fn set_fonts(&self, fonts: Fonts) {
|
||||
*self.new_fonts.lock() = Some(Arc::new(fonts));
|
||||
}
|
||||
|
||||
// TODO: return MutexGuard
|
||||
pub fn style(&self) -> Style {
|
||||
*self.style.try_lock().expect("style already locked")
|
||||
}
|
||||
|
||||
pub fn set_style(&self, style: Style) {
|
||||
*self.style.try_lock().expect("style already locked") = style;
|
||||
}
|
||||
|
||||
pub fn pixels_per_point(&self) -> f32 {
|
||||
self.input.pixels_per_point
|
||||
}
|
||||
|
||||
/// Useful for pixel-perfect rendering
|
||||
pub fn round_to_pixel(&self, point: f32) -> f32 {
|
||||
(point * self.input.pixels_per_point).round() / self.input.pixels_per_point
|
||||
}
|
||||
|
||||
pub fn round_pos_to_pixels(&self, pos: Pos2) -> Pos2 {
|
||||
pos2(self.round_to_pixel(pos.x), self.round_to_pixel(pos.y))
|
||||
}
|
||||
|
||||
pub fn round_vec_to_pixels(&self, vec: Vec2) -> Vec2 {
|
||||
vec2(self.round_to_pixel(vec.x), self.round_to_pixel(vec.y))
|
||||
}
|
||||
|
||||
pub fn round_rect_to_pixels(&self, rect: Rect) -> Rect {
|
||||
Rect {
|
||||
min: self.round_pos_to_pixels(rect.min),
|
||||
max: self.round_pos_to_pixels(rect.max),
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
pub fn begin_frame(self: &mut Arc<Self>, new_input: RawInput) {
|
||||
let mut self_: Self = (**self).clone();
|
||||
self_.begin_frame_mut(new_input);
|
||||
*self = Arc::new(self_);
|
||||
}
|
||||
|
||||
fn begin_frame_mut(&mut self, new_raw_input: RawInput) {
|
||||
self.memory().begin_frame(&self.input);
|
||||
|
||||
self.used_ids.lock().clear();
|
||||
|
||||
if let Some(new_fonts) = self.new_fonts.lock().take() {
|
||||
self.fonts = new_fonts;
|
||||
}
|
||||
|
||||
self.input = std::mem::take(&mut self.input).begin_frame(new_raw_input);
|
||||
}
|
||||
|
||||
pub fn end_frame(&self) -> (Output, PaintBatches) {
|
||||
self.memory().end_frame();
|
||||
let output: Output = std::mem::take(&mut self.output());
|
||||
let paint_batches = self.paint();
|
||||
(output, paint_batches)
|
||||
}
|
||||
|
||||
fn drain_paint_lists(&self) -> Vec<(Rect, PaintCmd)> {
|
||||
let memory = self.memory();
|
||||
self.graphics().drain(memory.areas.order()).collect()
|
||||
}
|
||||
|
||||
fn paint(&self) -> PaintBatches {
|
||||
let mut paint_options = *self.paint_options.lock();
|
||||
paint_options.aa_size = 1.0 / self.pixels_per_point();
|
||||
paint_options.aa_size *= 1.5; // Looks better, but TODO: should not be needed
|
||||
let paint_commands = self.drain_paint_lists();
|
||||
let num_primitives = paint_commands.len();
|
||||
let batches =
|
||||
mesher::paint_commands_into_triangles(paint_options, self.fonts(), paint_commands);
|
||||
|
||||
{
|
||||
let mut stats = PaintStats::default();
|
||||
stats.num_batches = batches.len();
|
||||
stats.num_primitives = num_primitives;
|
||||
for (_, triangles) in &batches {
|
||||
stats.num_vertices += triangles.vertices.len();
|
||||
stats.num_triangles += triangles.indices.len() / 3;
|
||||
}
|
||||
*self.paint_stats.lock() = stats;
|
||||
}
|
||||
|
||||
batches
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
/// A `Ui` for the entire screen, behind any windows.
|
||||
pub fn fullscreen_ui(self: &Arc<Self>) -> Ui {
|
||||
let rect = Rect::from_min_size(Default::default(), self.input().screen_size);
|
||||
let id = Id::background();
|
||||
let layer = Layer {
|
||||
order: Order::Background,
|
||||
id,
|
||||
};
|
||||
// Ensure we register the background area so it is painted:
|
||||
self.memory().areas.set_state(
|
||||
layer,
|
||||
containers::area::State {
|
||||
pos: rect.min,
|
||||
size: rect.size(),
|
||||
interactable: true,
|
||||
vel: Default::default(),
|
||||
},
|
||||
);
|
||||
Ui::new(self.clone(), layer, id, rect)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
/// Generate a id from the given source.
|
||||
/// If it is not unique, an error will be printed at the given position.
|
||||
pub fn make_unique_id<IdSource>(&self, source: IdSource, pos: Pos2) -> Id
|
||||
where
|
||||
IdSource: std::hash::Hash + std::fmt::Debug + Copy,
|
||||
{
|
||||
self.register_unique_id(Id::new(source), source, pos)
|
||||
}
|
||||
|
||||
/// If the given Id is not unique, an error will be printed at the given position.
|
||||
pub fn register_unique_id(&self, id: Id, source_name: impl std::fmt::Debug, pos: Pos2) -> Id {
|
||||
if let Some(clash_pos) = self.used_ids.lock().insert(id, pos) {
|
||||
if clash_pos.distance(pos) < 4.0 {
|
||||
self.show_error(
|
||||
pos,
|
||||
&format!("use of non-unique ID {:?} (name clash?)", source_name),
|
||||
);
|
||||
} else {
|
||||
self.show_error(
|
||||
clash_pos,
|
||||
&format!("first use of non-unique ID {:?} (name clash?)", source_name),
|
||||
);
|
||||
self.show_error(
|
||||
pos,
|
||||
&format!(
|
||||
"second use of non-unique ID {:?} (name clash?)",
|
||||
source_name
|
||||
),
|
||||
);
|
||||
}
|
||||
id
|
||||
} else {
|
||||
id
|
||||
}
|
||||
}
|
||||
|
||||
pub fn contains_mouse(&self, layer: Layer, clip_rect: Rect, rect: Rect) -> bool {
|
||||
let rect = rect.intersect(clip_rect);
|
||||
if let Some(mouse_pos) = self.input.mouse.pos {
|
||||
rect.contains(mouse_pos) && self.memory().layer_at(mouse_pos) == Some(layer)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn interact(
|
||||
&self,
|
||||
layer: Layer,
|
||||
clip_rect: Rect,
|
||||
rect: Rect,
|
||||
interaction_id: Option<Id>,
|
||||
sense: Sense,
|
||||
) -> InteractInfo {
|
||||
let interact_rect = rect.expand2(0.5 * self.style().item_spacing); // make it easier to click. TODO: nice way to do this
|
||||
let hovered = self.contains_mouse(layer, clip_rect, interact_rect);
|
||||
|
||||
if interaction_id.is_none() || sense == Sense::nothing() {
|
||||
// Not interested in input:
|
||||
return InteractInfo {
|
||||
rect,
|
||||
hovered,
|
||||
clicked: false,
|
||||
double_clicked: false,
|
||||
active: false,
|
||||
};
|
||||
}
|
||||
let interaction_id = interaction_id.unwrap();
|
||||
|
||||
let mut memory = self.memory();
|
||||
|
||||
memory.interaction.click_interest |= hovered && sense.click;
|
||||
memory.interaction.drag_interest |= hovered && sense.drag;
|
||||
|
||||
let active = memory.interaction.click_id == Some(interaction_id)
|
||||
|| memory.interaction.drag_id == Some(interaction_id);
|
||||
|
||||
if self.input.mouse.pressed {
|
||||
if hovered {
|
||||
let mut info = InteractInfo {
|
||||
rect,
|
||||
hovered: true,
|
||||
clicked: false,
|
||||
double_clicked: false,
|
||||
active: false,
|
||||
};
|
||||
|
||||
if sense.click && !memory.interaction.click_id.is_some() {
|
||||
// start of a click
|
||||
memory.interaction.click_id = Some(interaction_id);
|
||||
info.active = true;
|
||||
}
|
||||
|
||||
if sense.drag
|
||||
&& (!memory.interaction.drag_id.is_some() || memory.interaction.drag_is_window)
|
||||
{
|
||||
// start of a drag
|
||||
memory.interaction.drag_id = Some(interaction_id);
|
||||
memory.interaction.drag_is_window = false;
|
||||
memory.window_interaction = None; // HACK: stop moving windows (if any)
|
||||
info.active = true;
|
||||
}
|
||||
|
||||
info
|
||||
} else {
|
||||
// miss
|
||||
InteractInfo {
|
||||
rect,
|
||||
hovered,
|
||||
clicked: false,
|
||||
double_clicked: false,
|
||||
active: false,
|
||||
}
|
||||
}
|
||||
} else if self.input.mouse.released {
|
||||
let clicked = hovered && active;
|
||||
InteractInfo {
|
||||
rect,
|
||||
hovered,
|
||||
clicked,
|
||||
double_clicked: clicked && self.input.mouse.double_click,
|
||||
active,
|
||||
}
|
||||
} else if self.input.mouse.down {
|
||||
InteractInfo {
|
||||
rect,
|
||||
hovered: hovered && active,
|
||||
clicked: false,
|
||||
double_clicked: false,
|
||||
active,
|
||||
}
|
||||
} else {
|
||||
InteractInfo {
|
||||
rect,
|
||||
hovered,
|
||||
clicked: false,
|
||||
double_clicked: false,
|
||||
active,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
pub fn show_error(&self, pos: Pos2, text: impl Into<String>) {
|
||||
let text = text.into();
|
||||
let align = (Align::Min, Align::Min);
|
||||
let layer = Layer::debug();
|
||||
let text_style = TextStyle::Monospace;
|
||||
let font = &self.fonts[text_style];
|
||||
let galley = font.layout_multiline(text, f32::INFINITY);
|
||||
let rect = align_rect(Rect::from_min_size(pos, galley.size), align);
|
||||
self.add_paint_cmd(
|
||||
layer,
|
||||
PaintCmd::Rect {
|
||||
corner_radius: 0.0,
|
||||
fill: Some(color::gray(0, 240)),
|
||||
outline: Some(LineStyle::new(1.0, color::RED)),
|
||||
rect: rect.expand(2.0),
|
||||
},
|
||||
);
|
||||
self.add_galley(layer, rect.min, galley, text_style, Some(color::RED));
|
||||
}
|
||||
|
||||
pub fn debug_text(&self, pos: Pos2, text: impl Into<String>) {
|
||||
let text = text.into();
|
||||
let layer = Layer::debug();
|
||||
let align = (Align::Min, Align::Min);
|
||||
self.floating_text(
|
||||
layer,
|
||||
pos,
|
||||
text,
|
||||
TextStyle::Monospace,
|
||||
align,
|
||||
Some(color::YELLOW),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn debug_rect(&self, rect: Rect, text: impl Into<String>) {
|
||||
let text = text.into();
|
||||
let layer = Layer::debug();
|
||||
self.add_paint_cmd(
|
||||
layer,
|
||||
PaintCmd::Rect {
|
||||
corner_radius: 0.0,
|
||||
fill: None,
|
||||
outline: Some(LineStyle::new(1.0, color::RED)),
|
||||
rect,
|
||||
},
|
||||
);
|
||||
let align = (Align::Min, Align::Min);
|
||||
let text_style = TextStyle::Monospace;
|
||||
self.floating_text(layer, rect.min, text, text_style, align, Some(color::RED));
|
||||
}
|
||||
|
||||
/// Show some text anywhere on screen.
|
||||
/// To center the text at the given position, use `align: (Center, Center)`.
|
||||
pub fn floating_text(
|
||||
&self,
|
||||
layer: Layer,
|
||||
pos: Pos2,
|
||||
text: String,
|
||||
text_style: TextStyle,
|
||||
align: (Align, Align),
|
||||
text_color: Option<Color>,
|
||||
) -> Rect {
|
||||
let font = &self.fonts[text_style];
|
||||
let galley = font.layout_multiline(text, f32::INFINITY);
|
||||
let rect = align_rect(Rect::from_min_size(pos, galley.size), align);
|
||||
self.add_galley(layer, rect.min, galley, text_style, text_color);
|
||||
rect
|
||||
}
|
||||
|
||||
/// Already layed out text.
|
||||
pub fn add_galley(
|
||||
&self,
|
||||
layer: Layer,
|
||||
pos: Pos2,
|
||||
galley: font::Galley,
|
||||
text_style: TextStyle,
|
||||
color: Option<Color>,
|
||||
) {
|
||||
let color = color.unwrap_or_else(|| self.style().text_color);
|
||||
self.add_paint_cmd(
|
||||
layer,
|
||||
PaintCmd::Text {
|
||||
pos,
|
||||
galley,
|
||||
text_style,
|
||||
color,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
pub fn add_paint_cmd(&self, layer: Layer, paint_cmd: PaintCmd) {
|
||||
self.graphics()
|
||||
.layer(layer)
|
||||
.push((Rect::everything(), paint_cmd))
|
||||
}
|
||||
}
|
||||
|
||||
impl Context {
|
||||
pub fn settings_ui(&self, ui: &mut Ui) {
|
||||
use crate::containers::*;
|
||||
|
||||
CollapsingHeader::new("Style")
|
||||
.default_open(false)
|
||||
.show(ui, |ui| {
|
||||
self.paint_options.lock().ui(ui);
|
||||
self.style_ui(ui);
|
||||
});
|
||||
|
||||
CollapsingHeader::new("Fonts")
|
||||
.default_open(false)
|
||||
.show(ui, |ui| {
|
||||
let old_font_definitions = self.fonts().definitions();
|
||||
let mut new_font_definitions = old_font_definitions.clone();
|
||||
font_definitions_ui(&mut new_font_definitions, ui);
|
||||
self.fonts().texture().ui(ui);
|
||||
if *old_font_definitions != new_font_definitions {
|
||||
let fonts = Fonts::from_definitions(
|
||||
new_font_definitions,
|
||||
self.input().pixels_per_point,
|
||||
);
|
||||
self.set_fonts(fonts);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn inspection_ui(&self, ui: &mut Ui) {
|
||||
use crate::containers::*;
|
||||
|
||||
CollapsingHeader::new("Input")
|
||||
.default_open(true)
|
||||
.show(ui, |ui| ui.input().clone().ui(ui));
|
||||
|
||||
ui.collapsing("Stats", |ui| {
|
||||
ui.add(label!(
|
||||
"Screen size: {} x {} points, pixels_per_point: {}",
|
||||
ui.input().screen_size.x,
|
||||
ui.input().screen_size.y,
|
||||
ui.input().pixels_per_point,
|
||||
));
|
||||
|
||||
ui.add(label!("Painting:").text_style(TextStyle::Heading));
|
||||
self.paint_stats.lock().ui(ui);
|
||||
});
|
||||
}
|
||||
|
||||
pub fn memory_ui(&self, ui: &mut crate::Ui) {
|
||||
use crate::widgets::*;
|
||||
|
||||
if ui
|
||||
.add(Button::new("Reset all"))
|
||||
.tooltip_text("Reset all Egui state")
|
||||
.clicked
|
||||
{
|
||||
*self.memory() = Default::default();
|
||||
}
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.add(label!(
|
||||
"{} areas (window positions)",
|
||||
self.memory().areas.count()
|
||||
));
|
||||
if ui.add(Button::new("Reset")).clicked {
|
||||
self.memory().areas = Default::default();
|
||||
}
|
||||
});
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.add(label!(
|
||||
"{} collapsing headers",
|
||||
self.memory().collapsing_headers.len()
|
||||
));
|
||||
if ui.add(Button::new("Reset")).clicked {
|
||||
self.memory().collapsing_headers = Default::default();
|
||||
}
|
||||
});
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.add(label!("{} menu bars", self.memory().menu_bar.len()));
|
||||
if ui.add(Button::new("Reset")).clicked {
|
||||
self.memory().menu_bar = Default::default();
|
||||
}
|
||||
});
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.add(label!("{} scroll areas", self.memory().scroll_areas.len()));
|
||||
if ui.add(Button::new("Reset")).clicked {
|
||||
self.memory().scroll_areas = Default::default();
|
||||
}
|
||||
});
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.add(label!("{} resize areas", self.memory().resize.len()));
|
||||
if ui.add(Button::new("Reset")).clicked {
|
||||
self.memory().resize = Default::default();
|
||||
}
|
||||
});
|
||||
|
||||
ui.add(
|
||||
label!("NOTE: the position of this window cannot be reset from within itself.")
|
||||
.auto_shrink(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn font_definitions_ui(font_definitions: &mut paint::FontDefinitions, ui: &mut Ui) {
|
||||
use crate::widgets::*;
|
||||
for (text_style, (_family, size)) in font_definitions.iter_mut() {
|
||||
// TODO: radiobutton for family
|
||||
ui.add(
|
||||
Slider::f32(size, 4.0..=40.0)
|
||||
.precision(0)
|
||||
.text(format!("{:?}", text_style)),
|
||||
);
|
||||
}
|
||||
if ui.add(Button::new("Reset fonts")).clicked {
|
||||
*font_definitions = paint::fonts::default_font_definitions();
|
||||
}
|
||||
}
|
||||
|
||||
impl Context {
|
||||
pub fn style_ui(&self, ui: &mut Ui) {
|
||||
let mut style = self.style();
|
||||
style.ui(ui);
|
||||
self.set_style(style);
|
||||
}
|
||||
}
|
||||
|
||||
impl paint::PaintOptions {
|
||||
pub fn ui(&mut self, ui: &mut Ui) {
|
||||
use crate::widgets::*;
|
||||
ui.add(Checkbox::new(&mut self.anti_alias, "Antialias"));
|
||||
ui.add(Checkbox::new(
|
||||
&mut self.debug_paint_clip_rects,
|
||||
"Paint Clip Rects (debug)",
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
impl PaintStats {
|
||||
pub fn ui(&self, ui: &mut Ui) {
|
||||
ui.add(label!("Batches: {}", self.num_batches))
|
||||
.tooltip_text("Number of separate clip rectanlges");
|
||||
ui.add(label!("Primitives: {}", self.num_primitives))
|
||||
.tooltip_text("Boxes, circles, text areas etc");
|
||||
ui.add(label!("Vertices: {}", self.num_vertices));
|
||||
ui.add(label!("Triangles: {}", self.num_triangles));
|
||||
}
|
||||
}
|
||||
7
egui/src/examples.rs
Normal file
7
egui/src/examples.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
mod app;
|
||||
mod fractal_clock;
|
||||
|
||||
pub use {
|
||||
app::{ExampleApp, ExampleWindow},
|
||||
fractal_clock::FractalClock,
|
||||
};
|
||||
614
egui/src/examples/app.rs
Normal file
614
egui/src/examples/app.rs
Normal file
@@ -0,0 +1,614 @@
|
||||
// #![allow(dead_code, unused_variables)] // should be commented out
|
||||
use std::sync::Arc;
|
||||
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
|
||||
use crate::{color::*, containers::*, examples::FractalClock, widgets::*, *};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[derive(Default, Deserialize, Serialize)]
|
||||
#[serde(default)]
|
||||
pub struct ExampleApp {
|
||||
previous_web_location_hash: String,
|
||||
|
||||
open_windows: OpenWindows,
|
||||
// TODO: group the following together as ExampleWindows
|
||||
example_window: ExampleWindow,
|
||||
fractal_clock: FractalClock,
|
||||
}
|
||||
|
||||
impl ExampleApp {
|
||||
/// `web_location_hash`: for web demo only. e.g. "#fragmet".
|
||||
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:
|
||||
if web_location_hash == "#clock" {
|
||||
self.open_windows = OpenWindows {
|
||||
fractal_clock: true,
|
||||
..OpenWindows::none()
|
||||
};
|
||||
}
|
||||
|
||||
self.previous_web_location_hash = web_location_hash.to_owned();
|
||||
}
|
||||
|
||||
show_menu_bar(ui, &mut self.open_windows);
|
||||
self.windows(ui.ctx());
|
||||
}
|
||||
|
||||
pub fn windows(&mut self, ctx: &Arc<Context>) {
|
||||
// TODO: Make it even simpler to show a window
|
||||
|
||||
// TODO: window manager for automatic positioning?
|
||||
|
||||
let ExampleApp {
|
||||
open_windows,
|
||||
example_window,
|
||||
fractal_clock,
|
||||
..
|
||||
} = self;
|
||||
|
||||
Window::new("Examples")
|
||||
.open(&mut open_windows.examples)
|
||||
.default_pos(pos2(32.0, 100.0))
|
||||
.default_size(vec2(430.0, 600.0))
|
||||
.show(ctx, |ui| {
|
||||
example_window.ui(ui);
|
||||
});
|
||||
|
||||
Window::new("Settings")
|
||||
.open(&mut open_windows.settings)
|
||||
.default_pos(pos2(500.0, 100.0))
|
||||
.default_size(vec2(350.0, 400.0))
|
||||
.show(ctx, |ui| {
|
||||
ctx.settings_ui(ui);
|
||||
});
|
||||
|
||||
Window::new("Inspection")
|
||||
.open(&mut open_windows.inspection)
|
||||
.default_pos(pos2(500.0, 400.0))
|
||||
.default_size(vec2(400.0, 300.0))
|
||||
.show(ctx, |ui| {
|
||||
ctx.inspection_ui(ui);
|
||||
});
|
||||
|
||||
Window::new("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);
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
struct OpenWindows {
|
||||
// examples:
|
||||
examples: bool,
|
||||
example_tree: bool,
|
||||
fractal_clock: bool,
|
||||
|
||||
// egui stuff:
|
||||
settings: bool,
|
||||
inspection: bool,
|
||||
memory: bool,
|
||||
}
|
||||
|
||||
impl Default for OpenWindows {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
examples: true,
|
||||
..OpenWindows::none()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl OpenWindows {
|
||||
fn none() -> Self {
|
||||
Self {
|
||||
examples: false,
|
||||
example_tree: true,
|
||||
fractal_clock: false,
|
||||
|
||||
settings: false,
|
||||
inspection: false,
|
||||
memory: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn show_menu_bar(ui: &mut Ui, windows: &mut OpenWindows) {
|
||||
menu::bar(ui, |ui| {
|
||||
menu::menu(ui, "File", |ui| {
|
||||
if ui.add(Button::new("Clear memory")).clicked {
|
||||
*ui.ctx().memory() = Default::default();
|
||||
}
|
||||
});
|
||||
menu::menu(ui, "Windows", |ui| {
|
||||
ui.add(Checkbox::new(&mut windows.examples, "Examples"));
|
||||
ui.add(Checkbox::new(&mut windows.fractal_clock, "Fractal Clock"));
|
||||
ui.add(Separator::new());
|
||||
ui.add(Checkbox::new(&mut windows.settings, "Settings"));
|
||||
ui.add(Checkbox::new(&mut windows.inspection, "Inspection"));
|
||||
ui.add(Checkbox::new(&mut windows.memory, "Memory"));
|
||||
});
|
||||
menu::menu(ui, "About", |ui| {
|
||||
ui.add(label!("This is Egui"));
|
||||
ui.add(Hyperlink::new("https://github.com/emilk/emigui/").text("Egui home page"));
|
||||
});
|
||||
|
||||
if let Some(time) = ui.input().seconds_since_midnight {
|
||||
let time = format!(
|
||||
"{:02}:{:02}:{:02}.{:02}",
|
||||
(time.rem_euclid(24.0 * 60.0 * 60.0) / 3600.0).floor(),
|
||||
(time.rem_euclid(60.0 * 60.0) / 60.0).floor(),
|
||||
(time.rem_euclid(60.0)).floor(),
|
||||
(time.rem_euclid(1.0) * 100.0).floor()
|
||||
);
|
||||
ui.inner_layout(Layout::horizontal(Align::Max).reverse(), |ui| {
|
||||
if ui
|
||||
.add(Button::new(time).text_style(TextStyle::Monospace))
|
||||
.clicked
|
||||
{
|
||||
windows.fractal_clock = !windows.fractal_clock;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// Showcase some ui code
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct ExampleWindow {
|
||||
num_columns: usize,
|
||||
|
||||
widgets: Widgets,
|
||||
layout: LayoutExample,
|
||||
tree: Tree,
|
||||
box_painting: BoxPainting,
|
||||
painting: Painting,
|
||||
}
|
||||
|
||||
impl Default for ExampleWindow {
|
||||
fn default() -> ExampleWindow {
|
||||
ExampleWindow {
|
||||
num_columns: 2,
|
||||
|
||||
widgets: Default::default(),
|
||||
layout: Default::default(),
|
||||
tree: Tree::example(),
|
||||
box_painting: Default::default(),
|
||||
painting: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ExampleWindow {
|
||||
pub fn ui(&mut self, ui: &mut Ui) {
|
||||
ui.collapsing("About Egui", |ui| {
|
||||
ui.add(label!(
|
||||
"Egui is an experimental immediate mode GUI written in Rust."
|
||||
));
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Project home page:");
|
||||
ui.hyperlink("https://github.com/emilk/emigui/");
|
||||
});
|
||||
});
|
||||
|
||||
CollapsingHeader::new("Widgets")
|
||||
.default_open(true)
|
||||
.show(ui, |ui| {
|
||||
self.widgets.ui(ui);
|
||||
});
|
||||
|
||||
CollapsingHeader::new("Layout")
|
||||
.default_open(false)
|
||||
.show(ui, |ui| self.layout.ui(ui));
|
||||
|
||||
CollapsingHeader::new("Tree")
|
||||
.default_open(true)
|
||||
.show(ui, |ui| self.tree.ui(ui));
|
||||
|
||||
ui.collapsing("Columns", |ui| {
|
||||
ui.add(Slider::usize(&mut self.num_columns, 1..=10).text("Columns"));
|
||||
ui.columns(self.num_columns, |cols| {
|
||||
for (i, col) in cols.iter_mut().enumerate() {
|
||||
col.add(label!("Column {} out of {}", i + 1, self.num_columns));
|
||||
if i + 1 == self.num_columns && col.add(Button::new("Delete this")).clicked {
|
||||
self.num_columns -= 1;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
ui.collapsing("Test box rendering", |ui| self.box_painting.ui(ui));
|
||||
|
||||
CollapsingHeader::new("Scroll area")
|
||||
.default_open(false)
|
||||
.show(ui, |ui| {
|
||||
ScrollArea::default().show(ui, |ui| {
|
||||
ui.label(LOREM_IPSUM);
|
||||
});
|
||||
});
|
||||
|
||||
CollapsingHeader::new("Painting")
|
||||
.default_open(false)
|
||||
.show(ui, |ui| self.painting.ui(ui));
|
||||
|
||||
CollapsingHeader::new("Resize")
|
||||
.default_open(false)
|
||||
.show(ui, |ui| {
|
||||
Resize::default()
|
||||
.default_height(200.0)
|
||||
// .as_wide_as_possible()
|
||||
.auto_shrink_height(false)
|
||||
.show(ui, |ui| {
|
||||
ui.add(label!("This ui can be resized!"));
|
||||
ui.add(label!("Just pull the handle on the bottom right"));
|
||||
});
|
||||
});
|
||||
|
||||
ui.collapsing("Name clash example", |ui| {
|
||||
ui.label("\
|
||||
Widgets that store state require unique identifiers so we can track their state between frames. \
|
||||
Identifiers are normally derived from the titles of the widget.");
|
||||
|
||||
ui.label("\
|
||||
For instance, collapsable headers needs to store wether or not they are open. \
|
||||
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 folddable ui");
|
||||
});
|
||||
ui.collapsing("Collapsing header", |ui| {
|
||||
ui.label("Contents of second folddable ui");
|
||||
});
|
||||
|
||||
ui.label("\
|
||||
Most widgets don't need unique names, but are tracked \
|
||||
based on their position on screen. For instance, buttons:");
|
||||
ui.add(Button::new("Button"));
|
||||
ui.add(Button::new("Button"));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
#[serde(default)]
|
||||
struct Widgets {
|
||||
checked: bool,
|
||||
count: usize,
|
||||
radio: usize,
|
||||
slider_value: usize,
|
||||
single_line_text_input: String,
|
||||
multiline_text_input: String,
|
||||
}
|
||||
|
||||
impl Default for Widgets {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
checked: true,
|
||||
radio: 0,
|
||||
count: 0,
|
||||
slider_value: 100,
|
||||
single_line_text_input: "Hello World!".to_owned(),
|
||||
multiline_text_input: "Text can both be so wide that it needs a linebreak, but you can also add manual linebreak by pressing enter, creating new paragraphs.\nThis is the start of the next paragraph.\n\nClick me to edit me!".to_owned(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Widgets {
|
||||
pub fn ui(&mut self, ui: &mut Ui) {
|
||||
ui.horizontal(|ui| {
|
||||
ui.add(label!("Text can have").text_color(srgba(110, 255, 110, 255)));
|
||||
ui.add(label!("color").text_color(srgba(128, 140, 255, 255)));
|
||||
ui.add(label!("and tooltips (hover me)")).tooltip_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.add(Checkbox::new(&mut self.checked, "checkbox"));
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
if ui.add(radio(self.radio == 0, "First")).clicked {
|
||||
self.radio = 0;
|
||||
}
|
||||
if ui.add(radio(self.radio == 1, "Second")).clicked {
|
||||
self.radio = 1;
|
||||
}
|
||||
if ui.add(radio(self.radio == 2, "Final")).clicked {
|
||||
self.radio = 2;
|
||||
}
|
||||
});
|
||||
|
||||
ui.inner_layout(Layout::horizontal(Align::Center), |ui| {
|
||||
if ui
|
||||
.add(Button::new("Click me"))
|
||||
.tooltip_text("This will just increase a counter.")
|
||||
.clicked
|
||||
{
|
||||
self.count += 1;
|
||||
}
|
||||
ui.add(label!("The button has been clicked {} times", self.count));
|
||||
});
|
||||
|
||||
ui.add(Slider::usize(&mut self.slider_value, 1..=1000).text("value"));
|
||||
if ui.add(Button::new("Double it")).clicked {
|
||||
self.slider_value *= 2;
|
||||
}
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.add(label!("Single line text input:"));
|
||||
ui.add(
|
||||
TextEdit::new(&mut self.single_line_text_input)
|
||||
.multiline(false)
|
||||
.id("single line"),
|
||||
);
|
||||
}); // TODO: .tooltip_text("Enter text to edit me")
|
||||
|
||||
ui.add(label!("Multiline text input:"));
|
||||
ui.add(TextEdit::new(&mut self.multiline_text_input).id("multiline"));
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
#[serde(default)]
|
||||
struct BoxPainting {
|
||||
size: Vec2,
|
||||
corner_radius: f32,
|
||||
stroke_width: f32,
|
||||
num_boxes: usize,
|
||||
}
|
||||
|
||||
impl Default for BoxPainting {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
size: vec2(100.0, 50.0),
|
||||
corner_radius: 5.0,
|
||||
stroke_width: 2.0,
|
||||
num_boxes: 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BoxPainting {
|
||||
pub fn ui(&mut self, ui: &mut Ui) {
|
||||
ui.add(Slider::f32(&mut self.size.x, 0.0..=500.0).text("width"));
|
||||
ui.add(Slider::f32(&mut self.size.y, 0.0..=500.0).text("height"));
|
||||
ui.add(Slider::f32(&mut self.corner_radius, 0.0..=50.0).text("corner_radius"));
|
||||
ui.add(Slider::f32(&mut self.stroke_width, 0.0..=10.0).text("stroke_width"));
|
||||
ui.add(Slider::usize(&mut self.num_boxes, 0..=5).text("num_boxes"));
|
||||
|
||||
let pos = ui
|
||||
.allocate_space(vec2(self.size.x * (self.num_boxes as f32), self.size.y))
|
||||
.min;
|
||||
|
||||
let mut cmds = vec![];
|
||||
for i in 0..self.num_boxes {
|
||||
cmds.push(PaintCmd::Rect {
|
||||
corner_radius: self.corner_radius,
|
||||
fill: Some(gray(136, 255)),
|
||||
rect: Rect::from_min_size(
|
||||
pos2(10.0 + pos.x + (i as f32) * (self.size.x * 1.1), pos.y),
|
||||
self.size,
|
||||
),
|
||||
outline: Some(LineStyle::new(self.stroke_width, gray(255, 255))),
|
||||
});
|
||||
}
|
||||
ui.add_paint_cmds(cmds);
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[derive(Default, Deserialize, Serialize)]
|
||||
#[serde(default)]
|
||||
struct Painting {
|
||||
lines: Vec<Vec<Vec2>>,
|
||||
}
|
||||
|
||||
impl Painting {
|
||||
pub fn ui(&mut self, ui: &mut Ui) {
|
||||
ui.label("Draw with your mouse to paint");
|
||||
if ui.add(Button::new("Clear")).clicked {
|
||||
self.lines.clear();
|
||||
}
|
||||
|
||||
Resize::default()
|
||||
.default_height(200.0)
|
||||
.show(ui, |ui| self.content(ui));
|
||||
}
|
||||
|
||||
fn content(&mut self, ui: &mut Ui) {
|
||||
let rect = ui.allocate_space(ui.available_finite().size());
|
||||
let interact = ui.interact(rect, ui.id(), Sense::drag());
|
||||
let rect = interact.rect;
|
||||
ui.set_clip_rect(ui.clip_rect().intersect(rect)); // Make sure we don't paint out of bounds
|
||||
|
||||
if self.lines.is_empty() {
|
||||
self.lines.push(vec![]);
|
||||
}
|
||||
|
||||
let current_line = self.lines.last_mut().unwrap();
|
||||
|
||||
if interact.active {
|
||||
if let Some(mouse_pos) = ui.input().mouse.pos {
|
||||
let canvas_pos = mouse_pos - rect.min;
|
||||
if current_line.last() != Some(&canvas_pos) {
|
||||
current_line.push(canvas_pos);
|
||||
}
|
||||
}
|
||||
} else if !current_line.is_empty() {
|
||||
self.lines.push(vec![]);
|
||||
}
|
||||
|
||||
for line in &self.lines {
|
||||
if line.len() >= 2 {
|
||||
let points: Vec<Pos2> = line.iter().map(|p| rect.min + *p).collect();
|
||||
ui.add_paint_cmd(PaintCmd::Path {
|
||||
path: Path::from_open_points(&points),
|
||||
closed: false,
|
||||
outline: Some(LineStyle::new(2.0, LIGHT_GRAY)),
|
||||
fill: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
use crate::layout::*;
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
#[serde(default)]
|
||||
struct LayoutExample {
|
||||
dir: Direction,
|
||||
align: Option<Align>, // None == jusitifed
|
||||
reversed: bool,
|
||||
}
|
||||
|
||||
impl Default for LayoutExample {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
dir: Direction::Vertical,
|
||||
align: Some(Align::Center),
|
||||
reversed: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LayoutExample {
|
||||
pub fn ui(&mut self, ui: &mut Ui) {
|
||||
Resize::default()
|
||||
.default_size(vec2(200.0, 200.0))
|
||||
.show(ui, |ui| self.content_ui(ui));
|
||||
}
|
||||
|
||||
pub fn content_ui(&mut self, ui: &mut Ui) {
|
||||
let layout = Layout::from_dir_align(self.dir, self.align);
|
||||
if self.reversed {
|
||||
ui.set_layout(layout.reverse());
|
||||
} else {
|
||||
ui.set_layout(layout);
|
||||
}
|
||||
|
||||
// ui.add(label!("Available space: {:?}", ui.available().size()));
|
||||
if ui.add(Button::new("Reset")).clicked {
|
||||
*self = Default::default();
|
||||
}
|
||||
ui.add(Separator::new());
|
||||
ui.add(label!("Direction:"));
|
||||
|
||||
// TODO: enum iter
|
||||
|
||||
for &dir in &[Direction::Horizontal, Direction::Vertical] {
|
||||
if ui
|
||||
.add(RadioButton::new(self.dir == dir, format!("{:?}", dir)))
|
||||
.clicked
|
||||
{
|
||||
self.dir = dir;
|
||||
}
|
||||
}
|
||||
|
||||
ui.add(Checkbox::new(&mut self.reversed, "Reversed"));
|
||||
|
||||
ui.add(Separator::new());
|
||||
|
||||
ui.add(label!("Align:"));
|
||||
|
||||
for &align in &[Align::Min, Align::Center, Align::Max] {
|
||||
if ui
|
||||
.add(RadioButton::new(
|
||||
self.align == Some(align),
|
||||
format!("{:?}", align),
|
||||
))
|
||||
.clicked
|
||||
{
|
||||
self.align = Some(align);
|
||||
}
|
||||
}
|
||||
if ui
|
||||
.add(RadioButton::new(self.align == None, "Justified"))
|
||||
.tooltip_text("Try to fill full width/heigth (e.g. buttons)")
|
||||
.clicked
|
||||
{
|
||||
self.align = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
enum Action {
|
||||
Keep,
|
||||
Delete,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Deserialize, Serialize)]
|
||||
struct Tree(Vec<Tree>);
|
||||
|
||||
impl Tree {
|
||||
pub fn example() -> 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")
|
||||
}
|
||||
|
||||
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))
|
||||
.unwrap_or(Action::Keep)
|
||||
}
|
||||
|
||||
fn children_ui(&mut self, ui: &mut Ui, depth: usize) -> Action {
|
||||
if depth > 0 && ui.add(Button::new("delete").text_color(color::RED)).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
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
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.
|
||||
|
||||
Curabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, 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.";
|
||||
198
egui/src/examples/fractal_clock.rs
Normal file
198
egui/src/examples/fractal_clock.rs
Normal file
@@ -0,0 +1,198 @@
|
||||
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(-42.0))
|
||||
.scroll(false)
|
||||
// Dark background frame to make it pop:
|
||||
.frame(Frame::window(&ctx.style()).fill(Some(color::black(250))))
|
||||
.show(ctx, |ui| self.ui(ui));
|
||||
}
|
||||
|
||||
pub fn ui(&mut self, ui: &mut Ui) {
|
||||
if !self.paused {
|
||||
self.time = ui
|
||||
.input()
|
||||
.seconds_since_midnight
|
||||
.unwrap_or_else(|| ui.input().time);
|
||||
}
|
||||
|
||||
self.fractal_ui(ui, ui.available_finite());
|
||||
|
||||
let frame = Frame::popup(ui.style())
|
||||
.fill(Some(color::gray(34, 160)))
|
||||
.outline(None);
|
||||
|
||||
frame.show(&mut ui.left_column(320.0), |ui| {
|
||||
CollapsingHeader::new("Settings").show(ui, |ui| self.options_ui(ui));
|
||||
});
|
||||
|
||||
// Make sure we allocate what we used (everything)
|
||||
ui.allocate_space(ui.available_finite().size());
|
||||
}
|
||||
|
||||
fn options_ui(&mut self, ui: &mut Ui) {
|
||||
if ui.input().seconds_since_midnight.is_some() {
|
||||
ui.add(label!(
|
||||
"Local time: {:02}:{:02}:{:02}.{:03}",
|
||||
(self.time.rem_euclid(24.0 * 60.0 * 60.0) / 3600.0).floor(),
|
||||
(self.time.rem_euclid(60.0 * 60.0) / 60.0).floor(),
|
||||
(self.time.rem_euclid(60.0)).floor(),
|
||||
(self.time.rem_euclid(1.0) * 1000.0).floor()
|
||||
));
|
||||
} else {
|
||||
ui.add(label!(
|
||||
"The fractal_clock clock is not showing the correct 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();
|
||||
}
|
||||
|
||||
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, rect: Rect) {
|
||||
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 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 + TAU / 2.0,
|
||||
hands[1].angle - hands[2].angle + TAU / 2.0,
|
||||
];
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
68
egui/src/id.rs
Normal file
68
egui/src/id.rs
Normal file
@@ -0,0 +1,68 @@
|
||||
//! Egui tracks widgets frame-to-frame using `Id`s.
|
||||
//!
|
||||
//! For instance, if you start dragging a slider one frame, egui stores
|
||||
//! the sldiers Id as the current `interact_id` so that next frame when
|
||||
//! you move the mouse the same slider changes, even if the mouse has
|
||||
//! moved outside the slider.
|
||||
//!
|
||||
//! For some widgets `Id`s are also used to persist some state about the
|
||||
//! widgets, such as Window position or wether not a collapsing header region is open.
|
||||
//!
|
||||
//! This implicated that the `Id`s must be unqiue.
|
||||
//!
|
||||
//! For simple things like sliders and buttons that don't have any memory and
|
||||
//! doesn't move we can use the location of the widget as a source of identity.
|
||||
//! For instance, a slider only needs a unique and persistent ID while you are
|
||||
//! dragging the sldier. As long as it is still while moving, that is fine.
|
||||
//!
|
||||
//! For things that need to persist state even after moving (windows, collapsing headers)
|
||||
//! the location of the widgets is obviously not good enough. For instance,
|
||||
//! a collapsing region needs to remember wether or not it is open even
|
||||
//! if the layout next frame is different and the collapsing is not lower down
|
||||
//! on the screen.
|
||||
//!
|
||||
//! Then there are widgets that need no identifiers at all, like labels,
|
||||
//! because they have no state nor are interacted with.
|
||||
//!
|
||||
//! So we have two type of Ids: `PositionId` and `UniqueId`.
|
||||
//! TODO: have separate types for `PositionId` and `UniqueId`.
|
||||
|
||||
use std::hash::Hash;
|
||||
|
||||
use crate::math::Pos2;
|
||||
|
||||
#[derive(
|
||||
Clone, Copy, Debug, Hash, Eq, PartialEq, serde_derive::Deserialize, serde_derive::Serialize,
|
||||
)]
|
||||
pub struct Id(u64);
|
||||
|
||||
impl Id {
|
||||
pub fn background() -> Self {
|
||||
Self(0)
|
||||
}
|
||||
|
||||
pub fn tooltip() -> Self {
|
||||
Self(1)
|
||||
}
|
||||
|
||||
pub fn new(source: impl Hash) -> Id {
|
||||
use std::hash::Hasher;
|
||||
let mut hasher = ahash::AHasher::default();
|
||||
source.hash(&mut hasher);
|
||||
Id(hasher.finish())
|
||||
}
|
||||
|
||||
pub fn with(self, child: impl Hash) -> Id {
|
||||
use std::hash::Hasher;
|
||||
let mut hasher = ahash::AHasher::default();
|
||||
hasher.write_u64(self.0);
|
||||
child.hash(&mut hasher);
|
||||
Id(hasher.finish())
|
||||
}
|
||||
|
||||
pub fn from_pos(p: Pos2) -> Id {
|
||||
let x = p.x.round() as i32;
|
||||
let y = p.y.round() as i32;
|
||||
Id::new(&x).with(&y)
|
||||
}
|
||||
}
|
||||
324
egui/src/input.rs
Normal file
324
egui/src/input.rs
Normal file
@@ -0,0 +1,324 @@
|
||||
use serde_derive::Deserialize;
|
||||
|
||||
use crate::{math::*, movement_tracker::MovementTracker};
|
||||
|
||||
/// If mouse moves more than this, it is no longer a click (but maybe a drag)
|
||||
const MAX_CLICK_DIST: f32 = 6.0;
|
||||
/// The new mouse press must come within this many seconds from previous mouse release
|
||||
const MAX_CLICK_DELAY: f64 = 0.3;
|
||||
|
||||
/// What the integration gives to the gui.
|
||||
/// All coordinates in egui is in point/logical coordinates.
|
||||
#[derive(Clone, Debug, Default, Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct RawInput {
|
||||
/// Is the button currently down?
|
||||
pub mouse_down: bool,
|
||||
|
||||
/// Current position of the mouse in points.
|
||||
pub mouse_pos: Option<Pos2>,
|
||||
|
||||
/// How many pixels the user scrolled
|
||||
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.
|
||||
pub pixels_per_point: Option<f32>,
|
||||
|
||||
/// 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>,
|
||||
|
||||
/// In-order events received this frame
|
||||
pub events: Vec<Event>,
|
||||
}
|
||||
|
||||
/// What egui maintains
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct InputState {
|
||||
/// The raw input we got this fraem
|
||||
pub raw: RawInput,
|
||||
|
||||
pub mouse: MouseInput,
|
||||
|
||||
/// How many pixels the user scrolled
|
||||
pub scroll_delta: Vec2,
|
||||
|
||||
/// Size of the screen in points.
|
||||
pub screen_size: Vec2,
|
||||
|
||||
/// Also known as device pixel ratio, > 1 for HDPI screens.
|
||||
pub pixels_per_point: f32,
|
||||
|
||||
/// Time in seconds. Relative to whatever. Used for animation.
|
||||
pub time: f64,
|
||||
|
||||
/// 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>,
|
||||
|
||||
/// In-order events received this frame
|
||||
pub events: Vec<Event>,
|
||||
}
|
||||
|
||||
/// What egui maintains
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct MouseInput {
|
||||
/// Is the button currently down?
|
||||
/// true the frame when it is pressed,
|
||||
/// false the frame it is released.
|
||||
pub down: bool,
|
||||
|
||||
/// The mouse went from !down to down
|
||||
pub pressed: bool,
|
||||
|
||||
/// The mouse went from down to !down
|
||||
pub released: bool,
|
||||
|
||||
/// If the mouse is down, will it register as a click when released?
|
||||
/// Set to true on mouse down, set to false when mouse moves too much.
|
||||
pub could_be_click: bool,
|
||||
|
||||
/// Was there a click?
|
||||
/// Did a mouse button get released this frame closely after going down?
|
||||
pub click: bool,
|
||||
|
||||
/// Was there a double-click?
|
||||
pub double_click: bool,
|
||||
|
||||
/// When did the mouse get click last?
|
||||
/// Used to check for double-clicks.
|
||||
pub last_click_time: f64,
|
||||
|
||||
/// Current position of the mouse in points.
|
||||
/// None for touch screens when finger is not down.
|
||||
pub pos: Option<Pos2>,
|
||||
|
||||
/// Where did the current click/drag originate?
|
||||
pub press_origin: Option<Pos2>,
|
||||
|
||||
/// How much the mouse moved compared to last frame, in points.
|
||||
pub delta: Vec2,
|
||||
|
||||
/// Current velocity of mouse cursor.
|
||||
pub velocity: Vec2,
|
||||
|
||||
/// Recent movement of the mouse.
|
||||
/// Used for calculating velocity of mouse pointer.
|
||||
pub pos_tracker: MovementTracker<Pos2>,
|
||||
}
|
||||
|
||||
impl Default for MouseInput {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
down: false,
|
||||
pressed: false,
|
||||
released: false,
|
||||
could_be_click: false,
|
||||
click: false,
|
||||
double_click: false,
|
||||
last_click_time: std::f64::NEG_INFINITY,
|
||||
pos: None,
|
||||
press_origin: None,
|
||||
delta: Vec2::zero(),
|
||||
velocity: Vec2::zero(),
|
||||
pos_tracker: MovementTracker::new(1000, 0.1),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Event {
|
||||
Copy,
|
||||
Cut,
|
||||
/// Text input, e.g. via keyboard or paste action
|
||||
Text(String),
|
||||
Key {
|
||||
key: Key,
|
||||
pressed: bool,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Key {
|
||||
Alt,
|
||||
Backspace,
|
||||
Control,
|
||||
Delete,
|
||||
Down,
|
||||
End,
|
||||
Escape,
|
||||
Home,
|
||||
Insert,
|
||||
Left,
|
||||
/// Windows key or Mac Command key
|
||||
Logo,
|
||||
PageDown,
|
||||
PageUp,
|
||||
Return,
|
||||
Right,
|
||||
Shift,
|
||||
// Space,
|
||||
Tab,
|
||||
Up,
|
||||
}
|
||||
|
||||
impl InputState {
|
||||
#[must_use]
|
||||
pub fn begin_frame(self, new: RawInput) -> InputState {
|
||||
let mouse = self.mouse.begin_frame(&new);
|
||||
let dt = (new.time - self.raw.time) as f32;
|
||||
InputState {
|
||||
mouse,
|
||||
scroll_delta: new.scroll_delta,
|
||||
screen_size: new.screen_size,
|
||||
pixels_per_point: new.pixels_per_point.unwrap_or(1.0),
|
||||
time: new.time,
|
||||
dt,
|
||||
seconds_since_midnight: new.seconds_since_midnight,
|
||||
events: new.events.clone(), // TODO: remove clone() and use raw.events
|
||||
raw: new,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MouseInput {
|
||||
#[must_use]
|
||||
pub fn begin_frame(mut self, new: &RawInput) -> MouseInput {
|
||||
let delta = new
|
||||
.mouse_pos
|
||||
.and_then(|new| self.pos.map(|last| new - last))
|
||||
.unwrap_or_default();
|
||||
let pressed = !self.down && new.mouse_down;
|
||||
|
||||
let released = self.down && !new.mouse_down;
|
||||
let click = released && self.could_be_click;
|
||||
let double_click = click && (new.time - self.last_click_time) < MAX_CLICK_DELAY;
|
||||
let mut press_origin = self.press_origin;
|
||||
let mut could_be_click = self.could_be_click;
|
||||
let mut last_click_time = self.last_click_time;
|
||||
if click {
|
||||
last_click_time = new.time
|
||||
}
|
||||
|
||||
if pressed {
|
||||
press_origin = new.mouse_pos;
|
||||
could_be_click = true;
|
||||
} else if !self.down || self.pos.is_none() {
|
||||
press_origin = None;
|
||||
}
|
||||
|
||||
if let (Some(press_origin), Some(mouse_pos)) = (new.mouse_pos, press_origin) {
|
||||
could_be_click &= press_origin.distance(mouse_pos) < MAX_CLICK_DIST;
|
||||
} else {
|
||||
could_be_click = false;
|
||||
}
|
||||
|
||||
if let Some(mouse_pos) = new.mouse_pos {
|
||||
self.pos_tracker.add(new.time, mouse_pos);
|
||||
} else {
|
||||
// we do not clear the `mouse_tracker` here, because it is exactly when a finger has
|
||||
// released from the touch screen that we may want to assign a velocity to whatever
|
||||
// the user tried to throw
|
||||
}
|
||||
|
||||
let velocity = self.pos_tracker.velocity_noew(new.time).unwrap_or_default();
|
||||
|
||||
MouseInput {
|
||||
down: new.mouse_down && new.mouse_pos.is_some(),
|
||||
pressed,
|
||||
released,
|
||||
could_be_click,
|
||||
click,
|
||||
double_click,
|
||||
last_click_time,
|
||||
pos: new.mouse_pos,
|
||||
press_origin,
|
||||
delta,
|
||||
velocity,
|
||||
pos_tracker: self.pos_tracker,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RawInput {
|
||||
pub fn ui(&self, ui: &mut crate::Ui) {
|
||||
use crate::label;
|
||||
// TODO: simpler way to show values, e.g. `ui.value("Mouse Pos:", self.mouse_pos);
|
||||
// TODO: easily change default font!
|
||||
ui.add(label!("mouse_down: {}", self.mouse_down));
|
||||
ui.add(label!("mouse_pos: {:.1?}", self.mouse_pos));
|
||||
ui.add(label!("scroll_delta: {:?} points", self.scroll_delta));
|
||||
ui.add(label!("screen_size: {:?} points", self.screen_size));
|
||||
ui.add(label!("pixels_per_point: {:?}", self.pixels_per_point))
|
||||
.tooltip_text(
|
||||
"Also called hdpi factor.\nNumber of physical pixels per each logical pixel.",
|
||||
);
|
||||
ui.add(label!("time: {:.3} s", self.time));
|
||||
ui.add(label!(
|
||||
"seconds_since_midnight: {:?} s",
|
||||
self.seconds_since_midnight
|
||||
));
|
||||
ui.add(label!("events: {:?}", self.events))
|
||||
.tooltip_text("key presses etc");
|
||||
}
|
||||
}
|
||||
|
||||
impl InputState {
|
||||
pub fn ui(&self, ui: &mut crate::Ui) {
|
||||
use crate::label;
|
||||
|
||||
ui.collapsing("Raw Input", |ui| self.raw.ui(ui));
|
||||
|
||||
crate::containers::CollapsingHeader::new("mouse")
|
||||
.default_open(true)
|
||||
.show(ui, |ui| {
|
||||
self.mouse.ui(ui);
|
||||
});
|
||||
|
||||
ui.add(label!("scroll_delta: {:?} points", self.scroll_delta));
|
||||
ui.add(label!("screen_size: {:?} points", self.screen_size));
|
||||
ui.add(label!(
|
||||
"{} points for each physical pixel (hdpi factor)",
|
||||
self.pixels_per_point
|
||||
));
|
||||
ui.add(label!("time: {:.3} s", self.time));
|
||||
ui.add(label!("dt: {:.3} s", self.dt));
|
||||
ui.add(label!(
|
||||
"seconds_since_midnight: {:?} s",
|
||||
self.seconds_since_midnight
|
||||
));
|
||||
ui.add(label!("events: {:?}", self.events))
|
||||
.tooltip_text("key presses etc");
|
||||
}
|
||||
}
|
||||
|
||||
impl MouseInput {
|
||||
pub fn ui(&self, ui: &mut crate::Ui) {
|
||||
use crate::label;
|
||||
ui.add(label!("down: {}", self.down));
|
||||
ui.add(label!("pressed: {}", self.pressed));
|
||||
ui.add(label!("released: {}", self.released));
|
||||
ui.add(label!("could_be_click: {}", self.could_be_click));
|
||||
ui.add(label!("click: {}", self.click));
|
||||
ui.add(label!("double_click: {}", self.double_click));
|
||||
ui.add(label!("last_click_time: {:.3}", self.last_click_time));
|
||||
ui.add(label!("pos: {:?}", self.pos));
|
||||
ui.add(label!("press_origin: {:?}", self.press_origin));
|
||||
ui.add(label!("delta: {:?}", self.delta));
|
||||
ui.add(label!(
|
||||
"velocity: [{:3.0} {:3.0}] points/sec",
|
||||
self.velocity.x,
|
||||
self.velocity.y
|
||||
));
|
||||
}
|
||||
}
|
||||
62
egui/src/introspection.rs
Normal file
62
egui/src/introspection.rs
Normal file
@@ -0,0 +1,62 @@
|
||||
//! uis for egui types.
|
||||
use crate::{
|
||||
containers::show_tooltip,
|
||||
label,
|
||||
math::*,
|
||||
paint::{color::WHITE, PaintCmd, Texture, Triangles, Vertex},
|
||||
};
|
||||
|
||||
impl Texture {
|
||||
pub fn ui(&self, ui: &mut crate::Ui) {
|
||||
ui.add(label!(
|
||||
"Texture size: {} x {} (hover to zoom)",
|
||||
self.width,
|
||||
self.height
|
||||
));
|
||||
let mut size = vec2(self.width as f32, self.height as f32);
|
||||
if size.x > ui.available().width() {
|
||||
size *= ui.available().width() / size.x;
|
||||
}
|
||||
let rect = ui.allocate_space(size);
|
||||
let top_left = Vertex {
|
||||
pos: rect.min,
|
||||
uv: (0, 0),
|
||||
color: WHITE,
|
||||
};
|
||||
let bottom_right = Vertex {
|
||||
pos: rect.max,
|
||||
uv: (self.width as u16 - 1, self.height as u16 - 1),
|
||||
color: WHITE,
|
||||
};
|
||||
let mut triangles = Triangles::default();
|
||||
triangles.add_rect(top_left, bottom_right);
|
||||
ui.add_paint_cmd(PaintCmd::Triangles(triangles));
|
||||
|
||||
if ui.hovered(rect) {
|
||||
show_tooltip(ui.ctx(), |ui| {
|
||||
let pos = ui.top_left();
|
||||
let zoom_rect = ui.allocate_space(vec2(128.0, 128.0));
|
||||
let u = remap_clamp(pos.x, rect.range_x(), 0.0..=self.width as f32 - 1.0).round();
|
||||
let v = remap_clamp(pos.y, rect.range_y(), 0.0..=self.height as f32 - 1.0).round();
|
||||
|
||||
let texel_radius = 32.0;
|
||||
let u = clamp(u, texel_radius..=self.width as f32 - 1.0 - texel_radius);
|
||||
let v = clamp(v, texel_radius..=self.height as f32 - 1.0 - texel_radius);
|
||||
|
||||
let top_left = Vertex {
|
||||
pos: zoom_rect.min,
|
||||
uv: ((u - texel_radius) as u16, (v - texel_radius) as u16),
|
||||
color: WHITE,
|
||||
};
|
||||
let bottom_right = Vertex {
|
||||
pos: zoom_rect.max,
|
||||
uv: ((u + texel_radius) as u16, (v + texel_radius) as u16),
|
||||
color: WHITE,
|
||||
};
|
||||
let mut triangles = Triangles::default();
|
||||
triangles.add_rect(top_left, bottom_right);
|
||||
ui.add_paint_cmd(PaintCmd::Triangles(triangles));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
66
egui/src/layers.rs
Normal file
66
egui/src/layers.rs
Normal file
@@ -0,0 +1,66 @@
|
||||
use ahash::AHashMap;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
|
||||
use crate::{math::Rect, paint::PaintCmd, Id};
|
||||
|
||||
/// Different layer categories
|
||||
#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq, Ord, PartialOrd, Deserialize, Serialize)]
|
||||
pub enum Order {
|
||||
/// Painted behind all floating windows
|
||||
Background,
|
||||
/// Normal moveable windows that you reorder by click
|
||||
Middle,
|
||||
/// Popups, menus etc that should always be painted on top of windows
|
||||
Foreground,
|
||||
/// Debug layer, always painted last / on top
|
||||
Debug,
|
||||
}
|
||||
|
||||
/// An ideintifer for a paint layer.
|
||||
/// Also acts as an identifier for `Area`:s.
|
||||
#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq, Deserialize, Serialize)]
|
||||
pub struct Layer {
|
||||
pub order: Order,
|
||||
pub id: Id,
|
||||
}
|
||||
|
||||
impl Layer {
|
||||
pub fn debug() -> Self {
|
||||
Self {
|
||||
order: Order::Debug,
|
||||
id: Id::new("debug"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Each `PaintCmd` is paired with a clip rectangle.
|
||||
type PaintList = Vec<(Rect, PaintCmd)>;
|
||||
|
||||
/// TODO: improve this
|
||||
#[derive(Clone, Default)]
|
||||
pub struct GraphicLayers(AHashMap<Layer, PaintList>);
|
||||
|
||||
impl GraphicLayers {
|
||||
pub fn layer(&mut self, layer: Layer) -> &mut PaintList {
|
||||
self.0.entry(layer).or_default()
|
||||
}
|
||||
|
||||
pub fn drain(
|
||||
&mut self,
|
||||
area_order: &[Layer],
|
||||
) -> impl ExactSizeIterator<Item = (Rect, PaintCmd)> {
|
||||
let mut all_commands: Vec<_> = Default::default();
|
||||
|
||||
for layer in area_order {
|
||||
if let Some(commands) = self.0.get_mut(layer) {
|
||||
all_commands.extend(commands.drain(..));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(commands) = self.0.get_mut(&Layer::debug()) {
|
||||
all_commands.extend(commands.drain(..));
|
||||
}
|
||||
|
||||
all_commands.into_iter()
|
||||
}
|
||||
}
|
||||
221
egui/src/layout.rs
Normal file
221
egui/src/layout.rs
Normal file
@@ -0,0 +1,221 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
|
||||
use crate::{math::*, style::Style};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Direction {
|
||||
Horizontal,
|
||||
Vertical,
|
||||
}
|
||||
|
||||
impl Default for Direction {
|
||||
fn default() -> Direction {
|
||||
Direction::Vertical
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Align {
|
||||
/// Left/Top
|
||||
Min,
|
||||
|
||||
/// Note: requires a bounded/known available_width.
|
||||
Center,
|
||||
|
||||
/// Right/Bottom
|
||||
/// Note: requires a bounded/known available_width.
|
||||
Max,
|
||||
}
|
||||
|
||||
impl Default for Align {
|
||||
fn default() -> Align {
|
||||
Align::Min
|
||||
}
|
||||
}
|
||||
|
||||
/// Used e.g. to anchor a piece of text to a part of the rectangle.
|
||||
/// Give a position within the rect, specified by the aligns
|
||||
pub fn align_rect(rect: Rect, align: (Align, Align)) -> Rect {
|
||||
let x = match align.0 {
|
||||
Align::Min => rect.left(),
|
||||
Align::Center => rect.left() - 0.5 * rect.width(),
|
||||
Align::Max => rect.left() - rect.width(),
|
||||
};
|
||||
let y = match align.1 {
|
||||
Align::Min => rect.top(),
|
||||
Align::Center => rect.top() - 0.5 * rect.height(),
|
||||
Align::Max => rect.top() - rect.height(),
|
||||
};
|
||||
Rect::from_min_size(pos2(x, y), rect.size())
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Deserialize, Serialize)]
|
||||
pub struct Layout {
|
||||
/// Lay out things horizontally or vertically?
|
||||
dir: Direction,
|
||||
|
||||
/// For vertical layouts: put things to left, center or right?
|
||||
/// For horizontal layouts: put things to top, center or bottom?
|
||||
/// None means justified, which means full width (vertical layout) or height (horizontal layouts).
|
||||
align: Option<Align>,
|
||||
|
||||
/// Lay out things in reversed order, i.e. from the right or bottom-up.
|
||||
reversed: bool,
|
||||
}
|
||||
|
||||
impl Default for Layout {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
dir: Direction::Vertical,
|
||||
align: Some(Align::Min),
|
||||
reversed: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Layout {
|
||||
/// None align means justified, e.g. fill full width/height.
|
||||
pub fn from_dir_align(dir: Direction, align: Option<Align>) -> Self {
|
||||
Self {
|
||||
dir,
|
||||
align,
|
||||
reversed: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn vertical(align: Align) -> Self {
|
||||
Self {
|
||||
dir: Direction::Vertical,
|
||||
align: Some(align),
|
||||
reversed: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn horizontal(align: Align) -> Self {
|
||||
Self {
|
||||
dir: Direction::Horizontal,
|
||||
align: Some(align),
|
||||
reversed: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Full-width layout.
|
||||
/// Nice for menues etc where each button is full width.
|
||||
pub fn justified(dir: Direction) -> Self {
|
||||
Self {
|
||||
dir,
|
||||
align: None,
|
||||
reversed: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn reverse(self) -> Self {
|
||||
Self {
|
||||
dir: self.dir,
|
||||
align: self.align,
|
||||
reversed: !self.reversed,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn dir(&self) -> Direction {
|
||||
self.dir
|
||||
}
|
||||
|
||||
pub fn is_reversed(&self) -> bool {
|
||||
self.reversed
|
||||
}
|
||||
|
||||
/// Given the cursor in the region, how much space is available
|
||||
/// for the next widget?
|
||||
pub fn available(&self, cursor: Pos2, rect: Rect) -> Rect {
|
||||
if self.reversed {
|
||||
Rect::from_min_max(rect.min, cursor)
|
||||
} else {
|
||||
Rect::from_min_max(cursor, rect.max)
|
||||
}
|
||||
}
|
||||
|
||||
/// Reserve this much space and move the cursor.
|
||||
/// Returns where to put the widget.
|
||||
///
|
||||
/// # How sizes are negotiated
|
||||
/// Each widget should have a *minimum desired size* and a *desired size*.
|
||||
/// When asking for space, ask AT LEAST for you minimum, and don't ask for more than you need.
|
||||
/// If you want to fill the space, ask about `available().size()` and use that.
|
||||
///
|
||||
/// You may get MORE space than you asked for, for instance
|
||||
/// for `Justified` aligned layouts, like in menus.
|
||||
///
|
||||
/// You may get LESS space than you asked for if the current layout won't fit what you asked for.
|
||||
pub fn allocate_space(
|
||||
&self,
|
||||
cursor: &mut Pos2,
|
||||
style: &Style,
|
||||
available_size: Vec2,
|
||||
mut child_size: Vec2,
|
||||
) -> Rect {
|
||||
let available_size = available_size.max(child_size);
|
||||
|
||||
let mut child_move = Vec2::default();
|
||||
let mut cursor_change = Vec2::default();
|
||||
|
||||
if self.dir == Direction::Horizontal {
|
||||
if let Some(align) = self.align {
|
||||
child_move.y += match align {
|
||||
Align::Min => 0.0,
|
||||
Align::Center => 0.5 * (available_size.y - child_size.y),
|
||||
Align::Max => available_size.y - child_size.y,
|
||||
};
|
||||
} else {
|
||||
// justified: fill full height
|
||||
child_size.y = child_size.y.max(available_size.y);
|
||||
}
|
||||
|
||||
cursor_change.x += child_size.x;
|
||||
cursor_change.x += style.item_spacing.x; // Where to put next thing, if there is a next thing
|
||||
} else {
|
||||
if let Some(align) = self.align {
|
||||
child_move.x += match align {
|
||||
Align::Min => 0.0,
|
||||
Align::Center => 0.5 * (available_size.x - child_size.x),
|
||||
Align::Max => available_size.x - child_size.x,
|
||||
};
|
||||
} else {
|
||||
// justified: fill full width
|
||||
child_size.x = child_size.x.max(available_size.x);
|
||||
};
|
||||
cursor_change.y += child_size.y;
|
||||
cursor_change.y += style.item_spacing.y; // Where to put next thing, if there is a next thing
|
||||
}
|
||||
|
||||
if self.is_reversed() {
|
||||
// reverse: cursor starts at bottom right corner of new widget.
|
||||
|
||||
let child_pos = if self.dir == Direction::Horizontal {
|
||||
pos2(
|
||||
cursor.x - child_size.x,
|
||||
cursor.y - available_size.y + child_move.y,
|
||||
)
|
||||
} else {
|
||||
pos2(
|
||||
cursor.x - available_size.x + child_move.x,
|
||||
cursor.y - child_size.y,
|
||||
)
|
||||
};
|
||||
// let child_pos = *cursor - child_move - child_size;
|
||||
*cursor -= cursor_change;
|
||||
Rect::from_min_size(child_pos, child_size)
|
||||
} else {
|
||||
let child_pos = *cursor + child_move;
|
||||
*cursor += cursor_change;
|
||||
Rect::from_min_size(child_pos, child_size)
|
||||
}
|
||||
}
|
||||
}
|
||||
57
egui/src/lib.rs
Normal file
57
egui/src/lib.rs
Normal file
@@ -0,0 +1,57 @@
|
||||
#![deny(warnings)]
|
||||
#![warn(
|
||||
clippy::all,
|
||||
clippy::dbg_macro,
|
||||
clippy::doc_markdown,
|
||||
clippy::empty_enum,
|
||||
clippy::enum_glob_use,
|
||||
clippy::filter_map_next,
|
||||
clippy::fn_params_excessive_bools,
|
||||
clippy::imprecise_flops,
|
||||
clippy::lossy_float_literal,
|
||||
clippy::mem_forget,
|
||||
clippy::needless_borrow,
|
||||
clippy::needless_continue,
|
||||
clippy::pub_enum_variant_names,
|
||||
clippy::rest_pat_in_fully_bound_structs,
|
||||
// clippy::suboptimal_flops, // TODO
|
||||
clippy::todo,
|
||||
// clippy::use_self,
|
||||
future_incompatible,
|
||||
nonstandard_style,
|
||||
rust_2018_idioms,
|
||||
)]
|
||||
|
||||
pub mod containers;
|
||||
mod context;
|
||||
pub mod examples;
|
||||
mod id;
|
||||
mod input;
|
||||
mod introspection;
|
||||
mod layers;
|
||||
mod layout;
|
||||
pub mod math;
|
||||
mod memory;
|
||||
mod movement_tracker;
|
||||
pub mod paint;
|
||||
mod style;
|
||||
mod types;
|
||||
mod ui;
|
||||
pub mod widgets;
|
||||
|
||||
pub use {
|
||||
containers::*,
|
||||
context::Context,
|
||||
id::Id,
|
||||
input::*,
|
||||
layers::*,
|
||||
layout::*,
|
||||
math::*,
|
||||
memory::Memory,
|
||||
movement_tracker::MovementTracker,
|
||||
paint::{color, Color, TextStyle, Texture},
|
||||
style::Style,
|
||||
types::*,
|
||||
ui::Ui,
|
||||
widgets::*,
|
||||
};
|
||||
580
egui/src/math.rs
Normal file
580
egui/src/math.rs
Normal file
@@ -0,0 +1,580 @@
|
||||
use std::ops::{Add, AddAssign, Div, Mul, MulAssign, Neg, RangeInclusive, Sub, SubAssign};
|
||||
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Copy, Default, Deserialize, Serialize)]
|
||||
pub struct Vec2 {
|
||||
pub x: f32,
|
||||
pub y: f32,
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn vec2(x: f32, y: f32) -> Vec2 {
|
||||
Vec2 { x, y }
|
||||
}
|
||||
|
||||
impl Vec2 {
|
||||
pub fn zero() -> Self {
|
||||
Self { x: 0.0, y: 0.0 }
|
||||
}
|
||||
|
||||
pub fn infinity() -> Self {
|
||||
Self {
|
||||
x: f32::INFINITY,
|
||||
y: f32::INFINITY,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn splat(v: impl Into<f32>) -> Self {
|
||||
let v: f32 = v.into();
|
||||
Self { x: v, y: v }
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn normalized(self) -> Self {
|
||||
let len = self.length();
|
||||
if len <= 0.0 {
|
||||
self
|
||||
} else {
|
||||
self / len
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn rot90(self) -> Self {
|
||||
vec2(self.y, -self.x)
|
||||
}
|
||||
|
||||
pub fn length(self) -> f32 {
|
||||
self.x.hypot(self.y)
|
||||
}
|
||||
|
||||
pub fn length_sq(self) -> f32 {
|
||||
self.x * self.x + self.y * self.y
|
||||
}
|
||||
|
||||
pub fn distance(a: Self, b: Self) -> f32 {
|
||||
(a - b).length()
|
||||
}
|
||||
|
||||
pub fn distance_sq(a: Self, b: Self) -> f32 {
|
||||
(a - b).length_sq()
|
||||
}
|
||||
|
||||
pub fn angled(angle: f32) -> Self {
|
||||
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())
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn round(self) -> Self {
|
||||
vec2(self.x.round(), self.y.round())
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn ceil(self) -> Self {
|
||||
vec2(self.x.ceil(), self.y.ceil())
|
||||
}
|
||||
|
||||
pub fn is_finite(self) -> bool {
|
||||
self.x.is_finite() && self.y.is_finite()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn min(self, other: Self) -> Self {
|
||||
vec2(self.x.min(other.x), self.y.min(other.y))
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn max(self, other: Self) -> Self {
|
||||
vec2(self.x.max(other.x), self.y.max(other.y))
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn clamp(self, range: RangeInclusive<Self>) -> Self {
|
||||
Self {
|
||||
x: clamp(self.x, range.start().x..=range.end().x),
|
||||
y: clamp(self.y, range.start().y..=range.end().y),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Vec2 {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.x == other.x && self.y == other.y
|
||||
}
|
||||
}
|
||||
impl Eq for Vec2 {}
|
||||
|
||||
impl Neg for Vec2 {
|
||||
type Output = Vec2;
|
||||
|
||||
fn neg(self) -> Vec2 {
|
||||
vec2(-self.x, -self.y)
|
||||
}
|
||||
}
|
||||
|
||||
impl AddAssign for Vec2 {
|
||||
fn add_assign(&mut self, rhs: Vec2) {
|
||||
*self = Vec2 {
|
||||
x: self.x + rhs.x,
|
||||
y: self.y + rhs.y,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
impl SubAssign for Vec2 {
|
||||
fn sub_assign(&mut self, rhs: Vec2) {
|
||||
*self = Vec2 {
|
||||
x: self.x - rhs.x,
|
||||
y: self.y - rhs.y,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
impl Add for Vec2 {
|
||||
type Output = Vec2;
|
||||
fn add(self, rhs: Vec2) -> Vec2 {
|
||||
Vec2 {
|
||||
x: self.x + rhs.x,
|
||||
y: self.y + rhs.y,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Sub for Vec2 {
|
||||
type Output = Vec2;
|
||||
fn sub(self, rhs: Vec2) -> Vec2 {
|
||||
Vec2 {
|
||||
x: self.x - rhs.x,
|
||||
y: self.y - rhs.y,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MulAssign<f32> for Vec2 {
|
||||
fn mul_assign(&mut self, rhs: f32) {
|
||||
self.x *= rhs;
|
||||
self.y *= rhs;
|
||||
}
|
||||
}
|
||||
|
||||
impl Mul<f32> for Vec2 {
|
||||
type Output = Vec2;
|
||||
fn mul(self, factor: f32) -> Vec2 {
|
||||
Vec2 {
|
||||
x: self.x * factor,
|
||||
y: self.y * factor,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Mul<Vec2> for f32 {
|
||||
type Output = Vec2;
|
||||
fn mul(self, vec: Vec2) -> Vec2 {
|
||||
Vec2 {
|
||||
x: self * vec.x,
|
||||
y: self * vec.y,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Div<f32> for Vec2 {
|
||||
type Output = Vec2;
|
||||
fn div(self, factor: f32) -> Vec2 {
|
||||
Vec2 {
|
||||
x: self.x / factor,
|
||||
y: self.y / factor,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Vec2 {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "[{:.1} {:.1}]", self.x, self.y)
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// Sometimes called a Point. I prefer the shorter Pos2 so it is equal length to Vec2
|
||||
#[derive(Clone, Copy, Default, Deserialize, Serialize)]
|
||||
pub struct Pos2 {
|
||||
pub x: f32,
|
||||
pub y: f32,
|
||||
// implicit w = 1
|
||||
}
|
||||
|
||||
pub fn pos2(x: f32, y: f32) -> Pos2 {
|
||||
Pos2 { x, y }
|
||||
}
|
||||
|
||||
impl Pos2 {
|
||||
pub fn to_vec2(self) -> Vec2 {
|
||||
Vec2 {
|
||||
x: self.x,
|
||||
y: self.y,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn distance(self: Self, other: Self) -> f32 {
|
||||
(self - other).length()
|
||||
}
|
||||
|
||||
pub fn distance_sq(self: Self, other: Self) -> f32 {
|
||||
(self - other).length_sq()
|
||||
}
|
||||
|
||||
pub fn floor(self) -> Self {
|
||||
pos2(self.x.floor(), self.y.floor())
|
||||
}
|
||||
|
||||
pub fn round(self) -> Self {
|
||||
pos2(self.x.round(), self.y.round())
|
||||
}
|
||||
|
||||
pub fn ceil(self) -> Self {
|
||||
pos2(self.x.ceil(), self.y.ceil())
|
||||
}
|
||||
|
||||
pub fn is_finite(self) -> bool {
|
||||
self.x.is_finite() && self.y.is_finite()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn min(self, other: Self) -> Self {
|
||||
pos2(self.x.min(other.x), self.y.min(other.y))
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn max(self, other: Self) -> Self {
|
||||
pos2(self.x.max(other.x), self.y.max(other.y))
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn clamp(self, range: RangeInclusive<Self>) -> Self {
|
||||
Self {
|
||||
x: clamp(self.x, range.start().x..=range.end().x),
|
||||
y: clamp(self.y, range.start().y..=range.end().y),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Pos2 {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.x == other.x && self.y == other.y
|
||||
}
|
||||
}
|
||||
impl Eq for Pos2 {}
|
||||
|
||||
impl AddAssign<Vec2> for Pos2 {
|
||||
fn add_assign(&mut self, rhs: Vec2) {
|
||||
*self = Pos2 {
|
||||
x: self.x + rhs.x,
|
||||
y: self.y + rhs.y,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
impl SubAssign<Vec2> for Pos2 {
|
||||
fn sub_assign(&mut self, rhs: Vec2) {
|
||||
*self = Pos2 {
|
||||
x: self.x - rhs.x,
|
||||
y: self.y - rhs.y,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
impl Add<Vec2> for Pos2 {
|
||||
type Output = Pos2;
|
||||
fn add(self, rhs: Vec2) -> Pos2 {
|
||||
Pos2 {
|
||||
x: self.x + rhs.x,
|
||||
y: self.y + rhs.y,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Sub for Pos2 {
|
||||
type Output = Vec2;
|
||||
fn sub(self, rhs: Pos2) -> Vec2 {
|
||||
Vec2 {
|
||||
x: self.x - rhs.x,
|
||||
y: self.y - rhs.y,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Sub<Vec2> for Pos2 {
|
||||
type Output = Pos2;
|
||||
fn sub(self, rhs: Vec2) -> Pos2 {
|
||||
Pos2 {
|
||||
x: self.x - rhs.x,
|
||||
y: self.y - rhs.y,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Pos2 {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "[{:.1} {:.1}]", self.x, self.y)
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[derive(Clone, Copy, Default, Eq, PartialEq, Deserialize, Serialize)]
|
||||
pub struct Rect {
|
||||
pub min: Pos2,
|
||||
pub max: Pos2,
|
||||
}
|
||||
|
||||
impl Rect {
|
||||
/// Infinite rectangle that contains everything
|
||||
pub fn everything() -> Self {
|
||||
let inf = f32::INFINITY;
|
||||
Self {
|
||||
min: pos2(-inf, -inf),
|
||||
max: pos2(inf, inf),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn nothing() -> Self {
|
||||
let inf = f32::INFINITY;
|
||||
Self {
|
||||
min: pos2(inf, inf),
|
||||
max: pos2(-inf, -inf),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_min_max(min: Pos2, max: Pos2) -> Self {
|
||||
Rect { min, max }
|
||||
}
|
||||
|
||||
pub fn from_min_size(min: Pos2, size: Vec2) -> Self {
|
||||
Rect {
|
||||
min,
|
||||
max: min + size,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_center_size(center: Pos2, size: Vec2) -> Self {
|
||||
Rect {
|
||||
min: center - size * 0.5,
|
||||
max: center + size * 0.5,
|
||||
}
|
||||
}
|
||||
|
||||
/// Expand by this much in each direction, keeping the center
|
||||
#[must_use]
|
||||
pub fn expand(self, amnt: f32) -> Self {
|
||||
self.expand2(Vec2::splat(amnt))
|
||||
}
|
||||
|
||||
/// Expand by this much in each direction, keeping the center
|
||||
#[must_use]
|
||||
pub fn expand2(self, amnt: Vec2) -> Self {
|
||||
Rect::from_min_max(self.min - amnt, self.max + amnt)
|
||||
}
|
||||
|
||||
/// Shrink by this much in each direction, keeping the center
|
||||
#[must_use]
|
||||
pub fn shrink(self, amnt: f32) -> Self {
|
||||
self.shrink2(Vec2::splat(amnt))
|
||||
}
|
||||
|
||||
/// Shrink by this much in each direction, keeping the center
|
||||
#[must_use]
|
||||
pub fn shrink2(self, amnt: Vec2) -> Self {
|
||||
Rect::from_min_max(self.min + amnt, self.max - amnt)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn translate(self, amnt: Vec2) -> Self {
|
||||
Rect::from_min_size(self.min + amnt, self.size())
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn intersect(self, other: Rect) -> Self {
|
||||
Self {
|
||||
min: self.min.max(other.min),
|
||||
max: self.max.min(other.max),
|
||||
}
|
||||
}
|
||||
|
||||
/// keep min
|
||||
pub fn set_width(&mut self, w: f32) {
|
||||
self.max.x = self.min.x + w;
|
||||
}
|
||||
|
||||
/// keep min
|
||||
pub fn set_height(&mut self, h: f32) {
|
||||
self.max.y = self.min.y + h;
|
||||
}
|
||||
|
||||
/// Keep size
|
||||
pub fn set_center(&mut self, center: Pos2) {
|
||||
*self = self.translate(center - self.center());
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn contains(&self, p: Pos2) -> bool {
|
||||
self.min.x <= p.x
|
||||
&& p.x <= self.min.x + self.size().x
|
||||
&& self.min.y <= p.y
|
||||
&& p.y <= self.min.y + self.size().y
|
||||
}
|
||||
|
||||
pub fn extend_with(&mut self, p: Pos2) {
|
||||
self.min = self.min.min(p);
|
||||
self.max = self.max.max(p);
|
||||
}
|
||||
|
||||
pub fn union(self, other: Rect) -> Rect {
|
||||
Rect {
|
||||
min: self.min.min(other.min),
|
||||
max: self.max.max(other.max),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn center(&self) -> Pos2 {
|
||||
Pos2 {
|
||||
x: self.min.x + self.size().x / 2.0,
|
||||
y: self.min.y + self.size().y / 2.0,
|
||||
}
|
||||
}
|
||||
pub fn size(&self) -> Vec2 {
|
||||
self.max - self.min
|
||||
}
|
||||
pub fn width(&self) -> f32 {
|
||||
self.max.x - self.min.x
|
||||
}
|
||||
pub fn height(&self) -> f32 {
|
||||
self.max.y - self.min.y
|
||||
}
|
||||
|
||||
pub fn range_x(&self) -> RangeInclusive<f32> {
|
||||
self.min.x..=self.max.x
|
||||
}
|
||||
|
||||
pub fn range_y(&self) -> RangeInclusive<f32> {
|
||||
self.min.y..=self.max.y
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.max.x < self.min.x || self.max.y < self.min.y
|
||||
}
|
||||
|
||||
pub fn is_finite(&self) -> bool {
|
||||
self.min.is_finite() && self.max.is_finite()
|
||||
}
|
||||
|
||||
// Convenience functions (assumes origin is towards left top):
|
||||
pub fn left(&self) -> f32 {
|
||||
self.min.x
|
||||
}
|
||||
pub fn right(&self) -> f32 {
|
||||
self.max.x
|
||||
}
|
||||
pub fn top(&self) -> f32 {
|
||||
self.min.y
|
||||
}
|
||||
pub fn bottom(&self) -> f32 {
|
||||
self.max.y
|
||||
}
|
||||
pub fn left_top(&self) -> Pos2 {
|
||||
pos2(self.left(), self.top())
|
||||
}
|
||||
pub fn center_top(&self) -> Pos2 {
|
||||
pos2(self.center().x, self.top())
|
||||
}
|
||||
pub fn right_top(&self) -> Pos2 {
|
||||
pos2(self.right(), self.top())
|
||||
}
|
||||
pub fn left_center(&self) -> Pos2 {
|
||||
pos2(self.left(), self.center().y)
|
||||
}
|
||||
pub fn right_center(&self) -> Pos2 {
|
||||
pos2(self.right(), self.center().y)
|
||||
}
|
||||
pub fn left_bottom(&self) -> Pos2 {
|
||||
pos2(self.left(), self.bottom())
|
||||
}
|
||||
pub fn center_bottom(&self) -> Pos2 {
|
||||
pos2(self.center().x, self.bottom())
|
||||
}
|
||||
pub fn right_bottom(&self) -> Pos2 {
|
||||
pos2(self.right(), self.bottom())
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Rect {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "[{:?} - {:?}]", self.min, self.max)
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
pub fn lerp<T>(range: RangeInclusive<T>, t: f32) -> T
|
||||
where
|
||||
f32: Mul<T, Output = T>,
|
||||
T: Add<T, Output = T> + Copy,
|
||||
{
|
||||
(1.0 - t) * *range.start() + t * *range.end()
|
||||
}
|
||||
|
||||
pub fn remap(x: f32, from: RangeInclusive<f32>, to: RangeInclusive<f32>) -> f32 {
|
||||
let t = (x - from.start()) / (from.end() - from.start());
|
||||
lerp(to, t)
|
||||
}
|
||||
|
||||
pub fn remap_clamp(x: f32, from: RangeInclusive<f32>, to: RangeInclusive<f32>) -> f32 {
|
||||
if x <= *from.start() {
|
||||
*to.start()
|
||||
} else if *from.end() <= x {
|
||||
*to.end()
|
||||
} else {
|
||||
let t = (x - from.start()) / (from.end() - from.start());
|
||||
// Ensure no numerical inaccurcies sneak in:
|
||||
if 1.0 <= t {
|
||||
*to.end()
|
||||
} else {
|
||||
lerp(to, t)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clamp<T>(x: T, range: RangeInclusive<T>) -> T
|
||||
where
|
||||
T: Copy + PartialOrd,
|
||||
{
|
||||
if x <= *range.start() {
|
||||
*range.start()
|
||||
} else if *range.end() <= x {
|
||||
*range.end()
|
||||
} else {
|
||||
x
|
||||
}
|
||||
}
|
||||
|
||||
/// For t=[0,1], returns [0,1] with a derivate of zero at both ends
|
||||
pub fn ease_in_ease_out(t: f32) -> f32 {
|
||||
3.0 * t * t - 2.0 * t * t * t
|
||||
}
|
||||
|
||||
pub const TAU: f32 = 2.0 * std::f32::consts::PI;
|
||||
187
egui/src/memory.rs
Normal file
187
egui/src/memory.rs
Normal file
@@ -0,0 +1,187 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use crate::{
|
||||
containers::{area, collapsing_header, menu, resize, scroll_area, window},
|
||||
widgets::text_edit,
|
||||
Id, Layer, Pos2, Rect,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, Default, serde_derive::Deserialize, serde_derive::Serialize)]
|
||||
#[serde(default)]
|
||||
pub struct Memory {
|
||||
#[serde(skip)]
|
||||
pub(crate) interaction: Interaction,
|
||||
|
||||
/// The widget with keyboard focus (i.e. a text input field).
|
||||
#[serde(skip)]
|
||||
pub(crate) kb_focus_id: Option<Id>,
|
||||
|
||||
// states of various types of widgets
|
||||
pub(crate) collapsing_headers: HashMap<Id, collapsing_header::State>,
|
||||
pub(crate) menu_bar: HashMap<Id, menu::BarState>,
|
||||
pub(crate) resize: HashMap<Id, resize::State>,
|
||||
pub(crate) scroll_areas: HashMap<Id, scroll_area::State>,
|
||||
pub(crate) text_edit: HashMap<Id, text_edit::State>,
|
||||
|
||||
#[serde(skip)]
|
||||
pub(crate) window_interaction: Option<window::WindowInteraction>,
|
||||
|
||||
pub(crate) areas: Areas,
|
||||
}
|
||||
|
||||
/// Say there is a butotn in a scroll area.
|
||||
/// If the user clicks the button, the button should click.
|
||||
/// If the user drags the button we should scroll the scroll area.
|
||||
/// So what we do is that when the mouse is pressed we register both the button
|
||||
/// and the scroll area (as `click_id`/`drag_id`).
|
||||
/// If the user releases the button without moving the mouse we register it as a click on `click_id`.
|
||||
/// If the cursor moves too much we clear the `click_id` and start passing move events to `drag_id`.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Interaction {
|
||||
/// A widget interested in clicks that has a mouse press on it.
|
||||
pub click_id: Option<Id>,
|
||||
|
||||
/// A widget interested in drags that has a mouse press on it.
|
||||
pub drag_id: Option<Id>,
|
||||
|
||||
/// HACK: windows have low priority on dragging.
|
||||
/// This is so that if you drag a slider in a window,
|
||||
/// the slider will steal the drag away from the window.
|
||||
/// This is needed because we do window interaction first (to prevent frame delay),
|
||||
/// and then do content layout.
|
||||
pub drag_is_window: bool,
|
||||
|
||||
/// Any interest in catching clicks this frame?
|
||||
/// Cleared to false at start of each frame.
|
||||
pub click_interest: bool,
|
||||
|
||||
/// Any interest in catching clicks this frame?
|
||||
/// Cleared to false at start of each frame.
|
||||
pub drag_interest: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, serde_derive::Deserialize, serde_derive::Serialize)]
|
||||
#[serde(default)]
|
||||
pub struct Areas {
|
||||
areas: HashMap<Id, area::State>,
|
||||
/// Top is last
|
||||
order: Vec<Layer>,
|
||||
visible_last_frame: HashSet<Layer>,
|
||||
visible_current_frame: HashSet<Layer>,
|
||||
|
||||
/// When an area want to be on top, it is put in here.
|
||||
/// At the end of the frame, this is used to reorder the layers.
|
||||
/// This means if several layers want to be on top, they will keep their relative order.
|
||||
/// So if you close three windows and then reopen them all in one frame,
|
||||
/// they will all be sent to the top, but keep their previous internal order.
|
||||
wants_to_be_on_top: HashSet<Layer>,
|
||||
}
|
||||
|
||||
impl Memory {
|
||||
pub(crate) fn begin_frame(&mut self, prev_input: &crate::input::InputState) {
|
||||
self.interaction.click_interest = false;
|
||||
self.interaction.drag_interest = false;
|
||||
|
||||
if !prev_input.mouse.could_be_click {
|
||||
self.interaction.click_id = None;
|
||||
}
|
||||
|
||||
if !prev_input.mouse.down || prev_input.mouse.pos.is_none() {
|
||||
// mouse was not down last frame
|
||||
self.interaction.click_id = None;
|
||||
self.interaction.drag_id = None;
|
||||
|
||||
let window_interaction = self.window_interaction.take();
|
||||
if let Some(window_interaction) = window_interaction {
|
||||
if window_interaction.is_pure_move() {
|
||||
// Throw windows because it is fun:
|
||||
let area_layer = window_interaction.area_layer;
|
||||
let area_state = self.areas.get(area_layer.id).clone();
|
||||
if let Some(mut area_state) = area_state {
|
||||
area_state.vel = prev_input.mouse.velocity;
|
||||
self.areas.set_state(area_layer, area_state);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn end_frame(&mut self) {
|
||||
self.areas.end_frame()
|
||||
}
|
||||
|
||||
/// TODO: call once at the start of the frame for the current mouse pos
|
||||
pub fn layer_at(&self, pos: Pos2) -> Option<Layer> {
|
||||
self.areas.layer_at(pos)
|
||||
}
|
||||
}
|
||||
|
||||
impl Areas {
|
||||
pub(crate) fn count(&self) -> usize {
|
||||
self.areas.len()
|
||||
}
|
||||
|
||||
pub(crate) fn get(&mut self, id: Id) -> Option<area::State> {
|
||||
self.areas.get(&id).cloned()
|
||||
}
|
||||
|
||||
pub(crate) fn order(&self) -> &[Layer] {
|
||||
&self.order
|
||||
}
|
||||
|
||||
pub(crate) fn set_state(&mut self, layer: Layer, state: area::State) {
|
||||
self.visible_current_frame.insert(layer);
|
||||
let did_insert = self.areas.insert(layer.id, state).is_none();
|
||||
if did_insert {
|
||||
self.order.push(layer);
|
||||
}
|
||||
}
|
||||
|
||||
/// TODO: call once at the start of the frame for the current mouse pos
|
||||
pub fn layer_at(&self, pos: Pos2) -> Option<Layer> {
|
||||
for layer in self.order.iter().rev() {
|
||||
if self.is_visible(layer) {
|
||||
if let Some(state) = self.areas.get(&layer.id) {
|
||||
if state.interactable {
|
||||
let rect = Rect::from_min_size(state.pos, state.size);
|
||||
if rect.contains(pos) {
|
||||
return Some(*layer);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn visible_last_frame(&self, layer: &Layer) -> bool {
|
||||
self.visible_last_frame.contains(layer)
|
||||
}
|
||||
|
||||
pub fn is_visible(&self, layer: &Layer) -> bool {
|
||||
self.visible_last_frame.contains(layer) || self.visible_current_frame.contains(layer)
|
||||
}
|
||||
|
||||
pub fn move_to_top(&mut self, layer: Layer) {
|
||||
self.visible_current_frame.insert(layer);
|
||||
self.wants_to_be_on_top.insert(layer);
|
||||
|
||||
if self.order.iter().find(|x| **x == layer).is_none() {
|
||||
self.order.push(layer);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn end_frame(&mut self) {
|
||||
let Self {
|
||||
visible_last_frame,
|
||||
visible_current_frame,
|
||||
order,
|
||||
wants_to_be_on_top,
|
||||
..
|
||||
} = self;
|
||||
|
||||
*visible_last_frame = std::mem::take(visible_current_frame);
|
||||
order.sort_by_key(|layer| (layer.order, wants_to_be_on_top.contains(layer)));
|
||||
wants_to_be_on_top.clear();
|
||||
}
|
||||
}
|
||||
126
egui/src/movement_tracker.rs
Normal file
126
egui/src/movement_tracker.rs
Normal file
@@ -0,0 +1,126 @@
|
||||
use std::collections::VecDeque;
|
||||
|
||||
/// This struct tracks recent values of some time series.
|
||||
/// This can be used for things like smoothed averages (for e.g. FPS)
|
||||
/// or for smoothed velocity (e.g. mouse pointer speed).
|
||||
/// All times are in seconds.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct MovementTracker<T> {
|
||||
max_len: usize,
|
||||
max_age: f64,
|
||||
|
||||
/// (time, value) pais
|
||||
values: VecDeque<(f64, T)>,
|
||||
}
|
||||
|
||||
impl<T> MovementTracker<T>
|
||||
where
|
||||
T: Copy,
|
||||
{
|
||||
pub fn new(max_len: usize, max_age: f64) -> Self {
|
||||
Self {
|
||||
max_len,
|
||||
max_age,
|
||||
values: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.values.is_empty()
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.values.len()
|
||||
}
|
||||
|
||||
pub fn values<'a>(&'a self) -> impl Iterator<Item = T> + 'a {
|
||||
self.values.iter().map(|(_time, value)| *value)
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
self.values.clear()
|
||||
}
|
||||
|
||||
/// Values must be added with a monotonically increasing time, or at least not decreasing.
|
||||
pub fn add(&mut self, now: f64, value: T) {
|
||||
if let Some((last_time, _)) = self.values.back() {
|
||||
debug_assert!(now >= *last_time, "Time shouldn't go backwards");
|
||||
}
|
||||
self.values.push_back((now, value));
|
||||
self.flush(now);
|
||||
}
|
||||
|
||||
/// Mean time difference between values in this `MovementTracker`.
|
||||
pub fn mean_time_interval(&self) -> Option<f32> {
|
||||
if let (Some(first), Some(last)) = (self.values.front(), self.values.back()) {
|
||||
let n = self.len();
|
||||
if n >= 2 {
|
||||
Some((last.0 - first.0) as f32 / ((n - 1) as f32))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn flush(&mut self, now: f64) {
|
||||
while self.values.len() > self.max_len {
|
||||
self.values.pop_front();
|
||||
}
|
||||
while let Some((front_time, _)) = self.values.front() {
|
||||
if *front_time < now - self.max_age {
|
||||
self.values.pop_front();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> MovementTracker<T>
|
||||
where
|
||||
T: Copy,
|
||||
T: std::iter::Sum,
|
||||
T: std::ops::Div<f32, Output = T>,
|
||||
{
|
||||
pub fn sum(&self) -> T {
|
||||
self.values().sum()
|
||||
}
|
||||
|
||||
pub fn average(&self) -> Option<T> {
|
||||
let num = self.len();
|
||||
if num > 0 {
|
||||
Some(self.sum() / (num as f32))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Vel> MovementTracker<T>
|
||||
where
|
||||
T: Copy,
|
||||
T: std::ops::Sub<Output = Vel>,
|
||||
Vel: std::ops::Div<f32, Output = Vel>,
|
||||
{
|
||||
/// Calculate a smooth velocity (per second) from start until now
|
||||
pub fn velocity_noew(&mut self, now: f64) -> Option<Vel> {
|
||||
self.flush(now);
|
||||
self.velocity_all()
|
||||
}
|
||||
|
||||
/// Calculate a smooth velocity (per second) over the entire time span
|
||||
pub fn velocity_all(&self) -> Option<Vel> {
|
||||
if let (Some(first), Some(last)) = (self.values.front(), self.values.back()) {
|
||||
let dt = (last.0 - first.0) as f32;
|
||||
if dt > 0.0 {
|
||||
Some((last.1 - first.1) / dt)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
14
egui/src/paint.rs
Normal file
14
egui/src/paint.rs
Normal file
@@ -0,0 +1,14 @@
|
||||
pub mod color;
|
||||
pub mod command;
|
||||
pub mod font;
|
||||
pub mod fonts;
|
||||
pub mod mesher;
|
||||
mod texture_atlas;
|
||||
|
||||
pub use {
|
||||
color::Color,
|
||||
command::{LineStyle, PaintCmd},
|
||||
fonts::{FontDefinitions, Fonts, TextStyle},
|
||||
mesher::{PaintBatches, PaintOptions, Path, Triangles, Vertex},
|
||||
texture_atlas::Texture,
|
||||
};
|
||||
62
egui/src/paint/color.rs
Normal file
62
egui/src/paint/color.rs
Normal file
@@ -0,0 +1,62 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
|
||||
/// 0-255 `sRGBA`. TODO: rename `sRGBA` for clarity.
|
||||
/// Uses premultiplied alpha.
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, Ord, PartialEq, PartialOrd, Deserialize, Serialize)]
|
||||
pub struct Color {
|
||||
pub r: u8,
|
||||
pub g: u8,
|
||||
pub b: u8,
|
||||
pub a: u8,
|
||||
}
|
||||
|
||||
pub const fn srgba(r: u8, g: u8, b: u8, a: u8) -> Color {
|
||||
Color { r, g, b, a }
|
||||
}
|
||||
|
||||
pub const fn gray(l: u8, a: u8) -> Color {
|
||||
Color {
|
||||
r: l,
|
||||
g: l,
|
||||
b: l,
|
||||
a,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn black(a: u8) -> Color {
|
||||
Color {
|
||||
r: 0,
|
||||
g: 0,
|
||||
b: 0,
|
||||
a,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn white(a: u8) -> Color {
|
||||
Color {
|
||||
r: a,
|
||||
g: a,
|
||||
b: a,
|
||||
a,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn additive_gray(l: u8) -> Color {
|
||||
Color {
|
||||
r: l,
|
||||
g: l,
|
||||
b: l,
|
||||
a: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub const TRANSPARENT: Color = srgba(0, 0, 0, 0);
|
||||
pub const BLACK: Color = srgba(0, 0, 0, 255);
|
||||
pub const LIGHT_GRAY: Color = srgba(220, 220, 220, 255);
|
||||
pub const GRAY: Color = srgba(160, 160, 160, 255);
|
||||
pub const WHITE: Color = srgba(255, 255, 255, 255);
|
||||
pub const RED: Color = srgba(255, 0, 0, 255);
|
||||
pub const GREEN: Color = srgba(0, 255, 0, 255);
|
||||
pub const BLUE: Color = srgba(0, 0, 255, 255);
|
||||
pub const YELLOW: Color = srgba(255, 255, 0, 255);
|
||||
pub const LIGHT_BLUE: Color = srgba(140, 160, 255, 255);
|
||||
66
egui/src/paint/command.rs
Normal file
66
egui/src/paint/command.rs
Normal file
@@ -0,0 +1,66 @@
|
||||
use {
|
||||
super::{font::Galley, fonts::TextStyle, Color, Path, Triangles},
|
||||
crate::math::{Pos2, Rect},
|
||||
};
|
||||
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
|
||||
// TODO: rename, e.g. `paint::Cmd`?
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum PaintCmd {
|
||||
Circle {
|
||||
center: Pos2,
|
||||
fill: Option<Color>,
|
||||
outline: Option<LineStyle>,
|
||||
radius: f32,
|
||||
},
|
||||
LineSegment {
|
||||
points: [Pos2; 2],
|
||||
style: LineStyle,
|
||||
},
|
||||
Path {
|
||||
path: Path,
|
||||
closed: bool,
|
||||
fill: Option<Color>,
|
||||
outline: Option<LineStyle>,
|
||||
},
|
||||
Rect {
|
||||
rect: Rect,
|
||||
corner_radius: f32,
|
||||
fill: Option<Color>,
|
||||
outline: Option<LineStyle>,
|
||||
},
|
||||
Text {
|
||||
/// Top left corner of the first character.
|
||||
pos: Pos2,
|
||||
/// The layed out text
|
||||
galley: Galley,
|
||||
text_style: TextStyle, // TODO: Font?
|
||||
color: Color,
|
||||
},
|
||||
Triangles(Triangles),
|
||||
}
|
||||
|
||||
impl PaintCmd {
|
||||
pub fn line_segment(points: [Pos2; 2], color: Color, width: f32) -> Self {
|
||||
Self::LineSegment {
|
||||
points,
|
||||
style: LineStyle::new(width, color),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
|
||||
pub struct LineStyle {
|
||||
pub width: f32,
|
||||
pub color: Color,
|
||||
}
|
||||
|
||||
impl LineStyle {
|
||||
pub fn new(width: impl Into<f32>, color: impl Into<Color>) -> Self {
|
||||
Self {
|
||||
width: width.into(),
|
||||
color: color.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
493
egui/src/paint/font.rs
Normal file
493
egui/src/paint/font.rs
Normal file
@@ -0,0 +1,493 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use {
|
||||
ahash::AHashMap,
|
||||
parking_lot::Mutex,
|
||||
rusttype::{point, Scale},
|
||||
};
|
||||
|
||||
use crate::math::{vec2, Vec2};
|
||||
|
||||
use super::texture_atlas::TextureAtlas;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub struct GalleyCursor {
|
||||
/// character count in whole galley
|
||||
pub char_idx: usize,
|
||||
/// line number
|
||||
pub line: usize,
|
||||
/// character count on this line
|
||||
pub column: usize,
|
||||
}
|
||||
|
||||
/// A collection of text locked into place.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Galley {
|
||||
/// The full text
|
||||
pub text: String,
|
||||
|
||||
/// Lines of text, from top to bottom.
|
||||
/// The number of chars in all lines sum up to text.chars().count()
|
||||
pub lines: Vec<Line>,
|
||||
|
||||
// Optimization: calculate once and reuse.
|
||||
pub size: Vec2,
|
||||
}
|
||||
|
||||
/// A typeset piece of text on a single line.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Line {
|
||||
/// The start of each character, probably starting at zero.
|
||||
/// The last element is the end of the last character.
|
||||
/// x_offsets.len() == text.chars().count() + 1
|
||||
/// This is never empty.
|
||||
/// Unit: points.
|
||||
pub x_offsets: Vec<f32>,
|
||||
|
||||
/// Top of the line, offset within the Galley.
|
||||
/// Unit: points.
|
||||
pub y_min: f32,
|
||||
|
||||
/// Bottom of the line, offset within the Galley.
|
||||
/// Unit: points.
|
||||
pub y_max: f32,
|
||||
|
||||
/// If true, the last char on this line is '\n'
|
||||
pub ends_with_newline: bool,
|
||||
}
|
||||
|
||||
impl Galley {
|
||||
pub fn sanity_check(&self) {
|
||||
let mut char_count = 0;
|
||||
for line in &self.lines {
|
||||
line.sanity_check();
|
||||
char_count += line.char_count();
|
||||
}
|
||||
assert_eq!(char_count, self.text.chars().count());
|
||||
}
|
||||
|
||||
/// If given a char index after the first line, the end of the last character is returned instead.
|
||||
/// Returns a Vec2 rather than a Pos2 as this is an offset into the galley. *shrug*
|
||||
pub fn char_start_pos(&self, char_idx: usize) -> Vec2 {
|
||||
let mut char_count = 0;
|
||||
for line in &self.lines {
|
||||
let line_char_count = line.char_count();
|
||||
if char_count <= char_idx && char_idx < char_count + line_char_count {
|
||||
let line_char_offset = char_idx - char_count;
|
||||
return vec2(line.x_offsets[line_char_offset], line.y_min);
|
||||
}
|
||||
char_count += line_char_count;
|
||||
}
|
||||
|
||||
if let Some(last) = self.lines.last() {
|
||||
vec2(last.max_x(), last.y_min)
|
||||
} else {
|
||||
// Empty galley
|
||||
vec2(0.0, 0.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Character offset at the given position within the galley
|
||||
pub fn char_at(&self, pos: Vec2) -> GalleyCursor {
|
||||
let mut best_y_dist = f32::INFINITY;
|
||||
let mut cursor = GalleyCursor::default();
|
||||
|
||||
let mut char_count = 0;
|
||||
for (line_nr, line) in self.lines.iter().enumerate() {
|
||||
let y_dist = (line.y_min - pos.y).abs().min((line.y_max - pos.y).abs());
|
||||
if y_dist < best_y_dist {
|
||||
best_y_dist = y_dist;
|
||||
let mut column = line.char_at(pos.x);
|
||||
if column == line.char_count() && line.ends_with_newline {
|
||||
// handle the case where line ends with a \n and we click after it.
|
||||
// We should return the position BEFORE the \n!
|
||||
column -= 1;
|
||||
}
|
||||
cursor = GalleyCursor {
|
||||
char_idx: char_count + column,
|
||||
line: line_nr,
|
||||
column,
|
||||
}
|
||||
}
|
||||
char_count += line.char_count();
|
||||
}
|
||||
cursor
|
||||
}
|
||||
}
|
||||
|
||||
impl Line {
|
||||
pub fn sanity_check(&self) {
|
||||
assert!(!self.x_offsets.is_empty());
|
||||
}
|
||||
|
||||
pub fn char_count(&self) -> usize {
|
||||
assert!(!self.x_offsets.is_empty());
|
||||
self.x_offsets.len() - 1
|
||||
}
|
||||
|
||||
pub fn min_x(&self) -> f32 {
|
||||
*self.x_offsets.first().unwrap()
|
||||
}
|
||||
|
||||
pub fn max_x(&self) -> f32 {
|
||||
*self.x_offsets.last().unwrap()
|
||||
}
|
||||
|
||||
/// Closest char at the desired x coordinate. return [0, char_count()]
|
||||
pub fn char_at(&self, desired_x: f32) -> usize {
|
||||
for (i, char_x_bounds) in self.x_offsets.windows(2).enumerate() {
|
||||
let char_center_x = 0.5 * (char_x_bounds[0] + char_x_bounds[1]);
|
||||
if desired_x < char_center_x {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
self.char_count()
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// const REPLACEMENT_CHAR: char = '\u{25A1}'; // □ white square Replaces a missing or unsupported Unicode character.
|
||||
// const REPLACEMENT_CHAR: char = '\u{FFFD}'; // <20> REPLACEMENT CHARACTER
|
||||
const REPLACEMENT_CHAR: char = '?';
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct UvRect {
|
||||
/// X/Y offset for nice rendering (unit: points).
|
||||
pub offset: Vec2,
|
||||
pub size: Vec2,
|
||||
|
||||
/// Top left corner UV in texture.
|
||||
pub min: (u16, u16),
|
||||
|
||||
/// Bottom right corner (exclusive).
|
||||
pub max: (u16, u16),
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct GlyphInfo {
|
||||
id: rusttype::GlyphId,
|
||||
|
||||
/// Unit: points.
|
||||
pub advance_width: f32,
|
||||
|
||||
/// Texture coordinates. None for space.
|
||||
pub uv_rect: Option<UvRect>,
|
||||
}
|
||||
|
||||
/// The interface uses points as the unit for everything.
|
||||
#[derive(Clone)]
|
||||
pub struct Font {
|
||||
font: rusttype::Font<'static>,
|
||||
/// Maximum character height
|
||||
scale_in_pixels: f32,
|
||||
pixels_per_point: f32,
|
||||
glyph_infos: AHashMap<char, GlyphInfo>, // TODO: see if we can optimize if we switch to a binary search
|
||||
atlas: Arc<Mutex<TextureAtlas>>,
|
||||
}
|
||||
|
||||
impl Font {
|
||||
pub fn new(
|
||||
atlas: Arc<Mutex<TextureAtlas>>,
|
||||
font_data: &'static [u8],
|
||||
scale_in_points: f32,
|
||||
pixels_per_point: f32,
|
||||
) -> Font {
|
||||
let font = rusttype::Font::try_from_bytes(font_data).expect("Error constructing Font");
|
||||
let scale_in_pixels = pixels_per_point * scale_in_points;
|
||||
|
||||
let mut font = Font {
|
||||
font,
|
||||
scale_in_pixels,
|
||||
pixels_per_point,
|
||||
glyph_infos: Default::default(),
|
||||
atlas,
|
||||
};
|
||||
|
||||
/// Printable ASCII characters [32, 126], which excludes control codes.
|
||||
const FIRST_ASCII: usize = 32; // 32 == space
|
||||
const LAST_ASCII: usize = 126;
|
||||
for c in (FIRST_ASCII..=LAST_ASCII).map(|c| c as u8 as char) {
|
||||
font.add_char(c);
|
||||
}
|
||||
font.add_char(REPLACEMENT_CHAR);
|
||||
|
||||
font
|
||||
}
|
||||
|
||||
pub fn round_to_pixel(&self, point: f32) -> f32 {
|
||||
(point * self.pixels_per_point).round() / self.pixels_per_point
|
||||
}
|
||||
|
||||
/// Height of one line of text. In points
|
||||
/// TODO: rename height ?
|
||||
pub fn line_spacing(&self) -> f32 {
|
||||
self.scale_in_pixels / self.pixels_per_point
|
||||
}
|
||||
pub fn height(&self) -> f32 {
|
||||
self.scale_in_pixels / self.pixels_per_point
|
||||
}
|
||||
|
||||
pub fn uv_rect(&self, c: char) -> Option<UvRect> {
|
||||
self.glyph_infos.get(&c).and_then(|gi| gi.uv_rect)
|
||||
}
|
||||
|
||||
fn glyph_info_or_none(&self, c: char) -> Option<&GlyphInfo> {
|
||||
self.glyph_infos.get(&c)
|
||||
}
|
||||
|
||||
fn glyph_info_or_replacemnet(&self, c: char) -> &GlyphInfo {
|
||||
self.glyph_info_or_none(c)
|
||||
.unwrap_or_else(|| self.glyph_info_or_none(REPLACEMENT_CHAR).unwrap())
|
||||
}
|
||||
|
||||
fn add_char(&mut self, c: char) {
|
||||
if self.glyph_infos.contains_key(&c) {
|
||||
return;
|
||||
}
|
||||
|
||||
let glyph = self.font.glyph(c);
|
||||
assert_ne!(
|
||||
glyph.id().0,
|
||||
0,
|
||||
"Failed to find a glyph for the character '{}'",
|
||||
c
|
||||
);
|
||||
let glyph = glyph.scaled(Scale::uniform(self.scale_in_pixels));
|
||||
let glyph = glyph.positioned(point(0.0, 0.0));
|
||||
|
||||
let uv_rect = if let Some(bb) = glyph.pixel_bounding_box() {
|
||||
let glyph_width = bb.width() as usize;
|
||||
let glyph_height = bb.height() as usize;
|
||||
assert!(glyph_width >= 1);
|
||||
assert!(glyph_height >= 1);
|
||||
|
||||
let mut atlas_lock = self.atlas.lock();
|
||||
let glyph_pos = atlas_lock.allocate((glyph_width, glyph_height));
|
||||
|
||||
let texture = atlas_lock.texture_mut();
|
||||
glyph.draw(|x, y, v| {
|
||||
if v > 0.0 {
|
||||
let px = glyph_pos.0 + x as usize;
|
||||
let py = glyph_pos.1 + y as usize;
|
||||
texture[(px, py)] = (v * 255.0).round() as u8;
|
||||
}
|
||||
});
|
||||
|
||||
let offset_y_in_pixels =
|
||||
self.scale_in_pixels as f32 + bb.min.y as f32 - 4.0 * self.pixels_per_point; // TODO: use font.v_metrics
|
||||
Some(UvRect {
|
||||
offset: vec2(
|
||||
bb.min.x as f32 / self.pixels_per_point,
|
||||
offset_y_in_pixels / self.pixels_per_point,
|
||||
),
|
||||
size: vec2(glyph_width as f32, glyph_height as f32) / self.pixels_per_point,
|
||||
min: (glyph_pos.0 as u16, glyph_pos.1 as u16),
|
||||
max: (
|
||||
(glyph_pos.0 + glyph_width) as u16,
|
||||
(glyph_pos.1 + glyph_height) as u16,
|
||||
),
|
||||
})
|
||||
} else {
|
||||
// No bounding box. Maybe a space?
|
||||
None
|
||||
};
|
||||
|
||||
let advance_width_in_points =
|
||||
glyph.unpositioned().h_metrics().advance_width / self.pixels_per_point;
|
||||
|
||||
self.glyph_infos.insert(
|
||||
c,
|
||||
GlyphInfo {
|
||||
id: glyph.id(),
|
||||
advance_width: advance_width_in_points,
|
||||
uv_rect,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Typeset the given text onto one line.
|
||||
/// Assumes there are no \n in the text.
|
||||
/// Always returns exactly one frament.
|
||||
pub fn layout_single_line(&self, text: String) -> Galley {
|
||||
let x_offsets = self.layout_single_line_fragment(&text);
|
||||
let line = Line {
|
||||
x_offsets,
|
||||
y_min: 0.0,
|
||||
y_max: self.height(),
|
||||
ends_with_newline: false,
|
||||
};
|
||||
let width = line.max_x();
|
||||
let size = vec2(width, self.height());
|
||||
let galley = Galley {
|
||||
text,
|
||||
lines: vec![line],
|
||||
size,
|
||||
};
|
||||
galley.sanity_check();
|
||||
galley
|
||||
}
|
||||
|
||||
pub fn layout_multiline(&self, text: String, max_width_in_points: f32) -> Galley {
|
||||
let line_spacing = self.line_spacing();
|
||||
let mut cursor_y = 0.0;
|
||||
let mut lines = Vec::new();
|
||||
|
||||
let mut paragraph_start = 0;
|
||||
|
||||
while paragraph_start < text.len() {
|
||||
let next_newline = text[paragraph_start..].find('\n');
|
||||
let paragraph_end = next_newline
|
||||
.map(|newline| paragraph_start + newline + 1)
|
||||
.unwrap_or_else(|| text.len());
|
||||
|
||||
assert!(paragraph_start < paragraph_end);
|
||||
let paragraph_text = &text[paragraph_start..paragraph_end];
|
||||
let mut paragraph_lines =
|
||||
self.layout_paragraph_max_width(paragraph_text, max_width_in_points);
|
||||
assert!(!paragraph_lines.is_empty());
|
||||
|
||||
for line in &mut paragraph_lines {
|
||||
line.y_min += cursor_y;
|
||||
line.y_max += cursor_y;
|
||||
}
|
||||
cursor_y = paragraph_lines.last().unwrap().y_max;
|
||||
cursor_y += line_spacing * 0.4; // extra spacing between paragraphs. less hacky
|
||||
|
||||
lines.append(&mut paragraph_lines);
|
||||
|
||||
paragraph_start = paragraph_end;
|
||||
}
|
||||
|
||||
if text.is_empty() || text.ends_with('\n') {
|
||||
// Add an empty last line for correct visuals etc:
|
||||
lines.push(Line {
|
||||
x_offsets: vec![0.0],
|
||||
y_min: cursor_y,
|
||||
y_max: cursor_y + line_spacing,
|
||||
ends_with_newline: text.ends_with('\n'),
|
||||
});
|
||||
}
|
||||
|
||||
let mut widest_line = 0.0;
|
||||
for line in &lines {
|
||||
widest_line = line.max_x().max(widest_line);
|
||||
}
|
||||
let size = vec2(widest_line, lines.last().unwrap().y_max);
|
||||
|
||||
let galley = Galley { text, lines, size };
|
||||
galley.sanity_check();
|
||||
galley
|
||||
}
|
||||
|
||||
/// Typeset the given text onto one line.
|
||||
/// Assumes there are no \n in the text.
|
||||
/// Return x_offsets, one longer than the number of characters in the text.
|
||||
fn layout_single_line_fragment(&self, text: &str) -> Vec<f32> {
|
||||
let scale_in_pixels = Scale::uniform(self.scale_in_pixels);
|
||||
|
||||
let mut x_offsets = Vec::with_capacity(text.chars().count() + 1);
|
||||
x_offsets.push(0.0);
|
||||
|
||||
let mut cursor_x_in_points = 0.0f32;
|
||||
let mut last_glyph_id = None;
|
||||
|
||||
for c in text.chars() {
|
||||
let glyph = self.glyph_info_or_replacemnet(c);
|
||||
|
||||
if let Some(last_glyph_id) = last_glyph_id {
|
||||
cursor_x_in_points +=
|
||||
self.font
|
||||
.pair_kerning(scale_in_pixels, last_glyph_id, glyph.id)
|
||||
/ self.pixels_per_point
|
||||
}
|
||||
cursor_x_in_points += glyph.advance_width;
|
||||
cursor_x_in_points = self.round_to_pixel(cursor_x_in_points);
|
||||
last_glyph_id = Some(glyph.id);
|
||||
|
||||
x_offsets.push(cursor_x_in_points);
|
||||
}
|
||||
|
||||
x_offsets
|
||||
}
|
||||
|
||||
/// A paragraph is text with no line break character in it.
|
||||
/// The text will be linebreaked by the given `max_width_in_points`.
|
||||
pub fn layout_paragraph_max_width(&self, text: &str, max_width_in_points: f32) -> Vec<Line> {
|
||||
let full_x_offsets = self.layout_single_line_fragment(text);
|
||||
|
||||
let mut line_start_x = full_x_offsets[0];
|
||||
assert_eq!(line_start_x, 0.0);
|
||||
let mut cursor_y = 0.0;
|
||||
let mut line_start_idx = 0;
|
||||
|
||||
// start index of the last space. A candidate for a new line.
|
||||
let mut last_space = None;
|
||||
|
||||
let mut out_lines = vec![];
|
||||
|
||||
for (i, (x, chr)) in full_x_offsets.iter().skip(1).zip(text.chars()).enumerate() {
|
||||
let line_width = x - line_start_x;
|
||||
|
||||
if line_width > max_width_in_points {
|
||||
if let Some(last_space_idx) = last_space {
|
||||
let include_trailing_space = true;
|
||||
let line = if include_trailing_space {
|
||||
Line {
|
||||
x_offsets: full_x_offsets[line_start_idx..=last_space_idx + 1]
|
||||
.iter()
|
||||
.map(|x| x - line_start_x)
|
||||
.collect(),
|
||||
y_min: cursor_y,
|
||||
y_max: cursor_y + self.height(),
|
||||
ends_with_newline: false, // we'll fix this later
|
||||
}
|
||||
} else {
|
||||
Line {
|
||||
x_offsets: full_x_offsets[line_start_idx..=last_space_idx]
|
||||
.iter()
|
||||
.map(|x| x - line_start_x)
|
||||
.collect(),
|
||||
y_min: cursor_y,
|
||||
y_max: cursor_y + self.height(),
|
||||
ends_with_newline: false, // we'll fix this later
|
||||
}
|
||||
};
|
||||
line.sanity_check();
|
||||
out_lines.push(line);
|
||||
|
||||
line_start_idx = last_space_idx + 1;
|
||||
line_start_x = full_x_offsets[line_start_idx];
|
||||
last_space = None;
|
||||
cursor_y += self.line_spacing();
|
||||
cursor_y = self.round_to_pixel(cursor_y);
|
||||
}
|
||||
}
|
||||
|
||||
const NON_BREAKING_SPACE: char = '\u{A0}';
|
||||
if chr.is_whitespace() && chr != NON_BREAKING_SPACE {
|
||||
last_space = Some(i);
|
||||
}
|
||||
}
|
||||
|
||||
if line_start_idx + 1 < full_x_offsets.len() {
|
||||
let line = Line {
|
||||
x_offsets: full_x_offsets[line_start_idx..]
|
||||
.iter()
|
||||
.map(|x| x - line_start_x)
|
||||
.collect(),
|
||||
y_min: cursor_y,
|
||||
y_max: cursor_y + self.height(),
|
||||
ends_with_newline: false, // we'll fix this later
|
||||
};
|
||||
line.sanity_check();
|
||||
out_lines.push(line);
|
||||
}
|
||||
|
||||
if text.ends_with('\n') {
|
||||
out_lines.last_mut().unwrap().ends_with_newline = true;
|
||||
}
|
||||
|
||||
out_lines
|
||||
}
|
||||
}
|
||||
126
egui/src/paint/fonts.rs
Normal file
126
egui/src/paint/fonts.rs
Normal file
@@ -0,0 +1,126 @@
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
hash::{Hash, Hasher},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use {parking_lot::Mutex, serde_derive::Serialize};
|
||||
|
||||
use super::{
|
||||
font::Font,
|
||||
texture_atlas::{Texture, TextureAtlas},
|
||||
};
|
||||
|
||||
/// TODO: rename
|
||||
#[derive(Copy, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
|
||||
pub enum TextStyle {
|
||||
Body,
|
||||
Button,
|
||||
Heading,
|
||||
Monospace,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
|
||||
pub enum FontFamily {
|
||||
Monospace,
|
||||
VariableWidth,
|
||||
}
|
||||
|
||||
pub type FontDefinitions = BTreeMap<TextStyle, (FontFamily, f32)>;
|
||||
|
||||
pub fn default_font_definitions() -> FontDefinitions {
|
||||
let mut definitions = FontDefinitions::new();
|
||||
definitions.insert(TextStyle::Body, (FontFamily::VariableWidth, 14.0));
|
||||
definitions.insert(TextStyle::Button, (FontFamily::VariableWidth, 16.0));
|
||||
definitions.insert(TextStyle::Heading, (FontFamily::VariableWidth, 24.0));
|
||||
definitions.insert(TextStyle::Monospace, (FontFamily::Monospace, 13.0));
|
||||
definitions
|
||||
}
|
||||
|
||||
pub struct Fonts {
|
||||
pixels_per_point: f32,
|
||||
definitions: FontDefinitions,
|
||||
fonts: BTreeMap<TextStyle, Font>,
|
||||
texture: Texture,
|
||||
}
|
||||
|
||||
impl Fonts {
|
||||
pub fn new(pixels_per_point: f32) -> Fonts {
|
||||
Fonts::from_definitions(default_font_definitions(), pixels_per_point)
|
||||
}
|
||||
|
||||
pub fn from_definitions(definitions: FontDefinitions, pixels_per_point: f32) -> Fonts {
|
||||
let mut fonts = Fonts {
|
||||
pixels_per_point,
|
||||
definitions: Default::default(),
|
||||
fonts: Default::default(),
|
||||
texture: Default::default(),
|
||||
};
|
||||
fonts.set_sizes(definitions);
|
||||
fonts
|
||||
}
|
||||
|
||||
pub fn definitions(&self) -> &FontDefinitions {
|
||||
&self.definitions
|
||||
}
|
||||
|
||||
pub fn set_sizes(&mut self, definitions: FontDefinitions) {
|
||||
if self.definitions == definitions {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut atlas = TextureAtlas::new(512, 8); // TODO: better default?
|
||||
|
||||
// Make the top left four pixels fully white:
|
||||
let pos = atlas.allocate((2, 2));
|
||||
assert_eq!(pos, (0, 0));
|
||||
atlas.texture_mut()[(0, 0)] = 255;
|
||||
atlas.texture_mut()[(0, 1)] = 255;
|
||||
atlas.texture_mut()[(1, 0)] = 255;
|
||||
atlas.texture_mut()[(1, 1)] = 255;
|
||||
|
||||
let atlas = Arc::new(Mutex::new(atlas));
|
||||
|
||||
// TODO: figure out a way to make the wasm smaller despite including a font. Zip it?
|
||||
let monospae_typeface_data = include_bytes!("../../fonts/ProggyClean.ttf"); // Use 13 for this. NOTHING ELSE.
|
||||
|
||||
// let monospae_typeface_data = include_bytes!("../../fonts/Roboto-Regular.ttf");
|
||||
|
||||
let variable_typeface_data = include_bytes!("../../fonts/Comfortaa-Regular.ttf"); // Funny, hard to read
|
||||
|
||||
// let variable_typeface_data = include_bytes!("../../fonts/DejaVuSans.ttf"); // Basic, boring, takes up more space
|
||||
|
||||
self.definitions = definitions.clone();
|
||||
self.fonts = definitions
|
||||
.into_iter()
|
||||
.map(|(text_style, (family, size))| {
|
||||
let typeface_data: &[u8] = match family {
|
||||
FontFamily::Monospace => monospae_typeface_data,
|
||||
FontFamily::VariableWidth => variable_typeface_data,
|
||||
};
|
||||
|
||||
(
|
||||
text_style,
|
||||
Font::new(atlas.clone(), typeface_data, size, self.pixels_per_point),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
self.texture = atlas.lock().texture().clone();
|
||||
|
||||
let mut hasher = ahash::AHasher::default();
|
||||
self.texture.pixels.hash(&mut hasher);
|
||||
self.texture.id = hasher.finish();
|
||||
}
|
||||
|
||||
pub fn texture(&self) -> &Texture {
|
||||
&self.texture
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Index<TextStyle> for Fonts {
|
||||
type Output = Font;
|
||||
|
||||
fn index(&self, text_style: TextStyle) -> &Font {
|
||||
&self.fonts[&text_style]
|
||||
}
|
||||
}
|
||||
703
egui/src/paint/mesher.rs
Normal file
703
egui/src/paint/mesher.rs
Normal file
@@ -0,0 +1,703 @@
|
||||
#![allow(clippy::identity_op)]
|
||||
|
||||
/// Outputs render info in a format suitable for e.g. OpenGL.
|
||||
use {
|
||||
super::{
|
||||
color::{self, srgba, Color},
|
||||
fonts::Fonts,
|
||||
LineStyle, PaintCmd,
|
||||
},
|
||||
crate::math::*,
|
||||
};
|
||||
|
||||
const WHITE_UV: (u16, u16) = (1, 1);
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, serde_derive::Serialize)]
|
||||
pub struct Vertex {
|
||||
/// Pixel coordinates
|
||||
pub pos: Pos2,
|
||||
/// Texel indices into the texture
|
||||
pub uv: (u16, u16),
|
||||
/// sRGBA, premultiplied alpha
|
||||
pub color: Color,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, serde_derive::Serialize)]
|
||||
pub struct Triangles {
|
||||
/// Draw as triangles (i.e. the length is a multiple of three)
|
||||
pub indices: Vec<u32>,
|
||||
pub vertices: Vec<Vertex>,
|
||||
}
|
||||
|
||||
/// Grouped by clip rectangles, in pixel coordinates
|
||||
pub type PaintBatches = Vec<(Rect, Triangles)>;
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
fn triangle(&mut self, a: u32, b: u32, c: u32) {
|
||||
self.indices.push(a);
|
||||
self.indices.push(b);
|
||||
self.indices.push(c);
|
||||
}
|
||||
|
||||
pub fn reserve_triangles(&mut self, additional_triangles: usize) {
|
||||
self.indices.reserve(3 * additional_triangles);
|
||||
}
|
||||
|
||||
pub fn reserve_vertices(&mut self, additional: usize) {
|
||||
self.vertices.reserve(additional);
|
||||
}
|
||||
|
||||
/// 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);
|
||||
|
||||
let top_right = Vertex {
|
||||
pos: pos2(bottom_right.pos.x, top_left.pos.y),
|
||||
uv: (bottom_right.uv.0, top_left.uv.1),
|
||||
color: top_left.color,
|
||||
};
|
||||
let botom_left = Vertex {
|
||||
pos: pos2(top_left.pos.x, bottom_right.pos.y),
|
||||
uv: (top_left.uv.0, bottom_right.uv.1),
|
||||
color: top_left.color,
|
||||
};
|
||||
self.vertices.push(top_left);
|
||||
self.vertices.push(top_right);
|
||||
self.vertices.push(botom_left);
|
||||
self.vertices.push(bottom_right);
|
||||
}
|
||||
|
||||
/// This is for platsform that only support 16-bit index buffers.
|
||||
/// Splits this mesh into many small if needed.
|
||||
/// All the returned meshes will have indices that fit into a `u16`.
|
||||
pub fn split_to_u16(self) -> Vec<Triangles> {
|
||||
const MAX_SIZE: u32 = 1 << 16;
|
||||
|
||||
if self.vertices.len() < MAX_SIZE as usize {
|
||||
return vec![self]; // Common-case optimization
|
||||
}
|
||||
|
||||
let mut output = vec![];
|
||||
let mut index_cursor = 0;
|
||||
|
||||
while index_cursor < self.indices.len() {
|
||||
let span_start = index_cursor;
|
||||
let mut min_vindex = self.indices[index_cursor];
|
||||
let mut max_vindex = self.indices[index_cursor];
|
||||
|
||||
while index_cursor < self.indices.len() {
|
||||
let (mut new_min, mut new_max) = (min_vindex, max_vindex);
|
||||
for i in 0..3 {
|
||||
let idx = self.indices[index_cursor + i];
|
||||
new_min = new_min.min(idx);
|
||||
new_max = new_max.max(idx);
|
||||
}
|
||||
|
||||
if new_max - new_min < MAX_SIZE {
|
||||
// Triangle fits
|
||||
min_vindex = new_min;
|
||||
max_vindex = new_max;
|
||||
index_cursor += 3;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
assert!(
|
||||
index_cursor > span_start,
|
||||
"One triangle spanned more than {} vertices",
|
||||
MAX_SIZE
|
||||
);
|
||||
|
||||
output.push(Triangles {
|
||||
indices: self.indices[span_start..index_cursor]
|
||||
.iter()
|
||||
.map(|vi| vi - min_vindex)
|
||||
.collect(),
|
||||
vertices: self.vertices[(min_vindex as usize)..=(max_vindex as usize)].to_vec(),
|
||||
});
|
||||
}
|
||||
output
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
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
|
||||
/// (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).
|
||||
normal: Vec2,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Path(Vec<PathPoint>);
|
||||
|
||||
impl Path {
|
||||
pub fn from_point_loop(points: &[Pos2]) -> Self {
|
||||
let mut path = Self::default();
|
||||
path.add_line_loop(points);
|
||||
path
|
||||
}
|
||||
|
||||
pub fn from_open_points(points: &[Pos2]) -> Self {
|
||||
let mut path = Self::default();
|
||||
path.add_open_points(points);
|
||||
path
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
self.0.clear();
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.0.is_empty()
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.0.len()
|
||||
}
|
||||
|
||||
pub fn reserve(&mut self, additional: usize) {
|
||||
self.0.reserve(additional)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn add_point(&mut self, pos: Pos2, normal: Vec2) {
|
||||
self.0.push(PathPoint { pos, normal });
|
||||
}
|
||||
|
||||
pub fn add_circle(&mut self, center: Pos2, radius: f32) {
|
||||
let n = (radius * 4.0).round() as i32; // TODO: tweak a bit more
|
||||
let n = clamp(n, 4..=64);
|
||||
self.reserve(n as usize);
|
||||
for i in 0..n {
|
||||
let angle = remap(i as f32, 0.0..=n as f32, 0.0..=TAU);
|
||||
let normal = vec2(angle.cos(), angle.sin());
|
||||
self.add_point(center + radius * normal, normal);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_line_segment(&mut self, points: [Pos2; 2]) {
|
||||
self.reserve(2);
|
||||
let normal = (points[1] - points[0]).normalized().rot90();
|
||||
self.add_point(points[0], normal);
|
||||
self.add_point(points[1], normal);
|
||||
}
|
||||
|
||||
pub fn add_open_points(&mut self, points: &[Pos2]) {
|
||||
let n = points.len();
|
||||
assert!(n >= 2);
|
||||
|
||||
if n == 2 {
|
||||
// Common case optimization:
|
||||
self.add_line_segment([points[0], points[1]]);
|
||||
} else {
|
||||
self.reserve(n);
|
||||
self.add_point(points[0], (points[1] - points[0]).normalized().rot90());
|
||||
for i in 1..n - 1 {
|
||||
let n0 = (points[i] - points[i - 1]).normalized().rot90(); // TODO: don't calculate each normal twice!
|
||||
let n1 = (points[i + 1] - points[i]).normalized().rot90(); // TODO: don't calculate each normal twice!
|
||||
let v = (n0 + n1) / 2.0;
|
||||
let normal = v / v.length_sq();
|
||||
self.add_point(points[i], normal); // TODO: handle VERY sharp turns better
|
||||
}
|
||||
self.add_point(
|
||||
points[n - 1],
|
||||
(points[n - 1] - points[n - 2]).normalized().rot90(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_line_loop(&mut self, points: &[Pos2]) {
|
||||
let n = points.len();
|
||||
assert!(n >= 2);
|
||||
self.reserve(n);
|
||||
|
||||
// TODO: optimize
|
||||
for i in 0..n {
|
||||
let n0 = (points[i] - points[(i + n - 1) % n]).normalized().rot90();
|
||||
let n1 = (points[(i + 1) % n] - points[i]).normalized().rot90();
|
||||
let v = (n0 + n1) / 2.0;
|
||||
let normal = v / v.length_sq();
|
||||
self.add_point(points[i], normal); // TODO: handle VERY sharp turns better
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_rectangle(&mut self, rect: Rect) {
|
||||
let min = rect.min;
|
||||
let max = rect.max;
|
||||
self.reserve(4);
|
||||
self.add_point(pos2(min.x, min.y), vec2(-1.0, -1.0));
|
||||
self.add_point(pos2(max.x, min.y), vec2(1.0, -1.0));
|
||||
self.add_point(pos2(max.x, max.y), vec2(1.0, 1.0));
|
||||
self.add_point(pos2(min.x, max.y), vec2(-1.0, 1.0));
|
||||
}
|
||||
|
||||
pub fn add_rounded_rectangle(&mut self, rect: Rect, corner_radius: f32) {
|
||||
let min = rect.min;
|
||||
let max = rect.max;
|
||||
|
||||
let cr = corner_radius
|
||||
.min(rect.width() * 0.5)
|
||||
.min(rect.height() * 0.5);
|
||||
|
||||
if cr <= 0.0 {
|
||||
self.add_rectangle(rect);
|
||||
} else {
|
||||
self.add_circle_quadrant(pos2(max.x - cr, max.y - cr), cr, 0.0);
|
||||
self.add_circle_quadrant(pos2(min.x + cr, max.y - cr), cr, 1.0);
|
||||
self.add_circle_quadrant(pos2(min.x + cr, min.y + cr), cr, 2.0);
|
||||
self.add_circle_quadrant(pos2(max.x - cr, min.y + cr), cr, 3.0);
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
pub fn add_circle_quadrant(&mut self, center: Pos2, radius: f32, quadrant: f32) {
|
||||
let n = (radius * 0.5).round() as i32; // TODO: tweak a bit more
|
||||
let n = clamp(n, 2..=32);
|
||||
self.reserve(n as usize + 1);
|
||||
const RIGHT_ANGLE: f32 = TAU / 4.0;
|
||||
for i in 0..=n {
|
||||
let angle = remap(
|
||||
i as f32,
|
||||
0.0..=n as f32,
|
||||
quadrant * RIGHT_ANGLE..=(quadrant + 1.0) * RIGHT_ANGLE,
|
||||
);
|
||||
let normal = vec2(angle.cos(), angle.sin());
|
||||
self.add_point(center + radius * normal, normal);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
pub enum PathType {
|
||||
Open,
|
||||
Closed,
|
||||
}
|
||||
use self::PathType::{Closed, Open};
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct PaintOptions {
|
||||
pub anti_alias: bool,
|
||||
/// Size of a pixel in points, e.g. 0.5
|
||||
pub aa_size: f32,
|
||||
pub debug_paint_clip_rects: bool,
|
||||
}
|
||||
|
||||
impl Default for PaintOptions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
anti_alias: true,
|
||||
aa_size: 1.0,
|
||||
debug_paint_clip_rects: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fill_closed_path(
|
||||
triangles: &mut Triangles,
|
||||
options: PaintOptions,
|
||||
path: &[PathPoint],
|
||||
color: Color,
|
||||
) {
|
||||
if color == color::TRANSPARENT {
|
||||
return;
|
||||
}
|
||||
|
||||
let n = path.len() as u32;
|
||||
let vert = |pos, color| Vertex {
|
||||
pos,
|
||||
uv: WHITE_UV,
|
||||
color,
|
||||
};
|
||||
if options.anti_alias {
|
||||
triangles.reserve_triangles(3 * n as usize);
|
||||
triangles.reserve_vertices(2 * n as usize);
|
||||
let color_outer = color::TRANSPARENT;
|
||||
let idx_inner = triangles.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);
|
||||
}
|
||||
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);
|
||||
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)));
|
||||
for i in 2..n {
|
||||
triangles.triangle(idx, idx + i - 1, idx + i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn paint_path_outline(
|
||||
triangles: &mut Triangles,
|
||||
options: PaintOptions,
|
||||
path_type: PathType,
|
||||
path: &[PathPoint],
|
||||
style: &LineStyle,
|
||||
) {
|
||||
if style.color == color::TRANSPARENT {
|
||||
return;
|
||||
}
|
||||
|
||||
let n = path.len() as u32;
|
||||
let idx = triangles.vertices.len() as u32;
|
||||
|
||||
let vert = |pos, color| Vertex {
|
||||
pos,
|
||||
uv: WHITE_UV,
|
||||
color,
|
||||
};
|
||||
|
||||
if options.anti_alias {
|
||||
let color_inner = style.color;
|
||||
let color_outer = color::TRANSPARENT;
|
||||
|
||||
let thin_line = style.width <= options.aa_size;
|
||||
if thin_line {
|
||||
/*
|
||||
We paint the line using three edges: outer, inner, outer.
|
||||
|
||||
. o i o outer, inner, outer
|
||||
. |---| aa_size (pixel width)
|
||||
*/
|
||||
|
||||
// Fade out as it gets thinner:
|
||||
let color_inner = mul_color(color_inner, style.width / options.aa_size);
|
||||
if color_inner == color::TRANSPARENT {
|
||||
return;
|
||||
}
|
||||
|
||||
triangles.reserve_triangles(4 * n as usize);
|
||||
triangles.reserve_vertices(3 * n as usize);
|
||||
|
||||
let mut i0 = n - 1;
|
||||
for i1 in 0..n {
|
||||
let connect_with_previous = path_type == PathType::Closed || i1 > 0;
|
||||
let p1 = &path[i1 as usize];
|
||||
let p = p1.pos;
|
||||
let n = p1.normal;
|
||||
triangles
|
||||
.vertices
|
||||
.push(vert(p + n * options.aa_size, color_outer));
|
||||
triangles.vertices.push(vert(p, color_inner));
|
||||
triangles
|
||||
.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);
|
||||
|
||||
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);
|
||||
}
|
||||
i0 = i1;
|
||||
}
|
||||
} else {
|
||||
// TODO: line caps for really thick lines?
|
||||
|
||||
/*
|
||||
We paint the line using four edges: outer, inner, inner, outer
|
||||
|
||||
. o i p i o outer, inner, point, inner, outer
|
||||
. |---| aa_size (pixel width)
|
||||
. |--------------| width
|
||||
. |---------| outer_rad
|
||||
. |-----| inner_rad
|
||||
*/
|
||||
|
||||
triangles.reserve_triangles(6 * n as usize);
|
||||
triangles.reserve_vertices(4 * n as usize);
|
||||
|
||||
let mut i0 = n - 1;
|
||||
for i1 in 0..n {
|
||||
let connect_with_previous = path_type == PathType::Closed || i1 > 0;
|
||||
let inner_rad = 0.5 * (style.width - options.aa_size);
|
||||
let outer_rad = 0.5 * (style.width + options.aa_size);
|
||||
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));
|
||||
|
||||
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);
|
||||
|
||||
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);
|
||||
|
||||
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);
|
||||
}
|
||||
i0 = i1;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
triangles.reserve_triangles(2 * n as usize);
|
||||
triangles.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(
|
||||
idx + (2 * i + 0) % (2 * n),
|
||||
idx + (2 * i + 1) % (2 * n),
|
||||
idx + (2 * i + 2) % (2 * n),
|
||||
);
|
||||
triangles.triangle(
|
||||
idx + (2 * i + 2) % (2 * n),
|
||||
idx + (2 * i + 1) % (2 * n),
|
||||
idx + (2 * i + 3) % (2 * n),
|
||||
);
|
||||
}
|
||||
|
||||
let thin_line = style.width <= options.aa_size;
|
||||
if thin_line {
|
||||
// Fade out thin lines rather than making them thinner
|
||||
let radius = options.aa_size / 2.0;
|
||||
let color = mul_color(style.color, style.width / options.aa_size);
|
||||
if color == color::TRANSPARENT {
|
||||
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));
|
||||
}
|
||||
} else {
|
||||
let radius = style.width / 2.0;
|
||||
for p in path {
|
||||
triangles
|
||||
.vertices
|
||||
.push(vert(p.pos + radius * p.normal, style.color));
|
||||
triangles
|
||||
.vertices
|
||||
.push(vert(p.pos - radius * p.normal, style.color));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn mul_color(color: Color, factor: f32) -> Color {
|
||||
// TODO: sRGBA correct fading
|
||||
debug_assert!(0.0 <= factor && factor <= 1.0);
|
||||
Color {
|
||||
r: (f32::from(color.r) * factor).round() as u8,
|
||||
g: (f32::from(color.g) * factor).round() as u8,
|
||||
b: (f32::from(color.b) * factor).round() as u8,
|
||||
a: (f32::from(color.a) * factor).round() as u8,
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// `reused_path`: only used to reuse memory
|
||||
pub fn paint_command_into_triangles(
|
||||
reused_path: &mut Path,
|
||||
options: PaintOptions,
|
||||
fonts: &Fonts,
|
||||
command: PaintCmd,
|
||||
out: &mut Triangles,
|
||||
) {
|
||||
let path = reused_path;
|
||||
path.clear();
|
||||
|
||||
match command {
|
||||
PaintCmd::Circle {
|
||||
center,
|
||||
fill,
|
||||
outline,
|
||||
radius,
|
||||
} => {
|
||||
path.add_circle(center, radius);
|
||||
if let Some(fill) = fill {
|
||||
fill_closed_path(out, options, &path.0, fill);
|
||||
}
|
||||
if let Some(outline) = outline {
|
||||
paint_path_outline(out, options, Closed, &path.0, &outline);
|
||||
}
|
||||
}
|
||||
PaintCmd::Triangles(triangles) => {
|
||||
out.append(&triangles);
|
||||
}
|
||||
PaintCmd::LineSegment { points, style } => {
|
||||
path.add_line_segment(points);
|
||||
paint_path_outline(out, options, Open, &path.0, &style);
|
||||
}
|
||||
PaintCmd::Path {
|
||||
path,
|
||||
closed,
|
||||
fill,
|
||||
outline,
|
||||
} => {
|
||||
if path.len() >= 2 {
|
||||
if let Some(fill) = fill {
|
||||
debug_assert!(
|
||||
closed,
|
||||
"You asked to fill a path that is not closed. That makes no sense."
|
||||
);
|
||||
fill_closed_path(out, options, &path.0, fill);
|
||||
}
|
||||
if let Some(outline) = outline {
|
||||
let typ = if closed { Closed } else { Open };
|
||||
paint_path_outline(out, options, typ, &path.0, &outline);
|
||||
}
|
||||
}
|
||||
}
|
||||
PaintCmd::Rect {
|
||||
corner_radius,
|
||||
fill,
|
||||
outline,
|
||||
mut rect,
|
||||
} => {
|
||||
// Common bug is to accidentally create an infinitely sized ractangle.
|
||||
// Make sure we can visualize that:
|
||||
rect.min = rect.min.max(pos2(-1e7, -1e7));
|
||||
rect.max = rect.max.min(pos2(1e7, 1e7));
|
||||
|
||||
path.add_rounded_rectangle(rect, corner_radius);
|
||||
if let Some(fill) = fill {
|
||||
fill_closed_path(out, options, &path.0, fill);
|
||||
}
|
||||
if let Some(outline) = outline {
|
||||
paint_path_outline(out, options, Closed, &path.0, &outline);
|
||||
}
|
||||
}
|
||||
PaintCmd::Text {
|
||||
pos,
|
||||
galley,
|
||||
text_style,
|
||||
color,
|
||||
} => {
|
||||
galley.sanity_check();
|
||||
|
||||
let num_chars = galley.text.chars().count();
|
||||
out.reserve_triangles(num_chars * 2);
|
||||
out.reserve_vertices(num_chars * 4);
|
||||
|
||||
let text_offset = vec2(0.0, 1.0); // Eye-balled for buttons. TODO: why is this needed?
|
||||
|
||||
let font = &fonts[text_style];
|
||||
let mut chars = galley.text.chars();
|
||||
for line in &galley.lines {
|
||||
for x_offset in line.x_offsets.iter().take(line.x_offsets.len() - 1) {
|
||||
let c = chars.next().unwrap();
|
||||
if let Some(glyph) = font.uv_rect(c) {
|
||||
let mut top_left = Vertex {
|
||||
pos: pos + glyph.offset + vec2(*x_offset, line.y_min) + text_offset,
|
||||
uv: glyph.min,
|
||||
color,
|
||||
};
|
||||
top_left.pos.x = font.round_to_pixel(top_left.pos.x); // Pixel-perfection.
|
||||
top_left.pos.y = font.round_to_pixel(top_left.pos.y); // Pixel-perfection.
|
||||
let bottom_right = Vertex {
|
||||
pos: top_left.pos + glyph.size,
|
||||
uv: glyph.max,
|
||||
color,
|
||||
};
|
||||
out.add_rect(top_left, bottom_right);
|
||||
}
|
||||
}
|
||||
}
|
||||
assert_eq!(chars.next(), None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Turns `PaintCmd`:s into sets of triangles
|
||||
pub fn paint_commands_into_triangles(
|
||||
options: PaintOptions,
|
||||
fonts: &Fonts,
|
||||
commands: Vec<(Rect, PaintCmd)>,
|
||||
) -> Vec<(Rect, Triangles)> {
|
||||
let mut reused_path = Path::default();
|
||||
|
||||
let mut batches = PaintBatches::default();
|
||||
for (clip_rect, cmd) in commands {
|
||||
// TODO: cull(clip_rect, cmd)
|
||||
|
||||
if batches.is_empty() || batches.last().unwrap().0 != clip_rect {
|
||||
batches.push((clip_rect, Triangles::default()));
|
||||
}
|
||||
|
||||
let out = &mut batches.last_mut().unwrap().1;
|
||||
paint_command_into_triangles(&mut reused_path, options, fonts, cmd, out);
|
||||
}
|
||||
|
||||
if options.debug_paint_clip_rects {
|
||||
for (clip_rect, triangles) in &mut batches {
|
||||
paint_command_into_triangles(
|
||||
&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))),
|
||||
},
|
||||
triangles,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
batches
|
||||
}
|
||||
91
egui/src/paint/texture_atlas.rs
Normal file
91
egui/src/paint/texture_atlas.rs
Normal file
@@ -0,0 +1,91 @@
|
||||
#[derive(Clone, Default)]
|
||||
pub struct Texture {
|
||||
/// e.g. a hash of the data. Use this to detect changes!
|
||||
pub id: u64, // TODO
|
||||
pub width: usize,
|
||||
pub height: usize,
|
||||
pub pixels: Vec<u8>,
|
||||
}
|
||||
|
||||
impl std::ops::Index<(usize, usize)> for Texture {
|
||||
type Output = u8;
|
||||
|
||||
fn index(&self, (x, y): (usize, usize)) -> &u8 {
|
||||
assert!(x < self.width);
|
||||
assert!(y < self.height);
|
||||
&self.pixels[y * self.width + x]
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::IndexMut<(usize, usize)> for Texture {
|
||||
fn index_mut(&mut self, (x, y): (usize, usize)) -> &mut u8 {
|
||||
assert!(x < self.width);
|
||||
assert!(y < self.height);
|
||||
&mut self.pixels[y * self.width + x]
|
||||
}
|
||||
}
|
||||
|
||||
/// A texture pixels, used for fonts.
|
||||
#[derive(Clone, Default)]
|
||||
pub struct TextureAtlas {
|
||||
texture: Texture,
|
||||
|
||||
/// Used for when adding new rects
|
||||
cursor: (usize, usize),
|
||||
row_height: usize,
|
||||
}
|
||||
|
||||
impl TextureAtlas {
|
||||
pub fn new(width: usize, height: usize) -> Self {
|
||||
Self {
|
||||
texture: Texture {
|
||||
id: 0,
|
||||
width,
|
||||
height,
|
||||
pixels: vec![0; width * height],
|
||||
},
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn texture(&self) -> &Texture {
|
||||
&self.texture
|
||||
}
|
||||
|
||||
pub fn texture_mut(&mut self) -> &mut Texture {
|
||||
self.texture.id += 1;
|
||||
&mut self.texture
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
self.cursor = (0, 0);
|
||||
self.row_height = 0;
|
||||
}
|
||||
|
||||
/// Returns the coordinates of where the rect ended up.
|
||||
pub fn allocate(&mut self, (w, h): (usize, usize)) -> (usize, usize) {
|
||||
assert!(w <= self.texture.width);
|
||||
if self.cursor.0 + w > self.texture.width {
|
||||
// New row:
|
||||
self.cursor.0 = 0;
|
||||
self.cursor.1 += self.row_height;
|
||||
self.row_height = 0;
|
||||
}
|
||||
|
||||
self.row_height = self.row_height.max(h);
|
||||
while self.cursor.1 + self.row_height >= self.texture.height {
|
||||
self.texture.height *= 2;
|
||||
}
|
||||
|
||||
if self.texture.width * self.texture.height > self.texture.pixels.len() {
|
||||
self.texture
|
||||
.pixels
|
||||
.resize(self.texture.width * self.texture.height, 0);
|
||||
}
|
||||
|
||||
let pos = self.cursor;
|
||||
self.cursor.0 += w;
|
||||
self.texture.id += 1;
|
||||
(pos.0 as usize, pos.1 as usize)
|
||||
}
|
||||
}
|
||||
233
egui/src/style.rs
Normal file
233
egui/src/style.rs
Normal file
@@ -0,0 +1,233 @@
|
||||
#![allow(clippy::if_same_then_else)]
|
||||
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
|
||||
use crate::{color::*, math::*, paint::LineStyle, types::*};
|
||||
|
||||
// TODO: split into Spacing and Style?
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
|
||||
pub struct Style {
|
||||
/// Horizontal and vertical padding within a window frame.
|
||||
pub window_padding: Vec2,
|
||||
|
||||
/// Button size is text size plus this on each side
|
||||
pub button_padding: Vec2,
|
||||
|
||||
/// Horizontal and vertical spacing between widgets
|
||||
pub item_spacing: Vec2,
|
||||
|
||||
/// Indent collapsing regions etc by this much.
|
||||
pub indent: f32,
|
||||
|
||||
/// Anything clickable is (at least) this wide.
|
||||
pub clickable_diameter: f32,
|
||||
|
||||
/// Checkboxes, radio button and collapsing headers have an icon at the start.
|
||||
/// The text starts after this many pixels.
|
||||
pub start_icon_width: f32,
|
||||
|
||||
// -----------------------------------------------
|
||||
// Purely visual:
|
||||
pub interact: Interact,
|
||||
|
||||
// TODO: an WidgetStyle ?
|
||||
pub text_color: Color,
|
||||
|
||||
/// For stuff like check marks in check boxes.
|
||||
pub line_width: f32,
|
||||
|
||||
pub thin_outline: LineStyle,
|
||||
|
||||
/// e.g. the background of windows
|
||||
pub background_fill: Color,
|
||||
|
||||
/// e.g. the background of the slider or text edit
|
||||
pub dark_bg_color: Color,
|
||||
|
||||
pub cursor_blink_hz: f32,
|
||||
pub text_cursor_width: f32,
|
||||
|
||||
// TODO: add ability to disable animations!
|
||||
/// How many seconds a typical animation should last
|
||||
pub animation_time: f32,
|
||||
|
||||
pub window: Window,
|
||||
|
||||
pub menu_bar: MenuBar,
|
||||
|
||||
/// Allow child widgets to be just on the border and still have an outline with some thickness
|
||||
pub clip_rect_margin: f32,
|
||||
|
||||
// -----------------------------------------------
|
||||
// Debug rendering:
|
||||
pub debug_widget_rects: bool,
|
||||
}
|
||||
|
||||
impl Default for Style {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
window_padding: vec2(6.0, 6.0),
|
||||
button_padding: vec2(5.0, 3.0),
|
||||
item_spacing: vec2(8.0, 4.0),
|
||||
indent: 21.0,
|
||||
clickable_diameter: 22.0,
|
||||
start_icon_width: 14.0,
|
||||
interact: Default::default(),
|
||||
text_color: gray(160, 255),
|
||||
line_width: 1.0,
|
||||
thin_outline: LineStyle::new(0.5, GRAY),
|
||||
background_fill: gray(32, 250),
|
||||
dark_bg_color: gray(0, 140),
|
||||
cursor_blink_hz: 1.0,
|
||||
text_cursor_width: 2.0,
|
||||
animation_time: 1.0 / 15.0,
|
||||
window: Window::default(),
|
||||
menu_bar: MenuBar::default(),
|
||||
clip_rect_margin: 3.0,
|
||||
debug_widget_rects: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
|
||||
pub struct Interact {
|
||||
pub active: WidgetStyle,
|
||||
pub hovered: WidgetStyle,
|
||||
pub inactive: WidgetStyle,
|
||||
}
|
||||
|
||||
impl Default for Interact {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
active: WidgetStyle {
|
||||
bg_fill: Some(gray(0, 128)),
|
||||
fill: srgba(120, 120, 200, 255),
|
||||
stroke_color: WHITE,
|
||||
stroke_width: 2.0,
|
||||
rect_outline: Some(LineStyle::new(2.0, WHITE)),
|
||||
corner_radius: 5.0,
|
||||
},
|
||||
hovered: WidgetStyle {
|
||||
bg_fill: None,
|
||||
fill: srgba(100, 100, 150, 255),
|
||||
stroke_color: gray(240, 255),
|
||||
stroke_width: 1.5,
|
||||
rect_outline: Some(LineStyle::new(1.0, WHITE)),
|
||||
corner_radius: 5.0,
|
||||
},
|
||||
inactive: WidgetStyle {
|
||||
bg_fill: None,
|
||||
fill: srgba(60, 60, 80, 255),
|
||||
stroke_color: gray(210, 255), // Mustn't look grayed out!
|
||||
stroke_width: 1.0,
|
||||
rect_outline: Some(LineStyle::new(1.0, white(128))),
|
||||
corner_radius: 0.0,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Interact {
|
||||
pub fn style(&self, interact: &InteractInfo) -> &WidgetStyle {
|
||||
if interact.active {
|
||||
&self.active
|
||||
} else if interact.hovered {
|
||||
&self.hovered
|
||||
} else {
|
||||
&self.inactive
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
|
||||
pub struct WidgetStyle {
|
||||
/// Background color of widget
|
||||
pub bg_fill: Option<Color>,
|
||||
|
||||
/// Fill color of the interactive part of a component (slider grab, checkbox, ...)
|
||||
/// When you need a fill.
|
||||
pub fill: Color,
|
||||
|
||||
/// Stroke and text color of the interactive part of a component (button, slider grab, checkbox, ...)
|
||||
pub stroke_color: Color,
|
||||
|
||||
/// For lines etc
|
||||
pub stroke_width: f32,
|
||||
|
||||
/// For surrounding rectangle of things that need it,
|
||||
/// like buttons, the box of the checkbox, etc.
|
||||
pub rect_outline: Option<LineStyle>,
|
||||
|
||||
/// Button frames etdc
|
||||
pub corner_radius: f32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
|
||||
pub struct Window {
|
||||
pub corner_radius: f32,
|
||||
}
|
||||
|
||||
impl Default for Window {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
corner_radius: 10.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
|
||||
pub struct MenuBar {
|
||||
pub height: f32,
|
||||
}
|
||||
|
||||
impl Default for MenuBar {
|
||||
fn default() -> Self {
|
||||
Self { height: 16.0 }
|
||||
}
|
||||
}
|
||||
|
||||
impl Style {
|
||||
/// Use this style for interactive things
|
||||
pub fn interact(&self, interact: &InteractInfo) -> &WidgetStyle {
|
||||
self.interact.style(interact)
|
||||
}
|
||||
|
||||
/// Returns small icon rectangle and big icon rectangle
|
||||
pub fn icon_rectangles(&self, rect: Rect) -> (Rect, Rect) {
|
||||
let box_side = self.start_icon_width;
|
||||
let big_icon_rect = Rect::from_center_size(
|
||||
pos2(rect.left() + box_side / 2.0, rect.center().y),
|
||||
vec2(box_side, box_side),
|
||||
);
|
||||
|
||||
let small_rect_side = 8.0; // TODO: make a parameter
|
||||
let small_icon_rect =
|
||||
Rect::from_center_size(big_icon_rect.center(), Vec2::splat(small_rect_side));
|
||||
|
||||
(small_icon_rect, big_icon_rect)
|
||||
}
|
||||
}
|
||||
|
||||
impl Style {
|
||||
#[rustfmt::skip]
|
||||
pub fn ui(&mut self, ui: &mut crate::Ui) {
|
||||
use crate::{widgets::*};
|
||||
if ui.add(Button::new("Reset style")).clicked {
|
||||
*self = Default::default();
|
||||
}
|
||||
|
||||
ui.add(Checkbox::new(&mut self.debug_widget_rects, "Paint debug rectangles around widgets"));
|
||||
|
||||
ui.add(Slider::f32(&mut self.item_spacing.x, 0.0..=10.0).text("item_spacing.x").precision(0));
|
||||
ui.add(Slider::f32(&mut self.item_spacing.y, 0.0..=10.0).text("item_spacing.y").precision(0));
|
||||
ui.add(Slider::f32(&mut self.window_padding.x, 0.0..=10.0).text("window_padding.x").precision(0));
|
||||
ui.add(Slider::f32(&mut self.window_padding.y, 0.0..=10.0).text("window_padding.y").precision(0));
|
||||
ui.add(Slider::f32(&mut self.indent, 0.0..=100.0).text("indent").precision(0));
|
||||
ui.add(Slider::f32(&mut self.button_padding.x, 0.0..=20.0).text("button_padding.x").precision(0));
|
||||
ui.add(Slider::f32(&mut self.button_padding.y, 0.0..=20.0).text("button_padding.y").precision(0));
|
||||
ui.add(Slider::f32(&mut self.clickable_diameter, 0.0..=60.0).text("clickable_diameter").precision(0));
|
||||
ui.add(Slider::f32(&mut self.start_icon_width, 0.0..=60.0).text("start_icon_width").precision(0));
|
||||
ui.add(Slider::f32(&mut self.line_width, 0.0..=10.0).text("line_width").precision(1));
|
||||
ui.add(Slider::f32(&mut self.animation_time, 0.0..=1.0).text("animation_time").precision(2));
|
||||
}
|
||||
}
|
||||
172
egui/src/types.rs
Normal file
172
egui/src/types.rs
Normal file
@@ -0,0 +1,172 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use serde_derive::Serialize;
|
||||
|
||||
use crate::{math::Rect, Context, Ui};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[derive(Clone, Default, Serialize)]
|
||||
pub struct Output {
|
||||
pub cursor_icon: CursorIcon,
|
||||
|
||||
/// If set, open this url.
|
||||
pub open_url: Option<String>,
|
||||
|
||||
/// Response to Event::Copy or Event::Cut. Ignore if empty.
|
||||
pub copied_text: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum CursorIcon {
|
||||
Default,
|
||||
/// Pointing hand, used for e.g. web links
|
||||
PointingHand,
|
||||
ResizeHorizontal,
|
||||
ResizeNeSw,
|
||||
ResizeNwSe,
|
||||
ResizeVertical,
|
||||
Text,
|
||||
}
|
||||
|
||||
impl Default for CursorIcon {
|
||||
fn default() -> Self {
|
||||
Self::Default
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize)]
|
||||
pub struct InteractInfo {
|
||||
/// The mouse is hovering above this thing
|
||||
pub hovered: bool,
|
||||
|
||||
/// The mouse pressed this thing ealier, and now released on this thing too.
|
||||
pub clicked: bool,
|
||||
|
||||
pub double_clicked: bool,
|
||||
|
||||
/// The mouse is interacting with this thing (e.g. dragging it or holding it)
|
||||
pub active: bool,
|
||||
|
||||
/// The region of the screen we are talking about
|
||||
pub rect: Rect,
|
||||
}
|
||||
|
||||
impl InteractInfo {
|
||||
pub fn nothing() -> Self {
|
||||
Self {
|
||||
hovered: false,
|
||||
clicked: false,
|
||||
double_clicked: false,
|
||||
active: false,
|
||||
rect: Rect::nothing(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn union(self, other: Self) -> Self {
|
||||
Self {
|
||||
hovered: self.hovered || other.hovered,
|
||||
clicked: self.clicked || other.clicked,
|
||||
double_clicked: self.double_clicked || other.double_clicked,
|
||||
active: self.active || other.active,
|
||||
rect: self.rect.union(other.rect),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// TODO: rename GuiResponse
|
||||
pub struct GuiResponse {
|
||||
/// The mouse is hovering above this
|
||||
pub hovered: bool,
|
||||
|
||||
/// The mouse clicked this thing this frame
|
||||
pub clicked: bool,
|
||||
|
||||
pub double_clicked: bool,
|
||||
|
||||
/// The mouse is interacting with this thing (e.g. dragging it)
|
||||
pub active: bool,
|
||||
|
||||
/// The area of the screen we are talking about
|
||||
pub rect: Rect,
|
||||
|
||||
/// Used for optionally showing a tooltip
|
||||
pub ctx: Arc<Context>,
|
||||
}
|
||||
|
||||
impl GuiResponse {
|
||||
/// Show some stuff if the item was hovered
|
||||
pub fn tooltip(&mut self, add_contents: impl FnOnce(&mut Ui)) -> &mut Self {
|
||||
if self.hovered {
|
||||
crate::containers::show_tooltip(&self.ctx, add_contents);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Show this text if the item was hovered
|
||||
pub fn tooltip_text(&mut self, text: impl Into<String>) -> &mut Self {
|
||||
self.tooltip(|popup| {
|
||||
popup.add(crate::widgets::Label::new(text));
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<InteractInfo> for GuiResponse {
|
||||
fn into(self) -> InteractInfo {
|
||||
InteractInfo {
|
||||
hovered: self.hovered,
|
||||
clicked: self.clicked,
|
||||
double_clicked: self.double_clicked,
|
||||
active: self.active,
|
||||
rect: self.rect,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// What sort of interaction is a widget sensitive to?
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct Sense {
|
||||
/// buttons, sliders, windows ...
|
||||
pub click: bool,
|
||||
|
||||
/// sliders, windows, scroll bars, scroll areas ...
|
||||
pub drag: bool,
|
||||
}
|
||||
|
||||
impl Sense {
|
||||
pub fn nothing() -> Self {
|
||||
Self {
|
||||
click: false,
|
||||
drag: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn click() -> Self {
|
||||
Self {
|
||||
click: true,
|
||||
drag: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn drag() -> Self {
|
||||
Self {
|
||||
click: false,
|
||||
drag: true,
|
||||
}
|
||||
}
|
||||
|
||||
/// e.g. a slider or window
|
||||
pub fn click_and_drag() -> Self {
|
||||
Self {
|
||||
click: true,
|
||||
drag: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
692
egui/src/ui.rs
Normal file
692
egui/src/ui.rs
Normal file
@@ -0,0 +1,692 @@
|
||||
use std::{hash::Hash, sync::Arc};
|
||||
|
||||
use crate::{color::*, containers::*, layout::*, paint::*, widgets::*, *};
|
||||
|
||||
/// Represents a region of the screen
|
||||
/// with a type of layout (horizontal or vertical).
|
||||
pub struct Ui {
|
||||
/// How we access input, output and memory
|
||||
ctx: Arc<Context>,
|
||||
|
||||
/// ID of this ui.
|
||||
/// Generated based on id of parent ui together with
|
||||
/// another source of child identity (e.g. window title).
|
||||
/// Acts like a namespace for child uis.
|
||||
/// Hopefully unique.
|
||||
id: Id,
|
||||
|
||||
/// Where to put the graphics output of this Ui
|
||||
layer: Layer,
|
||||
|
||||
/// Everything painted in this ui will be clipped against this.
|
||||
/// This means nothing outside of this rectangle will be visible on screen.
|
||||
clip_rect: Rect,
|
||||
|
||||
/// The `rect` represents where in screen-space the ui is
|
||||
/// and its max size (original available_space).
|
||||
/// Note that the size may be infinite in one or both dimensions.
|
||||
/// The widgets will TRY to fit within the rect,
|
||||
/// but may overflow (which you will see in child_bounds).
|
||||
/// Some widgets (like separator lines) will try to fill the full desired width of the ui.
|
||||
/// If the desired size is zero, it is a signal that child widgets should be as small as possible.
|
||||
/// If the desired size is initie, it is a signal that child widgets should take up as much room as they want.
|
||||
desired_rect: Rect, // TODO: rename as max_rect ?
|
||||
|
||||
/// Bounding box of all children.
|
||||
/// This is used to see how large a ui actually
|
||||
/// needs to be after all children has been added.
|
||||
/// You can think of this as the minimum size.
|
||||
child_bounds: Rect, // TODO: rename as min_rect ?
|
||||
|
||||
/// Overide default style in this ui
|
||||
style: Style,
|
||||
|
||||
layout: Layout,
|
||||
|
||||
/// Where the next widget will be put.
|
||||
/// Progresses along self.dir.
|
||||
/// Initially set to rect.min
|
||||
/// If something has already been added, this will point ot style.item_spacing beyond the latest child.
|
||||
/// The cursor can thus be style.item_spacing pixels outside of the child_bounds.
|
||||
cursor: Pos2, // TODO: move into Layout?
|
||||
}
|
||||
|
||||
impl Ui {
|
||||
// ------------------------------------------------------------------------
|
||||
// Creation:
|
||||
|
||||
pub fn new(ctx: Arc<Context>, layer: Layer, id: Id, rect: Rect) -> Self {
|
||||
let style = ctx.style();
|
||||
Ui {
|
||||
ctx,
|
||||
id,
|
||||
layer,
|
||||
clip_rect: rect.expand(style.clip_rect_margin),
|
||||
desired_rect: rect,
|
||||
child_bounds: Rect::from_min_size(rect.min, Vec2::zero()), // TODO: Rect::nothing() ?
|
||||
style,
|
||||
layout: Default::default(),
|
||||
cursor: rect.min,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn child_ui(&self, child_rect: Rect) -> Self {
|
||||
// let clip_rect = self
|
||||
// .clip_rect
|
||||
// .intersect(&child_rect.expand(self.style().clip_rect_margin));
|
||||
let clip_rect = self.clip_rect(); // Keep it unless the child explciitly desires differently
|
||||
Ui {
|
||||
ctx: self.ctx.clone(),
|
||||
id: self.id,
|
||||
layer: self.layer,
|
||||
clip_rect,
|
||||
desired_rect: child_rect,
|
||||
child_bounds: Rect::from_min_size(child_rect.min, Vec2::zero()), // TODO: Rect::nothing() ?
|
||||
style: self.style,
|
||||
layout: self.layout,
|
||||
cursor: child_rect.min,
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------
|
||||
|
||||
pub fn round_to_pixel(&self, point: f32) -> f32 {
|
||||
self.ctx.round_to_pixel(point)
|
||||
}
|
||||
|
||||
pub fn round_vec_to_pixels(&self, vec: Vec2) -> Vec2 {
|
||||
self.ctx.round_vec_to_pixels(vec)
|
||||
}
|
||||
|
||||
pub fn round_pos_to_pixels(&self, pos: Pos2) -> Pos2 {
|
||||
self.ctx.round_pos_to_pixels(pos)
|
||||
}
|
||||
|
||||
pub fn id(&self) -> Id {
|
||||
self.id
|
||||
}
|
||||
|
||||
/// Options for this ui, and any child uis we may spawn.
|
||||
pub fn style(&self) -> &Style {
|
||||
&self.style
|
||||
}
|
||||
|
||||
pub fn set_style(&mut self, style: Style) {
|
||||
self.style = style
|
||||
}
|
||||
|
||||
pub fn ctx(&self) -> &Arc<Context> {
|
||||
&self.ctx
|
||||
}
|
||||
|
||||
pub fn input(&self) -> &InputState {
|
||||
self.ctx.input()
|
||||
}
|
||||
|
||||
pub fn memory(&self) -> parking_lot::MutexGuard<'_, Memory> {
|
||||
self.ctx.memory()
|
||||
}
|
||||
|
||||
pub fn output(&self) -> parking_lot::MutexGuard<'_, Output> {
|
||||
self.ctx.output()
|
||||
}
|
||||
|
||||
pub fn fonts(&self) -> &Fonts {
|
||||
self.ctx.fonts()
|
||||
}
|
||||
|
||||
/// Screen-space rectangle for clipping what we paint in this ui.
|
||||
/// This is used, for instance, to avoid painting outside a window that is smaller
|
||||
/// than its contents.
|
||||
pub fn clip_rect(&self) -> Rect {
|
||||
self.clip_rect
|
||||
}
|
||||
|
||||
pub fn set_clip_rect(&mut self, clip_rect: Rect) {
|
||||
self.clip_rect = clip_rect;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
/// Screen-space position of this Ui.
|
||||
/// This may have moved from its original if a child overflowed to the left or up (rare).
|
||||
pub fn top_left(&self) -> Pos2 {
|
||||
// If a child doesn't fit in desired_rect, we have effectively expanded:
|
||||
self.desired_rect.min.min(self.child_bounds.min)
|
||||
}
|
||||
|
||||
/// Screen-space position of the current bottom right corner of this Ui.
|
||||
/// This may move when we add children that overflow our desired rectangle bounds.
|
||||
/// This position may be at inifnity if the desired rect is initinite,
|
||||
/// which mappens when a parent widget says "be as big as you want to be".
|
||||
pub fn bottom_right(&self) -> Pos2 {
|
||||
// If a child doesn't fit in desired_rect, we have effectively expanded:
|
||||
self.desired_rect.max.max(self.child_bounds.max)
|
||||
}
|
||||
|
||||
/// Position and current size of the ui.
|
||||
/// The size is the maximum of the origional (minimum/desired) size and
|
||||
/// the size of the containted children.
|
||||
pub fn rect(&self) -> Rect {
|
||||
Rect::from_min_max(self.top_left(), self.bottom_right())
|
||||
}
|
||||
|
||||
/// This is like `rect()`, but will never be infinite.
|
||||
/// If the desired rect is infinite ("be as big as you want")
|
||||
/// this will be bounded by child bounds.
|
||||
pub fn rect_finite(&self) -> Rect {
|
||||
let mut bottom_right = self.child_bounds.max;
|
||||
if self.desired_rect.max.x.is_finite() {
|
||||
bottom_right.x = bottom_right.x.max(self.desired_rect.max.x);
|
||||
}
|
||||
if self.desired_rect.max.y.is_finite() {
|
||||
bottom_right.y = bottom_right.y.max(self.desired_rect.max.y);
|
||||
}
|
||||
|
||||
Rect::from_min_max(self.top_left(), bottom_right)
|
||||
}
|
||||
|
||||
/// Set the width of the ui.
|
||||
/// You won't be able to shrink it beyond its current child bounds.
|
||||
pub fn set_desired_width(&mut self, width: f32) {
|
||||
let min_width = self.child_bounds.max.x - self.top_left().x;
|
||||
let width = width.max(min_width);
|
||||
self.desired_rect.max.x = self.top_left().x + width;
|
||||
}
|
||||
|
||||
/// Set the height of the ui.
|
||||
/// You won't be able to shrink it beyond its current child bounds.
|
||||
pub fn set_desired_height(&mut self, height: f32) {
|
||||
let min_height = self.child_bounds.max.y - self.top_left().y;
|
||||
let height = height.max(min_height);
|
||||
self.desired_rect.max.y = self.top_left().y + height;
|
||||
}
|
||||
|
||||
/// Size of content
|
||||
pub fn bounding_size(&self) -> Vec2 {
|
||||
self.child_bounds.size()
|
||||
}
|
||||
|
||||
/// Expand the bounding rect of this ui to include a child at the given rect.
|
||||
pub fn expand_to_include_child(&mut self, rect: Rect) {
|
||||
self.child_bounds.extend_with(rect.min);
|
||||
self.child_bounds.extend_with(rect.max);
|
||||
}
|
||||
|
||||
pub fn expand_to_size(&mut self, size: Vec2) {
|
||||
self.child_bounds.extend_with(self.top_left() + size);
|
||||
}
|
||||
|
||||
/// Bounding box of all contained children
|
||||
pub fn child_bounds(&self) -> Rect {
|
||||
self.child_bounds
|
||||
}
|
||||
|
||||
pub fn force_set_child_bounds(&mut self, child_bounds: Rect) {
|
||||
self.child_bounds = child_bounds;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Layout related measures:
|
||||
|
||||
/// The available space at the moment, given the current cursor.
|
||||
/// This how much more space we can take up without overflowing our parent.
|
||||
/// Shrinks as widgets allocate space and the cursor moves.
|
||||
/// A small rectangle should be intepreted as "as little as possible".
|
||||
/// An infinite rectangle should be interpred as "as much as you want".
|
||||
/// In most layouts the next widget will be put in the top left corner of this `Rect`.
|
||||
pub fn available(&self) -> Rect {
|
||||
self.layout.available(self.cursor, self.rect())
|
||||
}
|
||||
|
||||
/// This is like `available()`, but will never be infinite.
|
||||
/// Use this for components that want to grow without bounds (but shouldn't).
|
||||
/// In most layouts the next widget will be put in the top left corner of this `Rect`.
|
||||
pub fn available_finite(&self) -> Rect {
|
||||
self.layout.available(self.cursor, self.rect_finite())
|
||||
}
|
||||
|
||||
pub fn layout(&self) -> &Layout {
|
||||
&self.layout
|
||||
}
|
||||
|
||||
// TODO: remove
|
||||
pub fn set_layout(&mut self, layout: Layout) {
|
||||
self.layout = layout;
|
||||
|
||||
// TODO: remove this HACK:
|
||||
if layout.is_reversed() {
|
||||
self.cursor = self.rect_finite().max;
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
pub fn contains_mouse(&self, rect: Rect) -> bool {
|
||||
self.ctx.contains_mouse(self.layer, self.clip_rect, rect)
|
||||
}
|
||||
|
||||
pub fn has_kb_focus(&self, id: Id) -> bool {
|
||||
self.memory().kb_focus_id == Some(id)
|
||||
}
|
||||
|
||||
pub fn request_kb_focus(&self, id: Id) {
|
||||
self.memory().kb_focus_id = Some(id);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
/// Will warn if the returned id is not guaranteed unique.
|
||||
/// Use this to generate widget ids for widgets that have persistent state in Memory.
|
||||
/// If the `id_source` is not unique within this ui
|
||||
/// then an error will be printed at the current cursor position.
|
||||
pub fn make_unique_id<IdSource>(&self, id_source: IdSource) -> Id
|
||||
where
|
||||
IdSource: Hash + std::fmt::Debug,
|
||||
{
|
||||
let id = self.id.with(&id_source);
|
||||
// TODO: clip name clash error messages to clip rect
|
||||
self.ctx.register_unique_id(id, id_source, self.cursor)
|
||||
}
|
||||
|
||||
/// Make an Id that is unique to this positon.
|
||||
/// Can be used for widgets that do NOT persist state in Memory
|
||||
/// but you still need to interact with (e.g. buttons, sliders).
|
||||
pub fn make_position_id(&self) -> Id {
|
||||
self.id.with(&Id::from_pos(self.cursor))
|
||||
}
|
||||
|
||||
pub fn make_child_id(&self, id_seed: impl Hash) -> Id {
|
||||
self.id.with(id_seed)
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Interaction
|
||||
|
||||
pub fn interact(&self, rect: Rect, id: Id, sense: Sense) -> InteractInfo {
|
||||
self.ctx
|
||||
.interact(self.layer, self.clip_rect, rect, Some(id), sense)
|
||||
}
|
||||
|
||||
pub fn interact_hover(&self, rect: Rect) -> InteractInfo {
|
||||
self.ctx
|
||||
.interact(self.layer, self.clip_rect, rect, None, Sense::nothing())
|
||||
}
|
||||
|
||||
pub fn hovered(&self, rect: Rect) -> bool {
|
||||
self.interact_hover(rect).hovered
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn response(&mut self, interact: InteractInfo) -> GuiResponse {
|
||||
// TODO: unify GuiResponse and InteractInfo. They are the same thing!
|
||||
GuiResponse {
|
||||
hovered: interact.hovered,
|
||||
clicked: interact.clicked,
|
||||
double_clicked: interact.double_clicked,
|
||||
active: interact.active,
|
||||
rect: interact.rect,
|
||||
ctx: self.ctx.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Stuff that moves the cursor, i.e. allocates space in this ui!
|
||||
|
||||
/// Reserve this much space and move the cursor.
|
||||
/// Returns where to put the widget.
|
||||
///
|
||||
/// # How sizes are negotiated
|
||||
/// Each widget should have a *minimum desired size* and a *desired size*.
|
||||
/// When asking for space, ask AT LEAST for you minimum, and don't ask for more than you need.
|
||||
/// If you want to fill the space, ask about `available().size()` and use that.
|
||||
///
|
||||
/// You may get MORE space than you asked for, for instance
|
||||
/// for `Justified` aligned layouts, like in menus.
|
||||
///
|
||||
/// You may get LESS space than you asked for if the current layout won't fit what you asked for.
|
||||
pub fn allocate_space(&mut self, child_size: Vec2) -> Rect {
|
||||
let child_size = self.round_vec_to_pixels(child_size);
|
||||
self.cursor = self.round_pos_to_pixels(self.cursor);
|
||||
|
||||
// For debug rendering
|
||||
let too_wide = child_size.x > self.available().width();
|
||||
let too_high = child_size.x > self.available().height();
|
||||
|
||||
let rect = self.reserve_space_impl(child_size);
|
||||
|
||||
if self.style().debug_widget_rects {
|
||||
self.add_paint_cmd(PaintCmd::Rect {
|
||||
rect,
|
||||
corner_radius: 0.0,
|
||||
outline: Some(LineStyle::new(1.0, LIGHT_BLUE)),
|
||||
fill: None,
|
||||
});
|
||||
|
||||
let color = color::srgba(200, 0, 0, 255);
|
||||
let width = 2.5;
|
||||
|
||||
let mut paint_line_seg =
|
||||
|a, b| self.add_paint_cmd(PaintCmd::line_segment([a, b], color, width));
|
||||
|
||||
if too_wide {
|
||||
paint_line_seg(rect.left_top(), rect.left_bottom());
|
||||
paint_line_seg(rect.left_center(), rect.right_center());
|
||||
paint_line_seg(rect.right_top(), rect.right_bottom());
|
||||
}
|
||||
|
||||
if too_high {
|
||||
paint_line_seg(rect.left_top(), rect.right_top());
|
||||
paint_line_seg(rect.center_top(), rect.center_bottom());
|
||||
paint_line_seg(rect.left_bottom(), rect.right_bottom());
|
||||
}
|
||||
}
|
||||
|
||||
rect
|
||||
}
|
||||
|
||||
/// Reserve this much space and move the cursor.
|
||||
/// Returns where to put the widget.
|
||||
fn reserve_space_impl(&mut self, child_size: Vec2) -> Rect {
|
||||
let available_size = self.available_finite().size();
|
||||
let child_rect =
|
||||
self.layout
|
||||
.allocate_space(&mut self.cursor, &self.style, available_size, child_size);
|
||||
self.child_bounds = self.child_bounds.union(child_rect);
|
||||
child_rect
|
||||
}
|
||||
|
||||
// ------------------------------------------------
|
||||
// Painting related stuff
|
||||
|
||||
/// It is up to the caller to make sure there is room for this.
|
||||
/// Can be used for free painting.
|
||||
/// NOTE: all coordinates are screen coordinates!
|
||||
pub fn add_paint_cmd(&mut self, paint_cmd: PaintCmd) {
|
||||
self.ctx
|
||||
.graphics()
|
||||
.layer(self.layer)
|
||||
.push((self.clip_rect(), paint_cmd))
|
||||
}
|
||||
|
||||
pub fn add_paint_cmds(&mut self, mut cmds: Vec<PaintCmd>) {
|
||||
let clip_rect = self.clip_rect();
|
||||
self.ctx
|
||||
.graphics()
|
||||
.layer(self.layer)
|
||||
.extend(cmds.drain(..).map(|cmd| (clip_rect, cmd)));
|
||||
}
|
||||
|
||||
/// Insert a paint cmd before existing ones
|
||||
pub fn insert_paint_cmd(&mut self, pos: usize, paint_cmd: PaintCmd) {
|
||||
self.ctx
|
||||
.graphics()
|
||||
.layer(self.layer)
|
||||
.insert(pos, (self.clip_rect(), paint_cmd));
|
||||
}
|
||||
|
||||
pub fn paint_list_len(&self) -> usize {
|
||||
self.ctx.graphics().layer(self.layer).len()
|
||||
}
|
||||
|
||||
/// Paint some debug text at current cursor
|
||||
pub fn debug_text(&self, text: impl Into<String>) {
|
||||
self.debug_text_at(self.cursor, text);
|
||||
}
|
||||
|
||||
pub fn debug_text_at(&self, pos: Pos2, text: impl Into<String>) {
|
||||
self.ctx.debug_text(pos, text);
|
||||
}
|
||||
|
||||
pub fn debug_rect(&mut self, rect: Rect, text: impl Into<String>) {
|
||||
self.add_paint_cmd(PaintCmd::Rect {
|
||||
corner_radius: 0.0,
|
||||
fill: None,
|
||||
outline: Some(LineStyle::new(1.0, color::RED)),
|
||||
rect,
|
||||
});
|
||||
let align = (Align::Min, Align::Min);
|
||||
let text_style = TextStyle::Monospace;
|
||||
self.floating_text(rect.min, text.into(), text_style, align, Some(color::RED));
|
||||
}
|
||||
|
||||
/// Show some text anywhere in the ui.
|
||||
/// To center the text at the given position, use `align: (Center, Center)`.
|
||||
/// If you want to draw text floating on top of everything,
|
||||
/// consider using `Context.floating_text` instead.
|
||||
pub fn floating_text(
|
||||
&mut self,
|
||||
pos: Pos2,
|
||||
text: impl Into<String>,
|
||||
text_style: TextStyle,
|
||||
align: (Align, Align),
|
||||
text_color: Option<Color>,
|
||||
) -> Rect {
|
||||
let font = &self.fonts()[text_style];
|
||||
let galley = font.layout_multiline(text.into(), f32::INFINITY);
|
||||
let rect = align_rect(Rect::from_min_size(pos, galley.size), align);
|
||||
self.add_galley(rect.min, galley, text_style, text_color);
|
||||
rect
|
||||
}
|
||||
|
||||
/// Already layed out text.
|
||||
pub fn add_galley(
|
||||
&mut self,
|
||||
pos: Pos2,
|
||||
galley: font::Galley,
|
||||
text_style: TextStyle,
|
||||
color: Option<Color>,
|
||||
) {
|
||||
let color = color.unwrap_or_else(|| self.style().text_color);
|
||||
self.add_paint_cmd(PaintCmd::Text {
|
||||
pos,
|
||||
galley,
|
||||
text_style,
|
||||
color,
|
||||
});
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Addding Widgets
|
||||
|
||||
pub fn add(&mut self, widget: impl Widget) -> GuiResponse {
|
||||
let interact = widget.ui(self);
|
||||
self.response(interact)
|
||||
}
|
||||
|
||||
// Convenience functions:
|
||||
|
||||
pub fn label(&mut self, label: impl Into<Label>) -> GuiResponse {
|
||||
self.add(label.into())
|
||||
}
|
||||
|
||||
pub fn hyperlink(&mut self, url: impl Into<String>) -> GuiResponse {
|
||||
self.add(Hyperlink::new(url))
|
||||
}
|
||||
|
||||
pub fn button(&mut self, text: impl Into<String>) -> GuiResponse {
|
||||
self.add(Button::new(text))
|
||||
}
|
||||
|
||||
// TODO: argument order?
|
||||
pub fn checkbox(&mut self, text: impl Into<String>, checked: &mut bool) -> GuiResponse {
|
||||
self.add(Checkbox::new(checked, text))
|
||||
}
|
||||
|
||||
// TODO: argument order?
|
||||
pub fn radio(&mut self, text: impl Into<String>, checked: bool) -> GuiResponse {
|
||||
self.add(RadioButton::new(checked, text))
|
||||
}
|
||||
|
||||
pub fn separator(&mut self) -> GuiResponse {
|
||||
self.add(Separator::new())
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Addding Containers / Sub-uis:
|
||||
|
||||
pub fn collapsing<R>(
|
||||
&mut self,
|
||||
text: impl Into<String>,
|
||||
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||
) -> Option<R> {
|
||||
CollapsingHeader::new(text).show(self, add_contents)
|
||||
}
|
||||
|
||||
/// Create a child ui at the current cursor.
|
||||
/// `size` is the desired size.
|
||||
/// Actual size may be much smaller if `avilable_size()` is not enough.
|
||||
/// Set `size` to `Vec::infinity()` to get as much space as possible.
|
||||
/// Just because you ask for a lot of space does not mean you have to use it!
|
||||
/// After `add_contents` is called the contents of `bounding_size`
|
||||
/// will decide how much space will be used in the parent ui.
|
||||
pub fn add_custom_contents(&mut self, size: Vec2, add_contents: impl FnOnce(&mut Ui)) -> Rect {
|
||||
let size = size.min(self.available().size());
|
||||
let child_rect = Rect::from_min_size(self.cursor, size);
|
||||
let mut child_ui = self.child_ui(child_rect);
|
||||
add_contents(&mut child_ui);
|
||||
self.allocate_space(child_ui.bounding_size())
|
||||
}
|
||||
|
||||
/// Create a child ui
|
||||
pub fn add_custom<R>(&mut self, add_contents: impl FnOnce(&mut Ui) -> R) -> (R, Rect) {
|
||||
let child_rect = self.available();
|
||||
let mut child_ui = self.child_ui(child_rect);
|
||||
let r = add_contents(&mut child_ui);
|
||||
let size = child_ui.bounding_size();
|
||||
(r, self.allocate_space(size))
|
||||
}
|
||||
|
||||
/// Create a child ui which is indented to the right
|
||||
pub fn indent<R>(
|
||||
&mut self,
|
||||
id_source: impl Hash,
|
||||
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||
) -> (R, Rect) {
|
||||
assert!(
|
||||
self.layout().dir() == Direction::Vertical,
|
||||
"You can only indent vertical layouts"
|
||||
);
|
||||
let indent = vec2(self.style.indent, 0.0);
|
||||
let child_rect = Rect::from_min_max(self.cursor + indent, self.bottom_right());
|
||||
let mut child_ui = Ui {
|
||||
id: self.id.with(id_source),
|
||||
..self.child_ui(child_rect)
|
||||
};
|
||||
let ret = add_contents(&mut child_ui);
|
||||
let size = child_ui.bounding_size();
|
||||
|
||||
// draw a grey line on the left to mark the indented section
|
||||
let line_start = child_rect.min - indent * 0.5;
|
||||
let line_start = self.round_pos_to_pixels(line_start);
|
||||
let line_end = pos2(line_start.x, line_start.y + size.y - 2.0);
|
||||
self.add_paint_cmd(PaintCmd::line_segment(
|
||||
[line_start, line_end],
|
||||
gray(150, 255),
|
||||
self.style.line_width,
|
||||
));
|
||||
|
||||
(ret, self.allocate_space(indent + size))
|
||||
}
|
||||
|
||||
pub fn left_column(&mut self, width: f32) -> Ui {
|
||||
self.column(Align::Min, width)
|
||||
}
|
||||
|
||||
pub fn centered_column(&mut self, width: f32) -> Ui {
|
||||
self.column(Align::Center, width)
|
||||
}
|
||||
|
||||
pub fn right_column(&mut self, width: f32) -> Ui {
|
||||
self.column(Align::Max, width)
|
||||
}
|
||||
|
||||
/// A column ui with a given width.
|
||||
pub fn column(&mut self, column_position: Align, width: f32) -> Ui {
|
||||
let x = match column_position {
|
||||
Align::Min => 0.0,
|
||||
Align::Center => self.available().width() / 2.0 - width / 2.0,
|
||||
Align::Max => self.available().width() - width,
|
||||
};
|
||||
self.child_ui(Rect::from_min_size(
|
||||
self.cursor + vec2(x, 0.0),
|
||||
vec2(width, self.available().height()),
|
||||
))
|
||||
}
|
||||
|
||||
/// Start a ui with horizontal layout
|
||||
pub fn horizontal<R>(&mut self, add_contents: impl FnOnce(&mut Ui) -> R) -> (R, Rect) {
|
||||
self.inner_layout(Layout::horizontal(Align::Min), add_contents)
|
||||
}
|
||||
|
||||
/// Start a ui with vertical layout
|
||||
pub fn vertical<R>(&mut self, add_contents: impl FnOnce(&mut Ui) -> R) -> (R, Rect) {
|
||||
self.inner_layout(Layout::vertical(Align::Min), add_contents)
|
||||
}
|
||||
|
||||
pub fn inner_layout<R>(
|
||||
&mut self,
|
||||
layout: Layout,
|
||||
add_contents: impl FnOnce(&mut Self) -> R,
|
||||
) -> (R, Rect) {
|
||||
let child_rect = Rect::from_min_max(self.cursor, self.bottom_right());
|
||||
let mut child_ui = Self {
|
||||
..self.child_ui(child_rect)
|
||||
};
|
||||
child_ui.set_layout(layout); // HACK: need a separate call right now
|
||||
let ret = add_contents(&mut child_ui);
|
||||
let size = child_ui.bounding_size();
|
||||
let rect = self.allocate_space(size);
|
||||
(ret, rect)
|
||||
}
|
||||
|
||||
/// Temporarily split split an Ui into several columns.
|
||||
///
|
||||
/// ``` ignore
|
||||
/// ui.columns(2, |columns| {
|
||||
/// columns[0].add(egui::widgets::label!("First column"));
|
||||
/// columns[1].add(egui::widgets::label!("Second column"));
|
||||
/// });
|
||||
/// ```
|
||||
pub fn columns<F, R>(&mut self, num_columns: usize, add_contents: F) -> R
|
||||
where
|
||||
F: FnOnce(&mut [Self]) -> R,
|
||||
{
|
||||
// TODO: ensure there is space
|
||||
let spacing = self.style.item_spacing.x;
|
||||
let total_spacing = spacing * (num_columns as f32 - 1.0);
|
||||
let column_width = (self.available().width() - total_spacing) / (num_columns as f32);
|
||||
|
||||
let mut columns: Vec<Self> = (0..num_columns)
|
||||
.map(|col_idx| {
|
||||
let pos = self.cursor + vec2((col_idx as f32) * (column_width + spacing), 0.0);
|
||||
let child_rect =
|
||||
Rect::from_min_max(pos, pos2(pos.x + column_width, self.bottom_right().y));
|
||||
|
||||
Self {
|
||||
id: self.make_child_id(&("column", col_idx)),
|
||||
..self.child_ui(child_rect)
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let result = add_contents(&mut columns[..]);
|
||||
|
||||
let mut sum_width = total_spacing;
|
||||
for column in &columns {
|
||||
sum_width += column.child_bounds.width();
|
||||
}
|
||||
|
||||
let mut max_height = 0.0;
|
||||
for ui in columns {
|
||||
let size = ui.bounding_size();
|
||||
max_height = size.y.max(max_height);
|
||||
}
|
||||
|
||||
let size = vec2(self.available().width().max(sum_width), max_height);
|
||||
self.allocate_space(size);
|
||||
result
|
||||
}
|
||||
|
||||
// ------------------------------------------------
|
||||
}
|
||||
493
egui/src/widgets.rs
Normal file
493
egui/src/widgets.rs
Normal file
@@ -0,0 +1,493 @@
|
||||
#![allow(clippy::new_without_default)]
|
||||
|
||||
use crate::{layout::Direction, *};
|
||||
|
||||
mod slider;
|
||||
pub mod text_edit;
|
||||
|
||||
pub use {paint::*, slider::*, text_edit::*};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// Anything implementing Widget can be added to a Ui with `Ui::add`
|
||||
pub trait Widget {
|
||||
fn ui(self, ui: &mut Ui) -> InteractInfo;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
pub struct Label {
|
||||
// TODO: not pub
|
||||
pub(crate) text: String,
|
||||
pub(crate) multiline: bool,
|
||||
auto_shrink: bool,
|
||||
pub(crate) text_style: TextStyle, // TODO: Option<TextStyle>, where None means "use the default for the ui"
|
||||
pub(crate) text_color: Option<Color>,
|
||||
}
|
||||
|
||||
impl Label {
|
||||
pub fn new(text: impl Into<String>) -> Self {
|
||||
Self {
|
||||
text: text.into(),
|
||||
multiline: true,
|
||||
auto_shrink: false,
|
||||
text_style: TextStyle::Body,
|
||||
text_color: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn text(&self) -> &str {
|
||||
&self.text
|
||||
}
|
||||
|
||||
pub fn multiline(mut self, multiline: bool) -> Self {
|
||||
self.multiline = multiline;
|
||||
self
|
||||
}
|
||||
|
||||
/// If true, will word wrap to `ui.available_finite().width()`.
|
||||
/// If false (default), will word wrap to `ui.available().width()`.
|
||||
/// This only makes a difference for auto-sized parents.
|
||||
pub fn auto_shrink(mut self) -> Self {
|
||||
self.auto_shrink = true;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn text_style(mut self, text_style: TextStyle) -> Self {
|
||||
self.text_style = text_style;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn text_color(mut self, text_color: Color) -> Self {
|
||||
self.text_color = Some(text_color);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn layout(&self, ui: &Ui) -> font::Galley {
|
||||
let max_width = if self.auto_shrink {
|
||||
ui.available_finite().width()
|
||||
} else {
|
||||
ui.available().width()
|
||||
};
|
||||
self.layout_width(ui, max_width)
|
||||
}
|
||||
|
||||
pub fn layout_width(&self, ui: &Ui, max_width: f32) -> font::Galley {
|
||||
let font = &ui.fonts()[self.text_style];
|
||||
if self.multiline {
|
||||
font.layout_multiline(self.text.clone(), max_width) // TODO: avoid clone
|
||||
} else {
|
||||
font.layout_single_line(self.text.clone()) // TODO: avoid clone
|
||||
}
|
||||
}
|
||||
|
||||
pub fn font_height(&self, ui: &Ui) -> f32 {
|
||||
ui.fonts()[self.text_style].height()
|
||||
}
|
||||
|
||||
// TODO: this should return a LabelLayout which has a paint method.
|
||||
// We can then split Widget::Ui in two: layout + allocating space, and painting.
|
||||
// this allows us to assemble lables, THEN detect interaction, THEN chose color style based on that.
|
||||
// pub fn layout(self, ui: &mut ui) -> LabelLayout { }
|
||||
|
||||
// TODO: a paint method for painting anywhere in a ui.
|
||||
// This should be the easiest method of putting text anywhere.
|
||||
|
||||
pub fn paint_galley(&self, ui: &mut Ui, pos: Pos2, galley: font::Galley) {
|
||||
ui.add_galley(pos, galley, self.text_style, self.text_color);
|
||||
}
|
||||
}
|
||||
|
||||
/// Usage: label!("Foo: {}", bar)
|
||||
#[macro_export]
|
||||
macro_rules! label {
|
||||
($fmt:expr) => ($crate::widgets::Label::new($fmt));
|
||||
($fmt:expr, $($arg:tt)*) => ($crate::widgets::Label::new(format!($fmt, $($arg)*)));
|
||||
}
|
||||
|
||||
impl Widget for Label {
|
||||
fn ui(self, ui: &mut Ui) -> InteractInfo {
|
||||
let galley = self.layout(ui);
|
||||
let rect = ui.allocate_space(galley.size);
|
||||
self.paint_galley(ui, rect.min, galley);
|
||||
ui.interact_hover(rect)
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<Label> for &str {
|
||||
fn into(self) -> Label {
|
||||
Label::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<Label> for String {
|
||||
fn into(self) -> Label {
|
||||
Label::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
pub struct Hyperlink {
|
||||
url: String,
|
||||
text: String,
|
||||
}
|
||||
|
||||
impl Hyperlink {
|
||||
pub fn new(url: impl Into<String>) -> Self {
|
||||
let url = url.into();
|
||||
Self {
|
||||
text: url.clone(),
|
||||
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 {
|
||||
fn ui(self, ui: &mut Ui) -> InteractInfo {
|
||||
let Hyperlink { url, text } = self;
|
||||
|
||||
let color = color::LIGHT_BLUE;
|
||||
let text_style = TextStyle::Body;
|
||||
let id = ui.make_child_id(&url);
|
||||
let font = &ui.fonts()[text_style];
|
||||
let galley = font.layout_multiline(text, ui.available().width());
|
||||
let rect = ui.allocate_space(galley.size);
|
||||
let interact = ui.interact(rect, id, Sense::click());
|
||||
if interact.hovered {
|
||||
ui.ctx().output().cursor_icon = CursorIcon::PointingHand;
|
||||
}
|
||||
if interact.clicked {
|
||||
ui.ctx().output().open_url = Some(url);
|
||||
}
|
||||
|
||||
if interact.hovered {
|
||||
// Underline:
|
||||
for line in &galley.lines {
|
||||
let pos = interact.rect.min;
|
||||
let y = pos.y + line.y_max;
|
||||
let y = ui.round_to_pixel(y);
|
||||
let min_x = pos.x + line.min_x();
|
||||
let max_x = pos.x + line.max_x();
|
||||
ui.add_paint_cmd(PaintCmd::line_segment(
|
||||
[pos2(min_x, y), pos2(max_x, y)],
|
||||
color,
|
||||
ui.style().line_width,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
ui.add_galley(interact.rect.min, galley, text_style, Some(color));
|
||||
|
||||
interact
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
pub struct Button {
|
||||
text: String,
|
||||
text_color: Option<Color>,
|
||||
text_style: TextStyle,
|
||||
/// None means default for interact
|
||||
fill: Option<Color>,
|
||||
}
|
||||
|
||||
impl Button {
|
||||
pub fn new(text: impl Into<String>) -> Self {
|
||||
Self {
|
||||
text: text.into(),
|
||||
text_color: None,
|
||||
text_style: TextStyle::Button,
|
||||
fill: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn text_color(mut self, text_color: Color) -> Self {
|
||||
self.text_color = Some(text_color);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn text_style(mut self, text_style: TextStyle) -> Self {
|
||||
self.text_style = text_style;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn fill(mut self, fill: Option<Color>) -> Self {
|
||||
self.fill = fill;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for Button {
|
||||
fn ui(self, ui: &mut Ui) -> InteractInfo {
|
||||
let Button {
|
||||
text,
|
||||
text_color,
|
||||
text_style,
|
||||
fill,
|
||||
} = self;
|
||||
|
||||
let id = ui.make_position_id();
|
||||
let font = &ui.fonts()[text_style];
|
||||
let galley = font.layout_multiline(text, ui.available().width());
|
||||
let padding = ui.style().button_padding;
|
||||
let mut size = galley.size + 2.0 * padding;
|
||||
size.y = size.y.max(ui.style().clickable_diameter);
|
||||
let rect = ui.allocate_space(size);
|
||||
let interact = ui.interact(rect, id, Sense::click());
|
||||
let text_cursor = interact.rect.left_center() + vec2(padding.x, -0.5 * galley.size.y);
|
||||
let bg_fill = fill.or(ui.style().interact(&interact).bg_fill);
|
||||
ui.add_paint_cmd(PaintCmd::Rect {
|
||||
corner_radius: ui.style().interact(&interact).corner_radius,
|
||||
fill: bg_fill,
|
||||
outline: ui.style().interact(&interact).rect_outline,
|
||||
rect: interact.rect,
|
||||
});
|
||||
let stroke_color = ui.style().interact(&interact).stroke_color;
|
||||
let text_color = text_color.unwrap_or(stroke_color);
|
||||
ui.add_galley(text_cursor, galley, text_style, Some(text_color));
|
||||
interact
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Checkbox<'a> {
|
||||
checked: &'a mut bool,
|
||||
text: String,
|
||||
text_color: Option<Color>,
|
||||
}
|
||||
|
||||
impl<'a> Checkbox<'a> {
|
||||
pub fn new(checked: &'a mut bool, text: impl Into<String>) -> Self {
|
||||
Checkbox {
|
||||
checked,
|
||||
text: text.into(),
|
||||
text_color: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn text_color(mut self, text_color: Color) -> Self {
|
||||
self.text_color = Some(text_color);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Widget for Checkbox<'a> {
|
||||
fn ui(self, ui: &mut Ui) -> InteractInfo {
|
||||
let Checkbox {
|
||||
checked,
|
||||
text,
|
||||
text_color,
|
||||
} = self;
|
||||
|
||||
let id = ui.make_position_id();
|
||||
let text_style = TextStyle::Button;
|
||||
let font = &ui.fonts()[text_style];
|
||||
let galley = font.layout_single_line(text);
|
||||
let size = ui.style().button_padding
|
||||
+ vec2(ui.style().start_icon_width, 0.0)
|
||||
+ galley.size
|
||||
+ ui.style().button_padding;
|
||||
let rect = ui.allocate_space(size);
|
||||
let interact = ui.interact(rect, id, Sense::click());
|
||||
let text_cursor =
|
||||
interact.rect.min + ui.style().button_padding + vec2(ui.style().start_icon_width, 0.0);
|
||||
if interact.clicked {
|
||||
*checked = !*checked;
|
||||
}
|
||||
let (small_icon_rect, big_icon_rect) = ui.style().icon_rectangles(interact.rect);
|
||||
ui.add_paint_cmd(PaintCmd::Rect {
|
||||
corner_radius: ui.style().interact(&interact).corner_radius,
|
||||
fill: ui.style().interact(&interact).bg_fill,
|
||||
outline: ui.style().interact(&interact).rect_outline,
|
||||
rect: big_icon_rect,
|
||||
});
|
||||
|
||||
let stroke_color = ui.style().interact(&interact).stroke_color;
|
||||
|
||||
if *checked {
|
||||
ui.add_paint_cmd(PaintCmd::Path {
|
||||
path: Path::from_open_points(&[
|
||||
pos2(small_icon_rect.left(), small_icon_rect.center().y),
|
||||
pos2(small_icon_rect.center().x, small_icon_rect.bottom()),
|
||||
pos2(small_icon_rect.right(), small_icon_rect.top()),
|
||||
]),
|
||||
closed: false,
|
||||
outline: Some(LineStyle::new(ui.style().line_width, stroke_color)),
|
||||
fill: None,
|
||||
});
|
||||
}
|
||||
|
||||
let text_color = text_color.unwrap_or(stroke_color);
|
||||
ui.add_galley(text_cursor, galley, text_style, Some(text_color));
|
||||
interact
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct RadioButton {
|
||||
checked: bool,
|
||||
text: String,
|
||||
text_color: Option<Color>,
|
||||
}
|
||||
|
||||
impl RadioButton {
|
||||
pub fn new(checked: bool, text: impl Into<String>) -> Self {
|
||||
Self {
|
||||
checked,
|
||||
text: text.into(),
|
||||
text_color: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn text_color(mut self, text_color: Color) -> Self {
|
||||
self.text_color = Some(text_color);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub fn radio(checked: bool, text: impl Into<String>) -> RadioButton {
|
||||
RadioButton::new(checked, text)
|
||||
}
|
||||
|
||||
impl Widget for RadioButton {
|
||||
fn ui(self, ui: &mut Ui) -> InteractInfo {
|
||||
let RadioButton {
|
||||
checked,
|
||||
text,
|
||||
text_color,
|
||||
} = self;
|
||||
let id = ui.make_position_id();
|
||||
let text_style = TextStyle::Button;
|
||||
let font = &ui.fonts()[text_style];
|
||||
let galley = font.layout_multiline(text, ui.available().width());
|
||||
let size = ui.style().button_padding
|
||||
+ vec2(ui.style().start_icon_width, 0.0)
|
||||
+ galley.size
|
||||
+ ui.style().button_padding;
|
||||
let rect = ui.allocate_space(size);
|
||||
let interact = ui.interact(rect, id, Sense::click());
|
||||
let text_cursor =
|
||||
interact.rect.min + ui.style().button_padding + vec2(ui.style().start_icon_width, 0.0);
|
||||
|
||||
let bg_fill = ui.style().interact(&interact).bg_fill;
|
||||
let stroke_color = ui.style().interact(&interact).stroke_color;
|
||||
|
||||
let (small_icon_rect, big_icon_rect) = ui.style().icon_rectangles(interact.rect);
|
||||
|
||||
ui.add_paint_cmd(PaintCmd::Circle {
|
||||
center: big_icon_rect.center(),
|
||||
fill: bg_fill,
|
||||
outline: ui.style().interact(&interact).rect_outline, // TODO
|
||||
radius: big_icon_rect.width() / 2.0,
|
||||
});
|
||||
|
||||
if checked {
|
||||
ui.add_paint_cmd(PaintCmd::Circle {
|
||||
center: small_icon_rect.center(),
|
||||
fill: Some(stroke_color),
|
||||
outline: None,
|
||||
radius: small_icon_rect.width() / 3.0,
|
||||
});
|
||||
}
|
||||
|
||||
let text_color = text_color.unwrap_or(stroke_color);
|
||||
ui.add_galley(text_cursor, galley, text_style, Some(text_color));
|
||||
interact
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
pub struct Separator {
|
||||
line_width: Option<f32>,
|
||||
min_spacing: f32,
|
||||
extra: f32,
|
||||
color: Color,
|
||||
}
|
||||
|
||||
impl Separator {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
line_width: None,
|
||||
min_spacing: 6.0,
|
||||
extra: 0.0,
|
||||
color: color::WHITE,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn line_width(mut self, line_width: f32) -> Self {
|
||||
self.line_width = Some(line_width);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn min_spacing(mut self, min_spacing: f32) -> Self {
|
||||
self.min_spacing = min_spacing;
|
||||
self
|
||||
}
|
||||
|
||||
/// Draw this much longer on each side
|
||||
pub fn extra(mut self, extra: f32) -> Self {
|
||||
self.extra = extra;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn color(mut self, color: Color) -> Self {
|
||||
self.color = color;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for Separator {
|
||||
fn ui(self, ui: &mut Ui) -> InteractInfo {
|
||||
let Separator {
|
||||
line_width,
|
||||
min_spacing,
|
||||
extra,
|
||||
color,
|
||||
} = self;
|
||||
|
||||
let line_width = line_width.unwrap_or_else(|| ui.style().line_width);
|
||||
|
||||
let available_space = ui.available_finite().size();
|
||||
|
||||
let (points, rect) = match ui.layout().dir() {
|
||||
Direction::Horizontal => {
|
||||
let rect = ui.allocate_space(vec2(min_spacing, available_space.y));
|
||||
(
|
||||
[
|
||||
pos2(rect.center().x, rect.top() - extra),
|
||||
pos2(rect.center().x, rect.bottom() + extra),
|
||||
],
|
||||
rect,
|
||||
)
|
||||
}
|
||||
Direction::Vertical => {
|
||||
let rect = ui.allocate_space(vec2(available_space.x, min_spacing));
|
||||
(
|
||||
[
|
||||
pos2(rect.left() - extra, rect.center().y),
|
||||
pos2(rect.right() + extra, rect.center().y),
|
||||
],
|
||||
rect,
|
||||
)
|
||||
}
|
||||
};
|
||||
ui.add_paint_cmd(PaintCmd::LineSegment {
|
||||
points,
|
||||
style: LineStyle::new(line_width, color),
|
||||
});
|
||||
ui.interact_hover(rect)
|
||||
}
|
||||
}
|
||||
195
egui/src/widgets/slider.rs
Normal file
195
egui/src/widgets/slider.rs
Normal file
@@ -0,0 +1,195 @@
|
||||
use std::ops::RangeInclusive;
|
||||
|
||||
use crate::{paint::*, widgets::Label, *};
|
||||
|
||||
/// Combined into one function (rather than two) to make it easier
|
||||
/// for the borrow checker.
|
||||
type SliderGetSet<'a> = Box<dyn 'a + FnMut(Option<f32>) -> f32>;
|
||||
|
||||
pub struct Slider<'a> {
|
||||
get_set_value: SliderGetSet<'a>,
|
||||
range: RangeInclusive<f32>,
|
||||
// TODO: label: Option<Label>
|
||||
text: Option<String>,
|
||||
precision: usize,
|
||||
text_color: Option<Color>,
|
||||
text_on_top: Option<bool>,
|
||||
id: Option<Id>,
|
||||
}
|
||||
|
||||
impl<'a> Slider<'a> {
|
||||
fn from_get_set(
|
||||
range: RangeInclusive<f32>,
|
||||
get_set_value: impl 'a + FnMut(Option<f32>) -> f32,
|
||||
) -> Self {
|
||||
Slider {
|
||||
get_set_value: Box::new(get_set_value),
|
||||
range,
|
||||
text: None,
|
||||
precision: 3,
|
||||
text_on_top: None,
|
||||
text_color: None,
|
||||
id: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn f32(value: &'a mut f32, range: RangeInclusive<f32>) -> Self {
|
||||
Slider {
|
||||
precision: 3,
|
||||
..Self::from_get_set(range, move |v: Option<f32>| {
|
||||
if let Some(v) = v {
|
||||
*value = v
|
||||
}
|
||||
*value
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn i32(value: &'a mut i32, range: RangeInclusive<i32>) -> Self {
|
||||
let range = (*range.start() as f32)..=(*range.end() as f32);
|
||||
Slider {
|
||||
precision: 0,
|
||||
..Self::from_get_set(range, move |v: Option<f32>| {
|
||||
if let Some(v) = v {
|
||||
*value = v.round() as i32
|
||||
}
|
||||
*value as f32
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn usize(value: &'a mut usize, range: RangeInclusive<usize>) -> Self {
|
||||
let range = (*range.start() as f32)..=(*range.end() as f32);
|
||||
Slider {
|
||||
precision: 0,
|
||||
..Self::from_get_set(range, move |v: Option<f32>| {
|
||||
if let Some(v) = v {
|
||||
*value = v.round() as usize
|
||||
}
|
||||
*value as f32
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn text(mut self, text: impl Into<String>) -> Self {
|
||||
self.text = Some(text.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn text_color(mut self, text_color: Color) -> Self {
|
||||
self.text_color = Some(text_color);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn precision(mut self, precision: usize) -> Self {
|
||||
self.precision = precision;
|
||||
self
|
||||
}
|
||||
|
||||
fn get_value_f32(&mut self) -> f32 {
|
||||
(self.get_set_value)(None)
|
||||
}
|
||||
|
||||
fn set_value_f32(&mut self, mut value: f32) {
|
||||
if self.precision == 0 {
|
||||
value = value.round();
|
||||
}
|
||||
(self.get_set_value)(Some(value));
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Widget for Slider<'a> {
|
||||
fn ui(mut self, ui: &mut Ui) -> InteractInfo {
|
||||
let text_style = TextStyle::Button;
|
||||
let font = &ui.fonts()[text_style];
|
||||
|
||||
if let Some(text) = &self.text {
|
||||
if self.id.is_none() {
|
||||
self.id = Some(Id::new(text));
|
||||
}
|
||||
|
||||
let text_on_top = self.text_on_top.unwrap_or_default();
|
||||
let text_color = self.text_color;
|
||||
let value = (self.get_set_value)(None);
|
||||
let full_text = format!("{}: {:.*}", text, self.precision, value);
|
||||
|
||||
let slider_sans_text = Slider { text: None, ..self };
|
||||
|
||||
if text_on_top {
|
||||
let galley = font.layout_single_line(full_text);
|
||||
let pos = ui.allocate_space(galley.size).min;
|
||||
ui.add_galley(pos, galley, text_style, text_color);
|
||||
slider_sans_text.ui(ui)
|
||||
} else {
|
||||
ui.columns(2, |columns| {
|
||||
// Slider on the left:
|
||||
let slider_response = columns[0].add(slider_sans_text);
|
||||
|
||||
// Place the text in line with the slider on the left:
|
||||
columns[1].set_desired_height(slider_response.rect.height());
|
||||
columns[1].inner_layout(Layout::horizontal(Align::Center), |ui| {
|
||||
ui.add(Label::new(full_text).multiline(false));
|
||||
});
|
||||
|
||||
slider_response.into()
|
||||
})
|
||||
}
|
||||
} else {
|
||||
let height = font.line_spacing().max(ui.style().clickable_diameter);
|
||||
let handle_radius = height / 2.5;
|
||||
|
||||
let id = self.id.unwrap_or_else(|| ui.make_position_id());
|
||||
|
||||
let size = Vec2 {
|
||||
x: ui.available().width(),
|
||||
y: height,
|
||||
};
|
||||
let rect = ui.allocate_space(size);
|
||||
let interact = ui.interact(rect, id, Sense::click_and_drag());
|
||||
|
||||
let left = interact.rect.left() + handle_radius;
|
||||
let right = interact.rect.right() - handle_radius;
|
||||
|
||||
let range = self.range.clone();
|
||||
debug_assert!(range.start() <= range.end());
|
||||
|
||||
if let Some(mouse_pos) = ui.input().mouse.pos {
|
||||
if interact.active {
|
||||
self.set_value_f32(remap_clamp(mouse_pos.x, left..=right, range.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
// Paint it:
|
||||
{
|
||||
let value = self.get_value_f32();
|
||||
|
||||
let rect = interact.rect;
|
||||
let rail_radius = ui.round_to_pixel((height / 8.0).max(2.0));
|
||||
let rail_rect = Rect::from_min_max(
|
||||
pos2(interact.rect.left(), rect.center().y - rail_radius),
|
||||
pos2(interact.rect.right(), rect.center().y + rail_radius),
|
||||
);
|
||||
let marker_center_x = remap_clamp(value, range, left..=right);
|
||||
|
||||
ui.add_paint_cmd(PaintCmd::Rect {
|
||||
rect: rail_rect,
|
||||
corner_radius: rail_radius,
|
||||
fill: Some(ui.style().background_fill),
|
||||
outline: Some(LineStyle::new(1.0, color::gray(200, 255))), // TODO
|
||||
});
|
||||
|
||||
ui.add_paint_cmd(PaintCmd::Circle {
|
||||
center: pos2(marker_center_x, rail_rect.center().y),
|
||||
radius: handle_radius,
|
||||
fill: Some(ui.style().interact(&interact).fill),
|
||||
outline: Some(LineStyle::new(
|
||||
ui.style().interact(&interact).stroke_width,
|
||||
ui.style().interact(&interact).stroke_color,
|
||||
)),
|
||||
});
|
||||
}
|
||||
|
||||
interact
|
||||
}
|
||||
}
|
||||
}
|
||||
261
egui/src/widgets/text_edit.rs
Normal file
261
egui/src/widgets/text_edit.rs
Normal file
@@ -0,0 +1,261 @@
|
||||
use crate::{paint::*, *};
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, serde_derive::Deserialize, serde_derive::Serialize)]
|
||||
pub(crate) struct State {
|
||||
/// Charctaer based, NOT bytes.
|
||||
/// TODO: store as line + row
|
||||
pub cursor: Option<usize>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TextEdit<'t> {
|
||||
text: &'t mut String,
|
||||
id: Option<Id>,
|
||||
text_style: TextStyle, // TODO: Option<TextStyle>, where None means "use the default for the current Ui"
|
||||
text_color: Option<Color>,
|
||||
multiline: bool,
|
||||
}
|
||||
|
||||
impl<'t> TextEdit<'t> {
|
||||
pub fn new(text: &'t mut String) -> Self {
|
||||
TextEdit {
|
||||
text,
|
||||
id: None,
|
||||
text_style: TextStyle::Body,
|
||||
text_color: Default::default(),
|
||||
multiline: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn id(mut self, id_source: impl std::hash::Hash) -> Self {
|
||||
self.id = Some(Id::new(id_source));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn text_style(mut self, text_style: TextStyle) -> Self {
|
||||
self.text_style = text_style;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn text_color(mut self, text_color: Color) -> Self {
|
||||
self.text_color = Some(text_color);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn multiline(mut self, multiline: bool) -> Self {
|
||||
self.multiline = multiline;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'t> Widget for TextEdit<'t> {
|
||||
fn ui(self, ui: &mut Ui) -> InteractInfo {
|
||||
let TextEdit {
|
||||
text,
|
||||
id,
|
||||
text_style,
|
||||
text_color,
|
||||
multiline,
|
||||
} = self;
|
||||
|
||||
let id = ui.make_child_id(id);
|
||||
|
||||
let mut state = ui.memory().text_edit.get(&id).cloned().unwrap_or_default();
|
||||
|
||||
let font = &ui.fonts()[text_style];
|
||||
let line_spacing = font.line_spacing();
|
||||
let available_width = ui.available().width();
|
||||
let mut galley = if multiline {
|
||||
font.layout_multiline(text.clone(), available_width)
|
||||
} else {
|
||||
font.layout_single_line(text.clone())
|
||||
};
|
||||
let desired_size = galley.size.max(vec2(available_width, line_spacing));
|
||||
let rect = ui.allocate_space(desired_size);
|
||||
let interact = ui.interact(rect, id, Sense::click_and_drag()); // TODO: implement drag-select
|
||||
|
||||
if interact.clicked {
|
||||
ui.request_kb_focus(id);
|
||||
if let Some(mouse_pos) = ui.input().mouse.pos {
|
||||
state.cursor = Some(galley.char_at(mouse_pos - interact.rect.min).char_idx);
|
||||
}
|
||||
}
|
||||
if interact.hovered {
|
||||
ui.output().cursor_icon = CursorIcon::Text;
|
||||
}
|
||||
let has_kb_focus = ui.has_kb_focus(id);
|
||||
|
||||
if has_kb_focus {
|
||||
let mut cursor = state.cursor.unwrap_or_else(|| text.chars().count());
|
||||
cursor = clamp(cursor, 0..=text.chars().count());
|
||||
|
||||
for event in &ui.input().events {
|
||||
match event {
|
||||
Event::Copy | Event::Cut => {
|
||||
// TODO: cut
|
||||
ui.ctx().output().copied_text = text.clone();
|
||||
}
|
||||
Event::Text(text_to_insert) => {
|
||||
insert_text(&mut cursor, text, text_to_insert);
|
||||
}
|
||||
Event::Key { key, pressed: true } => {
|
||||
on_key_press(&mut cursor, text, *key);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
state.cursor = Some(cursor);
|
||||
|
||||
// layout again to avoid frame delay:
|
||||
let font = &ui.fonts()[text_style];
|
||||
galley = if multiline {
|
||||
font.layout_multiline(text.clone(), available_width)
|
||||
} else {
|
||||
font.layout_single_line(text.clone())
|
||||
};
|
||||
|
||||
// dbg!(&galley);
|
||||
}
|
||||
|
||||
{
|
||||
let bg_rect = interact.rect.expand(2.0); // breathing room for content
|
||||
ui.add_paint_cmd(PaintCmd::Rect {
|
||||
rect: bg_rect,
|
||||
corner_radius: ui.style().interact.style(&interact).corner_radius,
|
||||
fill: Some(ui.style().dark_bg_color),
|
||||
outline: ui.style().interact.style(&interact).rect_outline,
|
||||
});
|
||||
}
|
||||
|
||||
if has_kb_focus {
|
||||
let cursor_blink_hz = ui.style().cursor_blink_hz;
|
||||
let show_cursor =
|
||||
(ui.input().time * cursor_blink_hz as f64 * 3.0).floor() as i64 % 3 != 0;
|
||||
if show_cursor {
|
||||
if let Some(cursor) = state.cursor {
|
||||
let cursor_pos = interact.rect.min + galley.char_start_pos(cursor);
|
||||
ui.add_paint_cmd(PaintCmd::line_segment(
|
||||
[cursor_pos, cursor_pos + vec2(0.0, line_spacing)],
|
||||
color::WHITE,
|
||||
ui.style().text_cursor_width,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ui.add_galley(interact.rect.min, galley, text_style, text_color);
|
||||
ui.memory().text_edit.insert(id, state);
|
||||
interact
|
||||
}
|
||||
}
|
||||
|
||||
fn insert_text(cursor: &mut usize, text: &mut String, text_to_insert: &str) {
|
||||
eprintln!("insert_text {:?}", text_to_insert);
|
||||
|
||||
let mut char_it = text.chars();
|
||||
let mut new_text = String::with_capacity(text.capacity());
|
||||
for _ in 0..*cursor {
|
||||
let c = char_it.next().unwrap();
|
||||
new_text.push(c);
|
||||
}
|
||||
*cursor += text_to_insert.chars().count();
|
||||
new_text += text_to_insert;
|
||||
new_text.extend(char_it);
|
||||
*text = new_text;
|
||||
}
|
||||
|
||||
fn on_key_press(cursor: &mut usize, text: &mut String, key: Key) {
|
||||
// eprintln!("on_key_press before: '{}', cursor at {}", text, cursor);
|
||||
|
||||
match key {
|
||||
Key::Backspace if *cursor > 0 => {
|
||||
*cursor -= 1;
|
||||
|
||||
let mut char_it = text.chars();
|
||||
let mut new_text = String::with_capacity(text.capacity());
|
||||
for _ in 0..*cursor {
|
||||
new_text.push(char_it.next().unwrap())
|
||||
}
|
||||
new_text.extend(char_it.skip(1));
|
||||
*text = new_text;
|
||||
}
|
||||
Key::Delete => {
|
||||
let mut char_it = text.chars();
|
||||
let mut new_text = String::with_capacity(text.capacity());
|
||||
for _ in 0..*cursor {
|
||||
new_text.push(char_it.next().unwrap())
|
||||
}
|
||||
new_text.extend(char_it.skip(1));
|
||||
*text = new_text;
|
||||
}
|
||||
Key::Home => {
|
||||
// To start of paragraph:
|
||||
let pos = line_col_from_char_idx(text, *cursor);
|
||||
*cursor = char_idx_from_line_col(text, (pos.0, 0));
|
||||
}
|
||||
Key::End => {
|
||||
// To end of paragraph:
|
||||
let pos = line_col_from_char_idx(text, *cursor);
|
||||
let line = line_from_number(text, pos.0);
|
||||
*cursor = char_idx_from_line_col(text, (pos.0, line.chars().count()));
|
||||
}
|
||||
Key::Left if *cursor > 0 => {
|
||||
*cursor -= 1;
|
||||
}
|
||||
Key::Right => {
|
||||
*cursor = (*cursor + 1).min(text.chars().count());
|
||||
}
|
||||
Key::Up => {
|
||||
let mut pos = line_col_from_char_idx(text, *cursor);
|
||||
pos.0 = pos.0.saturating_sub(1);
|
||||
*cursor = char_idx_from_line_col(text, pos);
|
||||
}
|
||||
Key::Down => {
|
||||
let mut pos = line_col_from_char_idx(text, *cursor);
|
||||
pos.0 += 1;
|
||||
*cursor = char_idx_from_line_col(text, pos);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// eprintln!("on_key_press after: '{}', cursor at {}\n", text, cursor);
|
||||
}
|
||||
|
||||
fn line_col_from_char_idx(s: &str, char_idx: usize) -> (usize, usize) {
|
||||
let mut char_count = 0;
|
||||
|
||||
let mut last_line_nr = 0;
|
||||
let mut last_line = s;
|
||||
for (line_nr, line) in s.split('\n').enumerate() {
|
||||
let line_width = line.chars().count();
|
||||
if char_idx <= char_count + line_width {
|
||||
return (line_nr, char_idx - char_count);
|
||||
}
|
||||
char_count += line_width + 1;
|
||||
last_line_nr = line_nr;
|
||||
last_line = line;
|
||||
}
|
||||
|
||||
// safe fallback:
|
||||
(last_line_nr, last_line.chars().count())
|
||||
}
|
||||
|
||||
fn char_idx_from_line_col(s: &str, pos: (usize, usize)) -> usize {
|
||||
let mut char_count = 0;
|
||||
for (line_nr, line) in s.split('\n').enumerate() {
|
||||
if line_nr == pos.0 {
|
||||
return char_count + pos.1.min(line.chars().count());
|
||||
}
|
||||
char_count += line.chars().count() + 1;
|
||||
}
|
||||
char_count
|
||||
}
|
||||
|
||||
fn line_from_number(s: &str, desired_line_number: usize) -> &str {
|
||||
for (line_nr, line) in s.split('\n').enumerate() {
|
||||
if line_nr == desired_line_number {
|
||||
return line;
|
||||
}
|
||||
}
|
||||
return s;
|
||||
}
|
||||
Reference in New Issue
Block a user