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

Rename to Emigui

This commit is contained in:
Emil Ernerfeldt
2019-01-12 23:07:30 +01:00
parent 1e8a4d3906
commit 8963a99a09
26 changed files with 281 additions and 249 deletions

127
emigui/src/emigui.rs Normal file
View File

@@ -0,0 +1,127 @@
use std::sync::Arc;
use crate::{
font::Font,
layout,
layout::{LayoutOptions, Region},
style,
types::GuiInput,
widgets::*,
Frame, Painter, RawInput,
};
#[derive(Clone, Copy, Default)]
struct Stats {
num_vertices: usize,
num_triangles: usize,
}
fn show_options(options: &mut LayoutOptions, gui: &mut Region) {
if gui.add(Button::new("Reset LayoutOptions")).clicked {
*options = Default::default();
}
gui.add(Slider::new(&mut options.item_spacing.x, 0.0, 10.0).text("item_spacing.x"));
gui.add(Slider::new(&mut options.item_spacing.y, 0.0, 10.0).text("item_spacing.y"));
gui.add(Slider::new(&mut options.window_padding.x, 0.0, 10.0).text("window_padding.x"));
gui.add(Slider::new(&mut options.window_padding.y, 0.0, 10.0).text("window_padding.y"));
gui.add(Slider::new(&mut options.indent, 0.0, 100.0).text("indent"));
gui.add(Slider::new(&mut options.button_padding.x, 0.0, 20.0).text("button_padding.x"));
gui.add(Slider::new(&mut options.button_padding.y, 0.0, 20.0).text("button_padding.y"));
gui.add(Slider::new(&mut options.start_icon_width, 0.0, 60.0).text("start_icon_width"));
}
fn show_style(style: &mut style::Style, gui: &mut Region) {
if gui.add(Button::new("Reset Style")).clicked {
*style = Default::default();
}
gui.add(Checkbox::new(&mut style.debug_rects, "debug_rects"));
gui.add(Slider::new(&mut style.line_width, 0.0, 10.0).text("line_width"));
}
/// Encapsulates input, layout and painting for ease of use.
pub struct Emigui {
pub last_input: RawInput,
pub data: Arc<layout::Data>,
pub style: style::Style,
pub painter: Painter,
stats: Stats,
}
impl Emigui {
pub fn new(font: Arc<Font>) -> Emigui {
Emigui {
last_input: Default::default(),
data: Arc::new(layout::Data::new(font.clone())),
style: Default::default(),
painter: Painter::new(font),
stats: Default::default(),
}
}
pub fn texture(&self) -> (u16, u16, &[u8]) {
self.painter.texture()
}
pub fn new_frame(&mut self, new_input: RawInput) {
let gui_input = GuiInput::from_last_and_new(&self.last_input, &new_input);
self.last_input = new_input;
let mut new_data = (*self.data).clone();
new_data.new_frame(gui_input);
self.data = Arc::new(new_data);
}
pub fn whole_screen_region(&mut self) -> layout::Region {
let size = self.data.input.screen_size;
layout::Region {
data: self.data.clone(),
id: Default::default(),
dir: layout::Direction::Vertical,
cursor: Default::default(),
bounding_size: Default::default(),
available_space: size,
}
}
pub fn options(&self) -> &layout::LayoutOptions {
&self.data.options
}
pub fn set_options(&mut self, options: layout::LayoutOptions) {
let mut new_data = (*self.data).clone();
new_data.options = options;
self.data = Arc::new(new_data);
}
pub fn paint(&mut self) -> Frame {
let gui_commands = self.data.graphics.lock().unwrap().drain();
let paint_commands = style::into_paint_commands(gui_commands, &self.style);
let frame = self.painter.paint(&paint_commands);
self.stats.num_vertices = frame.vertices.len();
self.stats.num_triangles = frame.indices.len() / 3;
frame
}
pub fn example(&mut self, region: &mut Region) {
let mut options = self.options().clone();
region.foldable("LayoutOptions", |gui| {
show_options(&mut options, gui);
});
let mut style = self.style.clone();
region.foldable("Style", |gui| {
show_style(&mut style, gui);
});
region.foldable("Stats", |gui| {
gui.add(label(format!("num_vertices: {}", self.stats.num_vertices)));
gui.add(label(format!(
"num_triangles: {}",
self.stats.num_triangles
)));
});
// self.set_options(options); // TODO
self.style = style;
}
}

400
emigui/src/font.rs Normal file
View File

@@ -0,0 +1,400 @@
use rusttype::{point, Scale};
use crate::math::{vec2, Vec2};
pub struct TextFragment {
/// The start of each character, starting at zero.
pub x_offsets: Vec<f32>,
/// 0 for the first line, n * line_spacing for the rest
pub y_offset: f32,
pub text: String,
}
impl TextFragment {
pub fn min_x(&self) -> f32 {
*self.x_offsets.first().unwrap()
}
pub fn max_x(&self) -> f32 {
*self.x_offsets.last().unwrap()
}
}
// ----------------------------------------------------------------------------
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct UvRect {
/// X/Y offset for nice rendering
pub offset: (i16, i16),
/// Top left corner.
pub min: (u16, u16),
/// Inclusive
pub max: (u16, u16),
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct GlyphInfo {
id: rusttype::GlyphId,
pub advance_width: f32,
/// Texture coordinates. None for space.
pub uv: Option<UvRect>,
}
/// Printable ASCII characters [32, 126], which excludes control codes.
const FIRST_ASCII: usize = 32; // 32 == space
const LAST_ASCII: usize = 126;
// TODO: break out texture atlas into separate struct, and fill it dynamically, potentially from multiple fonts.
#[derive(Clone)]
pub struct Font {
font: rusttype::Font<'static>,
/// Maximum character height
scale: usize,
/// NUM_CHARS big
glyph_infos: Vec<GlyphInfo>,
atlas_width: usize,
atlas_height: usize,
atlas: Vec<u8>,
}
impl Font {
pub fn new(scale: usize) -> Font {
// TODO: figure out a way to make the wasm smaller despite including a font.
// let font_data = include_bytes!("../fonts/ProggyClean.ttf"); // Use 13 for this. NOTHING ELSE.
// let font_data = include_bytes!("../fonts/DejaVuSans.ttf");
let font_data = include_bytes!("../fonts/Roboto-Regular.ttf");
let font = rusttype::Font::from_bytes(font_data as &[u8]).expect("Error constructing Font");
// println!(
// "font.v_metrics: {:?}",
// font.v_metrics(Scale::uniform(scale as f32))
// );
let glyphs: Vec<_> = Self::supported_characters()
.map(|c| {
let glyph = font.glyph(c);
assert_ne!(
glyph.id().0,
0,
"Failed to find a glyph for the character '{}'",
c
);
let glyph = glyph.scaled(Scale::uniform(scale as f32));
glyph.positioned(point(0.0, 0.0))
})
.collect();
// TODO: decide dynamically?
let atlas_width = 128;
let mut atlas_height = 8;
let mut atlas = vec![0; atlas_width * atlas_height];
// Make one white pixel for use for various stuff:
atlas[0] = 255;
let mut cursor_x = 1;
let mut cursor_y = 0;
let mut row_height = 1;
let mut glyph_infos = vec![];
for glyph in glyphs {
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);
assert!(glyph_width <= atlas_width);
if cursor_x + glyph_width > atlas_width {
// New row:
cursor_x = 0;
cursor_y += row_height;
row_height = 0;
}
row_height = row_height.max(glyph_height);
while cursor_y + row_height >= atlas_height {
atlas_height *= 2;
}
if atlas_width * atlas_height > atlas.len() {
atlas.resize(atlas_width * atlas_height, 0);
}
glyph.draw(|x, y, v| {
if v > 0.0 {
let x = x as usize;
let y = y as usize;
let px = cursor_x + x as usize;
let py = cursor_y + y as usize;
atlas[py * atlas_width + px] = (v * 255.0).round() as u8;
}
});
let offset_y = scale as i16 + bb.min.y as i16 - 4; // TODO: use font.v_metrics
glyph_infos.push(GlyphInfo {
id: glyph.id(),
advance_width: glyph.unpositioned().h_metrics().advance_width,
uv: Some(UvRect {
offset: (bb.min.x as i16, offset_y as i16),
min: (cursor_x as u16, cursor_y as u16),
max: (
(cursor_x + glyph_width - 1) as u16,
(cursor_y + glyph_height - 1) as u16,
),
}),
});
cursor_x += glyph_width;
} else {
// No bounding box. Maybe a space?
glyph_infos.push(GlyphInfo {
id: glyph.id(),
advance_width: glyph.unpositioned().h_metrics().advance_width,
uv: None,
});
}
}
Font {
font,
scale,
glyph_infos,
atlas_width,
atlas_height,
atlas,
}
}
pub fn line_spacing(&self) -> f32 {
self.scale as f32
}
pub fn supported_characters() -> impl Iterator<Item = char> {
(FIRST_ASCII..=LAST_ASCII).map(|c| c as u8 as char)
}
pub fn texture(&self) -> (u16, u16, &[u8]) {
(
self.atlas_width as u16,
self.atlas_height as u16,
&self.atlas,
)
}
pub fn pixel(&self, x: u16, y: u16) -> u8 {
let x = x as usize;
let y = y as usize;
assert!(x < self.atlas_width);
assert!(y < self.atlas_height);
self.atlas[y * self.atlas_width + x]
}
pub fn uv_rect(&self, c: char) -> Option<UvRect> {
let c = c as usize;
if FIRST_ASCII <= c && c <= LAST_ASCII {
self.glyph_infos[c - FIRST_ASCII].uv
} else {
None
}
}
fn glyph_info(&self, c: char) -> Option<GlyphInfo> {
let c = c as usize;
if FIRST_ASCII <= c && c <= LAST_ASCII {
Some(self.glyph_infos[c - FIRST_ASCII])
} else {
None
}
}
/// Returns the a single line of characters separated into words
pub fn layout_single_line(&self, text: &str) -> Vec<TextFragment> {
let scale = Scale::uniform(self.scale as f32);
let mut current_fragment = TextFragment {
x_offsets: vec![0.0],
y_offset: 0.0,
text: String::new(),
};
let mut all_fragments = vec![];
let mut cursor_x = 0.0f32;
let mut last_glyph_id = None;
for c in text.chars() {
if let Some(glyph) = self.glyph_info(c) {
if let Some(last_glyph_id) = last_glyph_id {
cursor_x += self.font.pair_kerning(scale, last_glyph_id, glyph.id)
}
cursor_x += glyph.advance_width;
cursor_x = cursor_x.round();
last_glyph_id = Some(glyph.id);
let is_space = glyph.uv.is_none();
if is_space {
// TODO: also break after hyphens etc
if !current_fragment.text.is_empty() {
all_fragments.push(current_fragment);
current_fragment = TextFragment {
x_offsets: vec![cursor_x],
y_offset: 0.0,
text: String::new(),
}
}
} else {
current_fragment.text.push(c);
current_fragment.x_offsets.push(cursor_x);
}
} else {
// Ignore unknown glyph
}
}
if !current_fragment.text.is_empty() {
all_fragments.push(current_fragment)
}
all_fragments
}
pub fn layout_single_line_max_width(&self, text: &str, max_width: f32) -> Vec<TextFragment> {
let mut words = self.layout_single_line(text);
if words.is_empty() || words.last().unwrap().max_x() <= max_width {
return words; // Early-out
}
let line_spacing = self.line_spacing();
// Break up lines:
let mut line_start_x = 0.0;
let mut cursor_y = 0.0;
for word in words.iter_mut().skip(1) {
if word.max_x() - line_start_x >= max_width {
// Time for a new line:
cursor_y += line_spacing;
line_start_x = word.min_x();
}
word.y_offset += cursor_y;
for x in &mut word.x_offsets {
*x -= line_start_x;
}
}
words
}
/// Returns each line + total bounding box size.
pub fn layout_multiline(&self, text: &str, max_width: f32) -> (Vec<TextFragment>, Vec2) {
let line_spacing = self.line_spacing();
let mut cursor_y = 0.0;
let mut text_fragments = Vec::new();
for line in text.split('\n') {
let mut line_fragments = self.layout_single_line_max_width(&line, max_width);
if let Some(last_word) = line_fragments.last() {
let line_height = last_word.y_offset + line_spacing;
for fragment in &mut line_fragments {
fragment.y_offset += cursor_y;
}
text_fragments.append(&mut line_fragments);
cursor_y += line_height; // TODO: add extra spacing between paragraphs
} else {
cursor_y += line_spacing;
}
cursor_y = cursor_y.round();
}
let mut widest_line = 0.0;
for fragment in &text_fragments {
widest_line = fragment.max_x().max(widest_line);
}
let bounding_size = vec2(widest_line, cursor_y);
(text_fragments, bounding_size)
}
pub fn debug_print_atlas_ascii_art(&self) {
for y in 0..self.atlas_height {
println!(
"{}",
as_ascii(&self.atlas[y * self.atlas_width..(y + 1) * self.atlas_width])
);
}
}
pub fn debug_print_all_chars(&self) {
let max_width = 160;
let scale = Scale::uniform(self.scale as f32);
let mut pixel_rows = vec![vec![0; max_width]; self.scale];
let mut cursor_x = 0.0;
let cursor_y = 0;
let mut last_glyph_id = None;
for c in Self::supported_characters() {
if let Some(glyph) = self.glyph_info(c) {
if let Some(last_glyph_id) = last_glyph_id {
cursor_x += self.font.pair_kerning(scale, last_glyph_id, glyph.id)
}
if cursor_x + glyph.advance_width >= max_width as f32 {
println!("{}", (0..max_width).map(|_| "X").collect::<String>());
for row in pixel_rows {
println!("{}", as_ascii(&row));
}
pixel_rows = vec![vec![0; max_width]; self.scale];
cursor_x = 0.0;
}
if let Some(uv) = glyph.uv {
for x in uv.min.0..=uv.max.0 {
for y in uv.min.1..=uv.max.1 {
let pixel = self.pixel(x as u16, y as u16);
let rx = uv.offset.0 + x as i16 - uv.min.0 as i16;
let ry = uv.offset.1 + y as i16 - uv.min.1 as i16;
let px = (cursor_x + rx as f32).round();
let py = cursor_y + ry;
if 0.0 <= px && 0 <= py {
pixel_rows[py as usize][px as usize] = pixel;
}
}
}
}
cursor_x += glyph.advance_width;
last_glyph_id = Some(glyph.id);
}
}
println!("{}", (0..max_width).map(|_| "X").collect::<String>());
}
}
fn as_ascii(pixels: &[u8]) -> String {
pixels
.iter()
.map(|pixel| {
if *pixel == 0 {
' '
} else if *pixel < 85 {
'.'
} else if *pixel < 170 {
'o'
} else if *pixel < 255 {
'O'
} else {
'X'
}
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn font_test() {
let font = Font::new(13);
font.debug_print_atlas_ascii_art();
font.debug_print_all_chars();
panic!();
}
}

516
emigui/src/layout.rs Normal file
View File

@@ -0,0 +1,516 @@
use std::{
collections::HashSet,
hash::Hash,
sync::{Arc, Mutex},
};
use crate::{
font::{Font, TextFragment},
math::*,
types::*,
widgets::{label, Widget},
};
// ----------------------------------------------------------------------------
#[derive(Clone, Copy, Debug, Serialize)]
pub struct LayoutOptions {
/// Horizontal and vertical padding within a window frame.
pub window_padding: Vec2,
/// Horizontal and vertical spacing between widgets
pub item_spacing: Vec2,
/// Indent foldable regions etc by this much.
pub indent: f32,
/// Button size is text size plus this on each side
pub button_padding: Vec2,
/// Checkboxed, radio button and foldables have an icon at the start.
/// The text starts after this many pixels.
pub start_icon_width: f32,
}
impl Default for LayoutOptions {
fn default() -> Self {
LayoutOptions {
item_spacing: vec2(8.0, 4.0),
window_padding: vec2(6.0, 6.0),
indent: 21.0,
button_padding: vec2(5.0, 3.0),
start_icon_width: 20.0,
}
}
}
// ----------------------------------------------------------------------------
// TODO: rename
pub struct GuiResponse {
/// The mouse is hovering above this
pub hovered: bool,
/// The mouse went got pressed on this thing this frame
pub clicked: bool,
/// The mouse is interacting with this thing (e.g. dragging it)
pub active: bool,
/// Used for showing a popup (if any)
data: Arc<Data>,
}
impl GuiResponse {
/// Show some stuff if the item was hovered
pub fn tooltip<F>(&mut self, add_contents: F) -> &mut Self
where
F: FnOnce(&mut Region),
{
if self.hovered {
let window_pos = self.data.input().mouse_pos + vec2(16.0, 16.0);
show_popup(&self.data, window_pos, add_contents);
}
self
}
/// Show this text if the item was hovered
pub fn tooltip_text<S: Into<String>>(&mut self, text: S) -> &mut Self {
self.tooltip(|popup| {
popup.add(label(text));
})
}
}
// ----------------------------------------------------------------------------
#[derive(Clone, Debug, Default)]
pub struct Memory {
/// The widget being interacted with (e.g. dragged, in case of a slider).
active_id: Option<Id>,
/// Which foldable regions are open.
open_foldables: HashSet<Id>,
}
// ----------------------------------------------------------------------------
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Direction {
Horizontal,
Vertical,
}
impl Default for Direction {
fn default() -> Direction {
Direction::Vertical
}
}
// ----------------------------------------------------------------------------
pub type Id = u64;
pub fn make_id<H: Hash>(source: &H) -> Id {
use std::hash::Hasher;
let mut hasher = std::collections::hash_map::DefaultHasher::new();
source.hash(&mut hasher);
hasher.finish()
}
// ----------------------------------------------------------------------------
/// TODO: improve this
#[derive(Clone, Default)]
pub struct GraphicLayers {
pub(crate) graphics: Vec<GuiCmd>,
pub(crate) hovering_graphics: Vec<GuiCmd>,
}
impl GraphicLayers {
pub fn drain(&mut self) -> impl ExactSizeIterator<Item = GuiCmd> {
// TODO: there must be a nicer way to do this?
let mut all_commands: Vec<_> = self.graphics.drain(..).collect();
all_commands.extend(self.hovering_graphics.drain(..));
all_commands.into_iter()
}
}
// ----------------------------------------------------------------------------
// TODO: give a better name.
/// Contains the input, options and output of all GUI commands.
pub struct Data {
pub(crate) options: LayoutOptions,
pub(crate) font: Arc<Font>,
pub(crate) input: GuiInput,
pub(crate) memory: Mutex<Memory>,
pub(crate) graphics: Mutex<GraphicLayers>,
}
impl Clone for Data {
fn clone(&self) -> Self {
Data {
options: self.options.clone(),
font: self.font.clone(),
input: self.input.clone(),
memory: Mutex::new(self.memory.lock().unwrap().clone()),
graphics: Mutex::new(self.graphics.lock().unwrap().clone()),
}
}
}
impl Data {
pub fn new(font: Arc<Font>) -> Data {
Data {
options: Default::default(),
font,
input: Default::default(),
memory: Default::default(),
graphics: Default::default(),
}
}
pub fn input(&self) -> &GuiInput {
&self.input
}
pub fn options(&self) -> &LayoutOptions {
&self.options
}
pub fn set_options(&mut self, options: LayoutOptions) {
self.options = options;
}
// TODO: move
pub fn new_frame(&mut self, gui_input: GuiInput) {
self.input = gui_input;
if !gui_input.mouse_down {
self.memory.lock().unwrap().active_id = None;
}
}
}
/// Show a pop-over window
pub fn show_popup<F>(data: &Arc<Data>, window_pos: Vec2, add_contents: F)
where
F: FnOnce(&mut Region),
{
// TODO: nicer way to do layering!
let num_graphics_before = data.graphics.lock().unwrap().graphics.len();
let window_padding = data.options.window_padding;
let mut popup_region = Region {
data: data.clone(),
id: Default::default(),
dir: Direction::Vertical,
cursor: window_pos + window_padding,
bounding_size: vec2(0.0, 0.0),
available_space: vec2(400.0, std::f32::INFINITY), // TODO: popup/tooltip width
};
add_contents(&mut popup_region);
// TODO: handle the last item_spacing in a nicer way
let inner_size = popup_region.bounding_size - data.options.item_spacing;
let outer_size = inner_size + 2.0 * window_padding;
let rect = Rect::from_min_size(window_pos, outer_size);
let mut graphics = data.graphics.lock().unwrap();
let popup_graphics = graphics.graphics.split_off(num_graphics_before);
graphics.hovering_graphics.push(GuiCmd::Window { rect });
graphics.hovering_graphics.extend(popup_graphics);
}
// ----------------------------------------------------------------------------
/// Represents a region of the screen
/// with a type of layout (horizontal or vertical).
/// TODO: make Region a trait so we can have type-safe HorizontalRegion etc?
pub struct Region {
pub(crate) data: Arc<Data>,
/// Unique ID of this region.
pub(crate) id: Id,
/// Doesn't change.
pub(crate) dir: Direction,
/// Changes only along self.dir
pub(crate) cursor: Vec2,
/// Bounding box children.
/// We keep track of our max-size along the orthogonal to self.dir
pub(crate) bounding_size: Vec2,
/// This how much space we can take up without overflowing our parent.
/// Shrinks as cursor increments.
pub(crate) available_space: Vec2,
}
impl Region {
/// 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_graphic(&mut self, gui_cmd: GuiCmd) {
self.data.graphics.lock().unwrap().graphics.push(gui_cmd)
}
pub fn options(&self) -> &LayoutOptions {
self.data.options()
}
pub fn input(&self) -> &GuiInput {
self.data.input()
}
pub fn cursor(&self) -> Vec2 {
self.cursor
}
pub fn font(&self) -> &Font {
&*self.data.font
}
pub fn width(&self) -> f32 {
self.available_space.x
}
// ------------------------------------------------------------------------
// Sub-regions:
pub fn foldable<S, F>(&mut self, text: S, add_contents: F) -> GuiResponse
where
S: Into<String>,
F: FnOnce(&mut Region),
{
assert!(
self.dir == Direction::Vertical,
"Horizontal foldable is unimplemented"
);
let text: String = text.into();
let id = self.make_child_id(&text);
let (text, text_size) = self.font().layout_multiline(&text, self.width());
let text_cursor = self.cursor + self.options().button_padding;
let (rect, interact) = self.reserve_space(
vec2(
self.available_space.x,
text_size.y + 2.0 * self.options().button_padding.y,
),
Some(id),
);
let open = {
let mut memory = self.data.memory.lock().unwrap();
if interact.clicked {
if memory.open_foldables.contains(&id) {
memory.open_foldables.remove(&id);
} else {
memory.open_foldables.insert(id);
}
}
memory.open_foldables.contains(&id)
};
self.add_graphic(GuiCmd::FoldableHeader {
interact,
rect,
open,
});
self.add_text(
text_cursor + vec2(self.options().start_icon_width, 0.0),
text,
);
if open {
let old_id = self.id;
self.id = id;
self.indent(add_contents);
self.id = old_id;
}
self.response(interact)
}
/// Create a child region which is indented to the right
pub fn indent<F>(&mut self, add_contents: F)
where
F: FnOnce(&mut Region),
{
let indent = vec2(self.options().indent, 0.0);
let mut child_region = Region {
data: self.data.clone(),
id: self.id,
dir: self.dir,
cursor: self.cursor + indent,
bounding_size: vec2(0.0, 0.0),
available_space: self.available_space - indent,
};
add_contents(&mut child_region);
let size = child_region.bounding_size;
self.reserve_space_inner(indent + size);
}
/// A horizontally centered region of the given width.
pub fn centered_column(&mut self, width: f32) -> Region {
Region {
data: self.data.clone(),
id: self.id,
dir: self.dir,
cursor: vec2((self.available_space.x - width) / 2.0, self.cursor.y),
bounding_size: vec2(0.0, 0.0),
available_space: vec2(width, self.available_space.y),
}
}
/// Start a region with horizontal layout
pub fn horizontal<F>(&mut self, add_contents: F)
where
F: FnOnce(&mut Region),
{
let mut child_region = Region {
data: self.data.clone(),
id: self.id,
dir: Direction::Horizontal,
cursor: self.cursor,
bounding_size: vec2(0.0, 0.0),
available_space: self.available_space,
};
add_contents(&mut child_region);
let size = child_region.bounding_size;
self.reserve_space_inner(size);
}
/// Temporarily split split a vertical layout into several columns.
///
/// gui.columns(2, |columns| {
/// columns[0].add(label("First column"));
/// columns[1].add(label("Second column"));
/// });
pub fn columns<F, R>(&mut self, num_columns: usize, add_contents: F) -> R
where
F: FnOnce(&mut [Region]) -> R,
{
// TODO: ensure there is space
let padding = self.options().item_spacing.x;
let total_padding = padding * (num_columns as f32 - 1.0);
let column_width = (self.available_space.x - total_padding) / (num_columns as f32);
let mut columns: Vec<Region> = (0..num_columns)
.map(|col_idx| Region {
data: self.data.clone(),
id: self.make_child_id(&("column", col_idx)),
dir: Direction::Vertical,
cursor: self.cursor + vec2((col_idx as f32) * (column_width + padding), 0.0),
bounding_size: vec2(0.0, 0.0),
available_space: vec2(column_width, self.available_space.y),
})
.collect();
let result = add_contents(&mut columns[..]);
let mut max_height = 0.0;
for region in columns {
let size = region.bounding_size;
max_height = size.y.max(max_height);
}
self.reserve_space_inner(vec2(self.available_space.x, max_height));
result
}
// ------------------------------------------------------------------------
pub fn add<W: Widget>(&mut self, widget: W) -> GuiResponse {
widget.add_to(self)
}
// ------------------------------------------------------------------------
pub fn reserve_space(
&mut self,
size: Vec2,
interaction_id: Option<Id>,
) -> (Rect, InteractInfo) {
let rect = Rect {
pos: self.cursor,
size,
};
self.reserve_space_inner(size + self.options().item_spacing);
let hovered = rect.contains(self.input().mouse_pos);
let clicked = hovered && self.input().mouse_clicked;
let active = if interaction_id.is_some() {
let mut memory = self.data.memory.lock().unwrap();
if clicked {
memory.active_id = interaction_id;
}
memory.active_id == interaction_id
} else {
false
};
let interact = InteractInfo {
hovered,
clicked,
active,
};
(rect, interact)
}
// TODO: Return a Rect
/// Reserve this much space and move the cursor.
pub fn reserve_space_inner(&mut self, size: Vec2) {
if self.dir == Direction::Horizontal {
self.cursor.x += size.x;
self.available_space.x -= size.x;
self.bounding_size.x += size.x;
self.bounding_size.y = self.bounding_size.y.max(size.y);
} else {
self.cursor.y += size.y;
self.available_space.y -= size.x;
self.bounding_size.y += size.y;
self.bounding_size.x = self.bounding_size.x.max(size.x);
}
}
pub fn make_child_id<H: Hash>(&self, child_id: &H) -> Id {
use std::hash::Hasher;
let mut hasher = std::collections::hash_map::DefaultHasher::new();
hasher.write_u64(self.id);
child_id.hash(&mut hasher);
hasher.finish()
}
pub fn combined_id(&self, child_id: Option<Id>) -> Option<Id> {
child_id.map(|child_id| {
use std::hash::Hasher;
let mut hasher = std::collections::hash_map::DefaultHasher::new();
hasher.write_u64(self.id);
child_id.hash(&mut hasher);
hasher.finish()
})
}
pub fn add_text(&mut self, pos: Vec2, text: Vec<TextFragment>) {
for fragment in text {
self.add_graphic(GuiCmd::Text {
pos: pos + vec2(0.0, fragment.y_offset),
style: TextStyle::Label,
text: fragment.text,
x_offsets: fragment.x_offsets,
});
}
}
pub fn response(&mut self, interact: InteractInfo) -> GuiResponse {
GuiResponse {
hovered: interact.hovered,
clicked: interact.clicked,
active: interact.active,
data: self.data.clone(),
}
}
}

26
emigui/src/lib.rs Normal file
View File

@@ -0,0 +1,26 @@
#![deny(warnings)]
extern crate rusttype;
extern crate serde;
#[macro_use] // TODO: get rid of this
extern crate serde_derive;
mod emigui;
mod font;
mod layout;
pub mod math;
mod painter;
mod style;
pub mod types;
pub mod widgets;
pub use crate::{
emigui::Emigui,
font::Font,
layout::LayoutOptions,
layout::Region,
painter::{Frame, Painter, Vertex},
style::Style,
types::RawInput,
};

153
emigui/src/math.rs Normal file
View File

@@ -0,0 +1,153 @@
#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize)]
pub struct Vec2 {
pub x: f32,
pub y: f32,
}
impl Vec2 {
pub fn normalized(self) -> Vec2 {
let len = self.x.hypot(self.y);
if len <= 0.0 {
self
} else {
self / len
}
}
pub fn rot90(self) -> Vec2 {
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
}
}
impl std::ops::AddAssign for Vec2 {
fn add_assign(&mut self, other: Vec2) {
*self = Vec2 {
x: self.x + other.x,
y: self.y + other.y,
};
}
}
impl std::ops::Add for Vec2 {
type Output = Vec2;
fn add(self, rhs: Vec2) -> Vec2 {
Vec2 {
x: self.x + rhs.x,
y: self.y + rhs.y,
}
}
}
impl std::ops::Sub for Vec2 {
type Output = Vec2;
fn sub(self, rhs: Vec2) -> Vec2 {
Vec2 {
x: self.x - rhs.x,
y: self.y - rhs.y,
}
}
}
impl std::ops::Mul<f32> for Vec2 {
type Output = Vec2;
fn mul(self, factor: f32) -> Vec2 {
Vec2 {
x: self.x * factor,
y: self.y * factor,
}
}
}
impl std::ops::Mul<Vec2> for f32 {
type Output = Vec2;
fn mul(self, vec: Vec2) -> Vec2 {
Vec2 {
x: self * vec.x,
y: self * vec.y,
}
}
}
impl std::ops::Div<f32> for Vec2 {
type Output = Vec2;
fn div(self, factor: f32) -> Vec2 {
Vec2 {
x: self.x / factor,
y: self.y / factor,
}
}
}
pub fn vec2(x: f32, y: f32) -> Vec2 {
Vec2 { x, y }
}
#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize)]
pub struct Rect {
pub pos: Vec2,
pub size: Vec2,
}
impl Rect {
pub fn from_min_size(min: Vec2, size: Vec2) -> Self {
Rect { pos: min, size }
}
pub fn from_center_size(center: Vec2, size: Vec2) -> Self {
Rect {
pos: center - size * 0.5,
size,
}
}
pub fn contains(&self, p: Vec2) -> bool {
self.pos.x <= p.x
&& p.x <= self.pos.x + self.size.x
&& self.pos.y <= p.y
&& p.y <= self.pos.y + self.size.y
}
pub fn center(&self) -> Vec2 {
Vec2 {
x: self.pos.x + self.size.x / 2.0,
y: self.pos.y + self.size.y / 2.0,
}
}
pub fn min(&self) -> Vec2 {
self.pos
}
pub fn max(&self) -> Vec2 {
self.pos + self.size
}
}
pub fn lerp(min: f32, max: f32, t: f32) -> f32 {
(1.0 - t) * min + t * max
}
pub fn remap(from: f32, from_min: f32, from_max: f32, to_min: f32, to_max: f32) -> f32 {
let t = (from - from_min) / (from_max - from_min);
lerp(to_min, to_max, t)
}
pub fn remap_clamp(from: f32, from_min: f32, from_max: f32, to_min: f32, to_max: f32) -> f32 {
let t = if from <= from_min {
0.0
} else if from >= from_max {
1.0
} else {
(from - from_min) / (from_max - from_min)
};
lerp(to_min, to_max, t)
}
pub const TAU: f32 = 2.0 * std::f32::consts::PI;

376
emigui/src/painter.rs Normal file
View File

@@ -0,0 +1,376 @@
use std::sync::Arc;
const ANTI_ALIAS: bool = true;
const AA_SIZE: f32 = 1.0;
/// Outputs render info in a format suitable for e.g. OpenGL.
use crate::{
font::Font,
math::{remap, vec2, Vec2, TAU},
types::{Color, PaintCmd},
};
#[derive(Clone, Copy, Debug, Default)]
pub struct Vertex {
/// Pixel coordinates
pub pos: Vec2,
/// Texel indices into the texture
pub uv: (u16, u16),
/// sRGBA
pub color: Color,
}
#[derive(Clone, Debug, Default)]
pub struct Frame {
pub clear_color: Option<Color>,
/// Draw as triangles (i.e. the length is a multiple of three)
pub indices: Vec<u32>,
pub vertices: Vec<Vertex>,
}
#[derive(Clone, Copy, PartialEq)]
pub enum PathType {
Open,
Closed,
}
use self::PathType::*;
impl Frame {
fn triangle(&mut self, a: u32, b: u32, c: u32) {
self.indices.push(a);
self.indices.push(b);
self.indices.push(c);
}
/// Uniformly colored rectangle
pub fn add_rect(&mut self, top_left: Vertex, bottom_right: Vertex) {
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: vec2(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: vec2(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);
}
pub fn fill_closed_path(&mut self, points: &[Vec2], normals: &[Vec2], color: Color) {
assert_eq!(points.len(), normals.len());
let n = points.len() as u32;
let vert = |pos, color| Vertex {
pos,
uv: (0, 0),
color,
};
if ANTI_ALIAS {
let color_outer = color.transparent();
let idx_inner = self.vertices.len() as u32;
let idx_outer = idx_inner + 1;
for i in 2..n {
self.triangle(idx_inner + 2 * (i - 1), idx_inner, idx_inner + 2 * i);
}
let mut i0 = n - 1;
for i1 in 0..n {
let dm = normals[i1 as usize] * AA_SIZE * 0.5;
self.vertices.push(vert(points[i1 as usize] - dm, color));
self.vertices
.push(vert(points[i1 as usize] + dm, color_outer));
self.triangle(idx_inner + i1 * 2, idx_inner + i0 * 2, idx_outer + 2 * i0);
self.triangle(idx_outer + i0 * 2, idx_outer + i1 * 2, idx_inner + 2 * i1);
i0 = i1;
}
} else {
let idx = self.vertices.len() as u32;
self.vertices
.extend(points.iter().map(|&pos| vert(pos, color)));
for i in 2..n {
self.triangle(idx, idx + i - 1, idx + i);
}
}
}
pub fn paint_path(
&mut self,
path_type: PathType,
points: &[Vec2],
normals: &[Vec2],
color: Color,
width: f32,
) {
assert_eq!(points.len(), normals.len());
let n = points.len() as u32;
let hw = width / 2.0;
let idx = self.vertices.len() as u32;
let vert = |pos, color| Vertex {
pos,
uv: (0, 0),
color,
};
if ANTI_ALIAS {
let color_outer = color.transparent();
let thin_line = width <= 1.0;
let mut color_inner = color;
if thin_line {
// Fade out as it gets thinner:
color_inner.a = (color_inner.a as f32 * width).round() as u8;
}
// TODO: line caps ?
let mut i0 = n - 1;
for i1 in 0..n {
let connect_with_previous = path_type == PathType::Closed || i1 > 0;
if thin_line {
let p = points[i1 as usize];
let n = normals[i1 as usize];
self.vertices.push(vert(p + n * AA_SIZE, color_outer));
self.vertices.push(vert(p, color_inner));
self.vertices.push(vert(p - n * AA_SIZE, color_outer));
if connect_with_previous {
self.triangle(idx + 3 * i0 + 0, idx + 3 * i0 + 1, idx + 3 * i1 + 0);
self.triangle(idx + 3 * i0 + 1, idx + 3 * i1 + 0, idx + 3 * i1 + 1);
self.triangle(idx + 3 * i0 + 1, idx + 3 * i0 + 2, idx + 3 * i1 + 1);
self.triangle(idx + 3 * i0 + 2, idx + 3 * i1 + 1, idx + 3 * i1 + 2);
}
} else {
let hw = (width - AA_SIZE) * 0.5;
let p = points[i1 as usize];
let n = normals[i1 as usize];
self.vertices
.push(vert(p + n * (hw + AA_SIZE), color_outer));
self.vertices.push(vert(p + n * (hw + 0.0), color_inner));
self.vertices.push(vert(p - n * (hw + 0.0), color_inner));
self.vertices
.push(vert(p - n * (hw + AA_SIZE), color_outer));
if connect_with_previous {
self.triangle(idx + 4 * i0 + 0, idx + 4 * i0 + 1, idx + 4 * i1 + 0);
self.triangle(idx + 4 * i0 + 1, idx + 4 * i1 + 0, idx + 4 * i1 + 1);
self.triangle(idx + 4 * i0 + 1, idx + 4 * i0 + 2, idx + 4 * i1 + 1);
self.triangle(idx + 4 * i0 + 2, idx + 4 * i1 + 1, idx + 4 * i1 + 2);
self.triangle(idx + 4 * i0 + 2, idx + 4 * i0 + 3, idx + 4 * i1 + 2);
self.triangle(idx + 4 * i0 + 3, idx + 4 * i1 + 2, idx + 4 * i1 + 3);
}
}
i0 = i1;
}
} else {
let last_index = if path_type == Closed { n } else { n - 1 };
for i in 0..last_index {
self.triangle(
idx + (2 * i + 0) % (2 * n),
idx + (2 * i + 1) % (2 * n),
idx + (2 * i + 2) % (2 * n),
);
self.triangle(
idx + (2 * i + 2) % (2 * n),
idx + (2 * i + 1) % (2 * n),
idx + (2 * i + 3) % (2 * n),
);
}
for (&p, &n) in points.iter().zip(normals) {
self.vertices.push(vert(p + hw * n, color));
self.vertices.push(vert(p - hw * n, color));
}
}
}
}
#[derive(Clone)]
pub struct Painter {
font: Arc<Font>,
}
impl Painter {
pub fn new(font: Arc<Font>) -> Painter {
Painter { font }
}
/// 8-bit row-major font atlas texture, (width, height, pixels).
pub fn texture(&self) -> (u16, u16, &[u8]) {
self.font.texture()
}
pub fn paint(&self, commands: &[PaintCmd]) -> Frame {
let mut path_points = Vec::new();
let mut path_normals = Vec::new();
let mut frame = Frame::default();
for cmd in commands {
match cmd {
PaintCmd::Circle {
center,
fill_color,
outline,
radius,
} => {
path_points.clear();
path_normals.clear();
let n = 32; // TODO: parameter
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());
path_normals.push(normal);
path_points.push(*center + *radius * normal);
}
if let Some(color) = fill_color {
frame.fill_closed_path(&path_points, &path_normals, *color);
}
if let Some(outline) = outline {
frame.paint_path(
Closed,
&path_points,
&path_normals,
outline.color,
outline.width,
);
}
}
PaintCmd::Clear { fill_color } => {
frame.clear_color = Some(*fill_color);
}
PaintCmd::Line {
points,
color,
width,
} => {
let n = points.len();
if n >= 2 {
path_points = points.clone();
path_normals.clear();
path_normals.push((path_points[1] - path_points[0]).normalized().rot90());
for i in 1..n - 1 {
let n0 = (path_points[i] - path_points[i - 1]).normalized().rot90();
let n1 = (path_points[i + 1] - path_points[i]).normalized().rot90();
let v = (n0 + n1) / 2.0;
let normal = v / v.length_sq();
path_normals.push(normal); // TODO: handle VERY sharp turns better
}
path_normals.push(
(path_points[n - 1] - path_points[n - 2])
.normalized()
.rot90(),
);
frame.paint_path(Open, &path_points, &path_normals, *color, *width);
}
}
PaintCmd::Rect {
corner_radius,
fill_color,
outline,
pos,
size,
} => {
path_points.clear();
path_normals.clear();
let min = *pos;
let max = *pos + *size;
let cr = corner_radius.min(size.x * 0.5).min(size.y * 0.5);
if cr <= 0.0 {
path_points.push(vec2(min.x, min.y));
path_normals.push(vec2(-1.0, -1.0));
path_points.push(vec2(max.x, min.y));
path_normals.push(vec2(1.0, -1.0));
path_points.push(vec2(max.x, max.y));
path_normals.push(vec2(1.0, 1.0));
path_points.push(vec2(min.x, max.y));
path_normals.push(vec2(-1.0, 1.0));
} else {
let n = 8;
let mut add_arc = |c, quadrant| {
let quadrant = quadrant as f32;
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());
path_points.push(c + cr * normal);
path_normals.push(normal);
}
};
add_arc(vec2(max.x - cr, max.y - cr), 0);
add_arc(vec2(min.x + cr, max.y - cr), 1);
add_arc(vec2(min.x + cr, min.y + cr), 2);
add_arc(vec2(max.x - cr, min.y + cr), 3);
}
if let Some(color) = fill_color {
frame.fill_closed_path(&path_points, &path_normals, *color);
}
if let Some(outline) = outline {
frame.paint_path(
Closed,
&path_points,
&path_normals,
outline.color,
outline.width,
);
}
}
PaintCmd::Text {
color,
pos,
text,
x_offsets,
} => {
for (c, x_offset) in text.chars().zip(x_offsets.iter()) {
if let Some(glyph) = self.font.uv_rect(c) {
let mut top_left = Vertex {
pos: *pos
+ vec2(
x_offset + (glyph.offset.0 as f32),
glyph.offset.1 as f32,
),
uv: (glyph.min.0, glyph.min.1),
color: *color,
};
top_left.pos.x = top_left.pos.x.round(); // Pixel-perfection.
top_left.pos.y = top_left.pos.y.round(); // Pixel-perfection.
let bottom_right = Vertex {
pos: top_left.pos
+ vec2(
(1 + glyph.max.0 - glyph.min.0) as f32,
(1 + glyph.max.1 - glyph.min.1) as f32,
),
uv: (glyph.max.0 + 1, glyph.max.1 + 1),
color: *color,
};
frame.add_rect(top_left, bottom_right);
}
}
}
}
}
frame
}
}

276
emigui/src/style.rs Normal file
View File

@@ -0,0 +1,276 @@
use crate::{math::*, types::*};
#[derive(Clone, Debug)]
pub struct Style {
/// Show rectangles around each widget
pub debug_rects: bool,
/// For stuff like check marks in check boxes.
pub line_width: f32,
}
impl Default for Style {
fn default() -> Style {
Style {
debug_rects: false,
line_width: 2.0,
}
}
}
impl Style {
/// e.g. the background of the slider
fn background_fill_color(&self) -> Color {
srgba(34, 34, 34, 200)
}
fn text_color(&self) -> Color {
srgba(255, 255, 255, 187)
}
/// Fill color of the interactive part of a component (button, slider grab, checkbox, ...)
fn interact_fill_color(&self, interact: &InteractInfo) -> Color {
if interact.active {
srgba(136, 136, 136, 255)
} else if interact.hovered {
srgba(100, 100, 100, 255)
} else {
srgba(68, 68, 68, 220)
}
}
/// Stroke and text color of the interactive part of a component (button, slider grab, checkbox, ...)
fn interact_stroke_color(&self, interact: &InteractInfo) -> Color {
if interact.active {
srgba(255, 255, 255, 255)
} else if interact.hovered {
srgba(255, 255, 255, 200)
} else {
srgba(255, 255, 255, 170)
}
}
/// Returns small icon rectangle and big icon rectangle
fn icon_rectangles(&self, rect: &Rect) -> (Rect, Rect) {
let box_side = 16.0;
let big_icon_rect = Rect::from_center_size(
vec2(rect.min().x + 4.0 + box_side * 0.5, rect.center().y),
vec2(box_side, box_side),
);
let small_icon_rect = Rect::from_center_size(big_icon_rect.center(), vec2(10.0, 10.0));
(small_icon_rect, big_icon_rect)
}
}
fn debug_rect(rect: Rect) -> PaintCmd {
PaintCmd::Rect {
corner_radius: 0.0,
fill_color: None,
outline: Some(Outline {
color: srgba(255, 255, 255, 255),
width: 1.0,
}),
pos: rect.pos,
size: rect.size,
}
}
/// TODO: a Style struct which defines colors etc
fn translate_cmd(out_commands: &mut Vec<PaintCmd>, style: &Style, cmd: GuiCmd) {
match cmd {
GuiCmd::PaintCommands(mut commands) => out_commands.append(&mut commands),
GuiCmd::Button { interact, rect } => {
out_commands.push(PaintCmd::Rect {
corner_radius: 5.0,
fill_color: Some(style.interact_fill_color(&interact)),
outline: None,
pos: rect.pos,
size: rect.size,
});
if style.debug_rects {
out_commands.push(debug_rect(rect));
}
}
GuiCmd::Checkbox {
checked,
interact,
rect,
} => {
let (small_icon_rect, big_icon_rect) = style.icon_rectangles(&rect);
out_commands.push(PaintCmd::Rect {
corner_radius: 3.0,
fill_color: Some(style.interact_fill_color(&interact)),
outline: None,
pos: big_icon_rect.pos,
size: big_icon_rect.size,
});
let stroke_color = style.interact_stroke_color(&interact);
if checked {
out_commands.push(PaintCmd::Line {
points: vec![
vec2(small_icon_rect.min().x, small_icon_rect.center().y),
vec2(small_icon_rect.center().x, small_icon_rect.max().y),
vec2(small_icon_rect.max().x, small_icon_rect.min().y),
],
color: stroke_color,
width: style.line_width,
});
}
if style.debug_rects {
out_commands.push(debug_rect(rect));
}
}
GuiCmd::FoldableHeader {
interact,
open,
rect,
} => {
let fill_color = style.interact_fill_color(&interact);
let stroke_color = style.interact_stroke_color(&interact);
out_commands.push(PaintCmd::Rect {
corner_radius: 3.0,
fill_color: Some(fill_color),
outline: None,
pos: rect.pos,
size: rect.size,
});
// TODO: paint a little triangle or arrow or something instead of this
let (small_icon_rect, _) = style.icon_rectangles(&rect);
// Draw a minus:
out_commands.push(PaintCmd::Line {
points: vec![
vec2(small_icon_rect.min().x, small_icon_rect.center().y),
vec2(small_icon_rect.max().x, small_icon_rect.center().y),
],
color: stroke_color,
width: style.line_width,
});
if !open {
// Draw it as a plus:
out_commands.push(PaintCmd::Line {
points: vec![
vec2(small_icon_rect.center().x, small_icon_rect.min().y),
vec2(small_icon_rect.center().x, small_icon_rect.max().y),
],
color: stroke_color,
width: style.line_width,
});
}
}
GuiCmd::RadioButton {
checked,
interact,
rect,
} => {
let fill_color = style.interact_fill_color(&interact);
let stroke_color = style.interact_stroke_color(&interact);
let (small_icon_rect, big_icon_rect) = style.icon_rectangles(&rect);
out_commands.push(PaintCmd::Circle {
center: big_icon_rect.center(),
fill_color: Some(fill_color),
outline: None,
radius: big_icon_rect.size.x / 2.0,
});
if checked {
out_commands.push(PaintCmd::Circle {
center: small_icon_rect.center(),
fill_color: Some(stroke_color),
outline: None,
radius: small_icon_rect.size.x / 2.0,
});
}
if style.debug_rects {
out_commands.push(debug_rect(rect));
}
}
GuiCmd::Slider {
interact,
max,
min,
rect,
value,
} => {
let thin_rect = Rect::from_center_size(rect.center(), vec2(rect.size.x, 6.0));
let marker_center_x = remap_clamp(value, min, max, rect.min().x, rect.max().x);
let marker_rect = Rect::from_center_size(
vec2(marker_center_x, thin_rect.center().y),
vec2(16.0, 16.0),
);
out_commands.push(PaintCmd::Rect {
corner_radius: 2.0,
fill_color: Some(style.background_fill_color()),
outline: None,
pos: thin_rect.pos,
size: thin_rect.size,
});
out_commands.push(PaintCmd::Rect {
corner_radius: 3.0,
fill_color: Some(style.interact_fill_color(&interact)),
outline: None,
pos: marker_rect.pos,
size: marker_rect.size,
});
if style.debug_rects {
out_commands.push(debug_rect(rect));
}
}
GuiCmd::Text {
pos,
style: text_style,
text,
x_offsets,
} => {
let color = match text_style {
TextStyle::Label => style.text_color(),
};
out_commands.push(PaintCmd::Text {
color,
pos,
text,
x_offsets,
});
}
GuiCmd::Window { rect } => {
out_commands.push(PaintCmd::Rect {
corner_radius: 5.0,
fill_color: Some(style.background_fill_color()),
outline: Some(Outline {
color: srgba(255, 255, 255, 255), // TODO
width: 1.0,
}),
pos: rect.pos,
size: rect.size,
});
}
}
}
pub fn into_paint_commands<'a, GuiCmdIterator>(
gui_commands: GuiCmdIterator,
style: &Style,
) -> Vec<PaintCmd>
where
GuiCmdIterator: Iterator<Item = GuiCmd>,
{
let mut paint_commands = vec![];
for gui_cmd in gui_commands {
translate_cmd(&mut paint_commands, style, gui_cmd)
}
paint_commands
}

186
emigui/src/types.rs Normal file
View File

@@ -0,0 +1,186 @@
use crate::math::{Rect, Vec2};
// ----------------------------------------------------------------------------
/// What the integration gives to the gui.
#[derive(Clone, Copy, Debug, Default, Deserialize)]
pub struct RawInput {
/// Is the button currently down?
pub mouse_down: bool,
/// Current position of the mouse in points.
pub mouse_pos: Vec2,
/// Size of the screen in points.
pub screen_size: Vec2,
}
/// What the gui maintains
#[derive(Clone, Copy, Debug, Default)]
pub struct GuiInput {
/// Is the button currently down?
pub mouse_down: bool,
/// The mouse went from !down to down
pub mouse_clicked: bool,
/// The mouse went from down to !down
pub mouse_released: bool,
/// Current position of the mouse in points.
pub mouse_pos: Vec2,
/// Size of the screen in points.
pub screen_size: Vec2,
}
impl GuiInput {
pub fn from_last_and_new(last: &RawInput, new: &RawInput) -> GuiInput {
GuiInput {
mouse_down: new.mouse_down,
mouse_clicked: !last.mouse_down && new.mouse_down,
mouse_released: last.mouse_down && !new.mouse_down,
mouse_pos: new.mouse_pos,
screen_size: new.screen_size,
}
}
}
// ----------------------------------------------------------------------------
/// 0-255 sRGBA
#[derive(Clone, Copy, Debug, Default, Serialize)]
pub struct Color {
pub r: u8,
pub g: u8,
pub b: u8,
pub a: u8,
}
impl Color {
pub const WHITE: Color = srgba(255, 255, 255, 255);
pub fn transparent(self) -> Color {
Color {
r: self.r,
g: self.g,
b: self.b,
a: 0,
}
}
}
pub const fn srgba(r: u8, g: u8, b: u8, a: u8) -> Color {
Color { r, g, b, a }
}
// ----------------------------------------------------------------------------
#[derive(Clone, Copy, Debug, Default, Serialize)]
pub struct InteractInfo {
/// The mouse is hovering above this
pub hovered: bool,
/// The mouse went got pressed on this thing this frame
pub clicked: bool,
/// The mouse is interacting with this thing (e.g. dragging it)
pub active: bool,
}
#[derive(Clone, Copy, Debug, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum TextStyle {
Label,
}
#[derive(Clone, Debug, Serialize)]
pub enum GuiCmd {
PaintCommands(Vec<PaintCmd>),
/// The background for a button
Button {
interact: InteractInfo,
rect: Rect,
},
Checkbox {
checked: bool,
interact: InteractInfo,
rect: Rect,
},
/// The header button background for a foldable region
FoldableHeader {
interact: InteractInfo,
open: bool,
rect: Rect,
},
RadioButton {
checked: bool,
interact: InteractInfo,
rect: Rect,
},
Slider {
interact: InteractInfo,
max: f32,
min: f32,
rect: Rect,
value: f32,
},
/// Paint a single line of mono-space text.
/// The text should start at the given position and flow to the right.
/// The text should be vertically centered at the given position.
Text {
pos: Vec2,
style: TextStyle,
text: String,
/// Start each character in the text, as offset from pos.
x_offsets: Vec<f32>,
},
/// Background of e.g. a popup
Window {
rect: Rect,
},
}
// ----------------------------------------------------------------------------
#[derive(Clone, Debug, Serialize)]
pub struct Outline {
pub width: f32,
pub color: Color,
}
#[derive(Clone, Debug, Serialize)] // TODO: copy
#[serde(rename_all = "snake_case", tag = "kind")]
pub enum PaintCmd {
Circle {
center: Vec2,
fill_color: Option<Color>,
outline: Option<Outline>,
radius: f32,
},
Clear {
fill_color: Color,
},
Line {
points: Vec<Vec2>,
color: Color,
width: f32,
},
Rect {
corner_radius: f32,
fill_color: Option<Color>,
outline: Option<Outline>,
pos: Vec2,
size: Vec2,
},
/// Paint a single line of text
Text {
color: Color,
/// Top left corner of the first character.
pos: Vec2,
text: String,
/// Start each character in the text, as offset from pos.
x_offsets: Vec<f32>,
// TODO: font info
},
}

246
emigui/src/widgets.rs Normal file
View File

@@ -0,0 +1,246 @@
use crate::{
layout::{make_id, GuiResponse, Id, Region},
math::{remap_clamp, vec2, Vec2},
types::GuiCmd,
};
// ----------------------------------------------------------------------------
/// Anything implementing Widget can be added to a Region with Region::add
pub trait Widget {
fn add_to(self, region: &mut Region) -> GuiResponse;
}
// ----------------------------------------------------------------------------
pub struct Label {
text: String,
}
impl Label {
pub fn new<S: Into<String>>(text: S) -> Self {
Label { text: text.into() }
}
}
pub fn label<S: Into<String>>(text: S) -> Label {
Label::new(text)
}
impl Widget for Label {
fn add_to(self, region: &mut Region) -> GuiResponse {
let (text, text_size) = region.font().layout_multiline(&self.text, region.width());
region.add_text(region.cursor(), text);
let (_, interact) = region.reserve_space(text_size, None);
region.response(interact)
}
}
// ----------------------------------------------------------------------------
pub struct Button {
text: String,
}
impl Button {
pub fn new<S: Into<String>>(text: S) -> Self {
Button { text: text.into() }
}
}
impl Widget for Button {
fn add_to(self, region: &mut Region) -> GuiResponse {
let id = region.make_child_id(&self.text);
let (text, text_size) = region.font().layout_multiline(&self.text, region.width());
let text_cursor = region.cursor() + region.options().button_padding;
let (rect, interact) =
region.reserve_space(text_size + 2.0 * region.options().button_padding, Some(id));
region.add_graphic(GuiCmd::Button { interact, rect });
region.add_text(text_cursor, text);
region.response(interact)
}
}
// ----------------------------------------------------------------------------
#[derive(Debug)]
pub struct Checkbox<'a> {
checked: &'a mut bool,
text: String,
}
impl<'a> Checkbox<'a> {
pub fn new<S: Into<String>>(checked: &'a mut bool, text: S) -> Self {
Checkbox {
checked,
text: text.into(),
}
}
}
impl<'a> Widget for Checkbox<'a> {
fn add_to(self, region: &mut Region) -> GuiResponse {
let id = region.make_child_id(&self.text);
let (text, text_size) = region.font().layout_multiline(&self.text, region.width());
let text_cursor = region.cursor()
+ region.options().button_padding
+ vec2(region.options().start_icon_width, 0.0);
let (rect, interact) = region.reserve_space(
region.options().button_padding
+ vec2(region.options().start_icon_width, 0.0)
+ text_size
+ region.options().button_padding,
Some(id),
);
if interact.clicked {
*self.checked = !*self.checked;
}
region.add_graphic(GuiCmd::Checkbox {
checked: *self.checked,
interact,
rect,
});
region.add_text(text_cursor, text);
region.response(interact)
}
}
// ----------------------------------------------------------------------------
#[derive(Debug)]
pub struct RadioButton {
checked: bool,
text: String,
}
impl RadioButton {
pub fn new<S: Into<String>>(checked: bool, text: S) -> Self {
RadioButton {
checked,
text: text.into(),
}
}
}
pub fn radio<S: Into<String>>(checked: bool, text: S) -> RadioButton {
RadioButton::new(checked, text)
}
impl Widget for RadioButton {
fn add_to(self, region: &mut Region) -> GuiResponse {
let id = region.make_child_id(&self.text);
let (text, text_size) = region.font().layout_multiline(&self.text, region.width());
let text_cursor = region.cursor()
+ region.options().button_padding
+ vec2(region.options().start_icon_width, 0.0);
let (rect, interact) = region.reserve_space(
region.options().button_padding
+ vec2(region.options().start_icon_width, 0.0)
+ text_size
+ region.options().button_padding,
Some(id),
);
region.add_graphic(GuiCmd::RadioButton {
checked: self.checked,
interact,
rect,
});
region.add_text(text_cursor, text);
region.response(interact)
}
}
// ----------------------------------------------------------------------------
#[derive(Debug)]
pub struct Slider<'a> {
value: &'a mut f32,
min: f32,
max: f32,
id: Option<Id>,
text: Option<String>,
text_on_top: Option<bool>,
}
impl<'a> Slider<'a> {
pub fn new(value: &'a mut f32, min: f32, max: f32) -> Self {
Slider {
value,
min,
max,
id: None,
text: None,
text_on_top: None,
}
}
pub fn id(mut self, id: Id) -> Self {
self.id = Some(id);
self
}
pub fn text<S: Into<String>>(mut self, text: S) -> Self {
self.text = Some(text.into());
self
}
}
impl<'a> Widget for Slider<'a> {
fn add_to(self, region: &mut Region) -> GuiResponse {
if let Some(text) = &self.text {
let text_on_top = self.text_on_top.unwrap_or_default();
let full_text = format!("{}: {:.3}", text, self.value);
let id = Some(self.id.unwrap_or(make_id(text)));
let mut naked = self;
naked.id = id;
naked.text = None;
if text_on_top {
let (text, text_size) = region.font().layout_multiline(&full_text, region.width());
region.add_text(region.cursor(), text);
region.reserve_space_inner(text_size);
naked.add_to(region)
} else {
region.columns(2, |columns| {
columns[1].add(label(full_text));
naked.add_to(&mut columns[0])
})
}
} else {
let value = self.value;
let min = self.min;
let max = self.max;
debug_assert!(min <= max);
let id = region.combined_id(self.id);
let (slider_rect, interact) = region.reserve_space(
Vec2 {
x: region.available_space.x,
y: region.data.font.line_spacing(),
},
id,
);
if interact.active {
*value = remap_clamp(
region.input().mouse_pos.x,
slider_rect.min().x,
slider_rect.max().x,
min,
max,
);
}
region.add_graphic(GuiCmd::Slider {
interact,
max,
min,
rect: slider_rect,
value: *value,
});
region.response(interact)
}
}
}
// ----------------------------------------------------------------------------