Refactor example code

This commit is contained in:
Emil Ernerfeldt 2020-04-12 12:07:51 +02:00
parent 4889296f7a
commit d999962602
13 changed files with 402 additions and 343 deletions

View file

@ -5,6 +5,10 @@ Maybe we can use build.rs to generate needed stuff, e.g. index.hmtl?
# Code # Code
* Read TTF from browser? * Read TTF from browser?
* requestAnimationFrame * requestAnimationFrame
* Rename region to something shorter
* `region: &Region` `region.add(...)` :/
* `gui: &Gui` `gui.add(...)` :)
* `ui: &Ui` `ui.add(...)` :)
# Additional bindings, e.g. Piston # Additional bindings, e.g. Piston

View file

@ -8,178 +8,178 @@
<!-- TODO: read https://www.html5rocks.com/en/mobile/mobifying/#toc-meta-viewport --> <!-- TODO: read https://www.html5rocks.com/en/mobile/mobifying/#toc-meta-viewport -->
<head> <head>
<title>Emigui A experiment in an Immediate Mode GUI written in Rust</title> <title>Emigui An experimental immediate mode GUI written in Rust</title>
<style> <style>
html { html {
/* Remove touch delay: */ /* Remove touch delay: */
touch-action: manipulation; touch-action: manipulation;
} }
body { body {
background: #000000; background: #000000;
} }
html, html,
body { body {
overflow: hidden; overflow: hidden;
} }
</style> </style>
</head> </head>
<body> <body>
<script> <script>
// The `--no-modules`-generated JS from `wasm-bindgen` attempts to use // The `--no-modules`-generated JS from `wasm-bindgen` attempts to use
// `WebAssembly.instantiateStreaming` to instantiate the wasm module, // `WebAssembly.instantiateStreaming` to instantiate the wasm module,
// but this doesn't work with `file://` urls. This example is frequently // but this doesn't work with `file://` urls. This example is frequently
// viewed by simply opening `index.html` in a browser (with a `file://` // viewed by simply opening `index.html` in a browser (with a `file://`
// url), so it would fail if we were to call this function! // url), so it would fail if we were to call this function!
// //
// Work around this for now by deleting the function to ensure that the // Work around this for now by deleting the function to ensure that the
// `no_modules.js` script doesn't have access to it. You won't need this // `no_modules.js` script doesn't have access to it. You won't need this
// hack when deploying over HTTP. // hack when deploying over HTTP.
delete WebAssembly.instantiateStreaming; delete WebAssembly.instantiateStreaming;
</script> </script>
<!-- this is the JS generated by the `wasm-bindgen` CLI tool --> <!-- this is the JS generated by the `wasm-bindgen` CLI tool -->
<script src="example_wasm.js"></script> <script src="example_wasm.js"></script>
<script> <script>
// we'll defer our execution until the wasm is ready to go // we'll defer our execution until the wasm is ready to go
// here we tell bindgen the path to the wasm file so it can start // here we tell bindgen the path to the wasm file so it can start
// initialization and return to us a promise when it's done // initialization and return to us a promise when it's done
wasm_bindgen("./example_wasm_bg.wasm") wasm_bindgen("./example_wasm_bg.wasm")
.then(on_wasm_loaded)["catch"](console.error); .then(on_wasm_loaded)["catch"](console.error);
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
var g_wasm_app = null; var g_wasm_app = null;
function paint_gui(canvas, input) { function paint_gui(canvas, input) {
if (g_wasm_app === null) { if (g_wasm_app === null) {
g_wasm_app = wasm_bindgen.new_webgl_gui("canvas", pixels_per_point()); g_wasm_app = wasm_bindgen.new_webgl_gui("canvas", pixels_per_point());
}
wasm_bindgen.run_gui(g_wasm_app, JSON.stringify(input));
}
// ----------------------------------------------------------------------------
var g_mouse_pos = null;
var g_mouse_down = false;
var g_is_touch = false;
function pixels_per_point() {
// return 1.0;
return window.devicePixelRatio || 1.0;
}
function auto_resize_canvas(canvas) {
canvas.style.width = window.innerWidth + "px";
canvas.style.height = window.innerHeight + "px";
canvas.width = window.innerWidth * pixels_per_point();
canvas.height = window.innerHeight * pixels_per_point();
}
function get_input(canvas) {
return {
mouse_down: g_mouse_down,
mouse_pos: g_mouse_pos,
screen_size: { x: window.innerWidth, y: window.innerHeight },
pixels_per_point: pixels_per_point(),
};
}
function mouse_pos_from_event(canvas, event) {
var rect = canvas.getBoundingClientRect();
return {
x: event.clientX - rect.left,
y: event.clientY - rect.top
};
}
const ANIMATION_FRAME = true;
function paint() {
var canvas = document.getElementById("canvas");
auto_resize_canvas(canvas);
paint_gui(canvas, get_input(canvas));
}
function paint_and_schedule() {
paint();
if (ANIMATION_FRAME) {
window.requestAnimationFrame(paint_and_schedule);
}
}
function on_wasm_loaded() {
var canvas = document.getElementById("canvas");
var invalidate = function() {
if (!ANIMATION_FRAME) {
paint();
} }
}; wasm_bindgen.run_gui(g_wasm_app, JSON.stringify(input));
}
// ----------------------------------------------------------------------------
var g_mouse_pos = null;
var g_mouse_down = false;
var g_is_touch = false;
canvas.addEventListener("mousedown", function(event) { function pixels_per_point() {
if (g_is_touch) { return; } // return 1.0;
g_mouse_pos = mouse_pos_from_event(canvas, event); return window.devicePixelRatio || 1.0;
g_mouse_down = true;
invalidate();
event.stopPropagation();
event.preventDefault();
});
canvas.addEventListener("mousemove", function(event) {
if (g_is_touch) { return; }
g_mouse_pos = mouse_pos_from_event(canvas, event);
invalidate();
event.stopPropagation();
event.preventDefault();
});
canvas.addEventListener("mouseup", function(event) {
if (g_is_touch) { return; }
g_mouse_pos = mouse_pos_from_event(canvas, event);
g_mouse_down = false;
invalidate();
event.stopPropagation();
event.preventDefault();
});
canvas.addEventListener("mouseleave", function(event) {
if (g_is_touch) { return; }
g_mouse_pos = null;
invalidate();
event.stopPropagation();
event.preventDefault();
});
canvas.addEventListener("touchstart", function(event) {
g_is_touch = true;
g_mouse_pos = { x: event.touches[0].pageX, y: event.touches[0].pageY };
g_mouse_down = true;
invalidate();
event.stopPropagation();
event.preventDefault();
});
canvas.addEventListener("touchmove", function(event) {
g_is_touch = true;
g_mouse_pos = { x: event.touches[0].pageX, y: event.touches[0].pageY };
invalidate();
event.stopPropagation();
event.preventDefault();
});
canvas.addEventListener("touchend", function(event) {
g_is_touch = true;
g_mouse_down = false; // First release mouse to click...
update_gui()();
g_mouse_pos = null; // ...remove hover effect
update_gui()();
event.stopPropagation();
event.preventDefault();
});
if (!ANIMATION_FRAME) {
window.addEventListener("load", invalidate);
window.addEventListener("pagehide", invalidate);
window.addEventListener("pageshow", invalidate);
window.addEventListener("resize", invalidate);
} }
paint_and_schedule(); function auto_resize_canvas(canvas) {
} canvas.style.width = window.innerWidth + "px";
canvas.style.height = window.innerHeight + "px";
canvas.width = window.innerWidth * pixels_per_point();
canvas.height = window.innerHeight * pixels_per_point();
}
function get_input(canvas) {
return {
mouse_down: g_mouse_down,
mouse_pos: g_mouse_pos,
screen_size: { x: window.innerWidth, y: window.innerHeight },
pixels_per_point: pixels_per_point(),
};
}
function mouse_pos_from_event(canvas, event) {
var rect = canvas.getBoundingClientRect();
return {
x: event.clientX - rect.left,
y: event.clientY - rect.top
};
}
const ANIMATION_FRAME = true;
function paint() {
var canvas = document.getElementById("canvas");
auto_resize_canvas(canvas);
paint_gui(canvas, get_input(canvas));
}
function paint_and_schedule() {
paint();
if (ANIMATION_FRAME) {
window.requestAnimationFrame(paint_and_schedule);
}
}
function on_wasm_loaded() {
var canvas = document.getElementById("canvas");
var invalidate = function () {
if (!ANIMATION_FRAME) {
paint();
}
};
canvas.addEventListener("mousedown", function (event) {
if (g_is_touch) { return; }
g_mouse_pos = mouse_pos_from_event(canvas, event);
g_mouse_down = true;
invalidate();
event.stopPropagation();
event.preventDefault();
});
canvas.addEventListener("mousemove", function (event) {
if (g_is_touch) { return; }
g_mouse_pos = mouse_pos_from_event(canvas, event);
invalidate();
event.stopPropagation();
event.preventDefault();
});
canvas.addEventListener("mouseup", function (event) {
if (g_is_touch) { return; }
g_mouse_pos = mouse_pos_from_event(canvas, event);
g_mouse_down = false;
invalidate();
event.stopPropagation();
event.preventDefault();
});
canvas.addEventListener("mouseleave", function (event) {
if (g_is_touch) { return; }
g_mouse_pos = null;
invalidate();
event.stopPropagation();
event.preventDefault();
});
canvas.addEventListener("touchstart", function (event) {
g_is_touch = true;
g_mouse_pos = { x: event.touches[0].pageX, y: event.touches[0].pageY };
g_mouse_down = true;
invalidate();
event.stopPropagation();
event.preventDefault();
});
canvas.addEventListener("touchmove", function (event) {
g_is_touch = true;
g_mouse_pos = { x: event.touches[0].pageX, y: event.touches[0].pageY };
invalidate();
event.stopPropagation();
event.preventDefault();
});
canvas.addEventListener("touchend", function (event) {
g_is_touch = true;
g_mouse_down = false; // First release mouse to click...
update_gui()();
g_mouse_pos = null; // ...remove hover effect
update_gui()();
event.stopPropagation();
event.preventDefault();
});
if (!ANIMATION_FRAME) {
window.addEventListener("load", invalidate);
window.addEventListener("pagehide", invalidate);
window.addEventListener("pageshow", invalidate);
window.addEventListener("resize", invalidate);
}
paint_and_schedule();
}
</script> </script>
<!-- TODO: make this cover the entire screen, with resize and all --> <!-- TODO: make this cover the entire screen, with resize and all -->
<canvas id="canvas" width="1024" height="1024"></canvas> <canvas id="canvas" width="1024" height="1024"></canvas>

View file

@ -1,12 +1,9 @@
use std::sync::Arc; use std::sync::Arc;
use crate::{ use crate::{
color::WHITE,
label, layout, label, layout,
layout::{show_popup, Region}, layout::Region,
math::{clamp, remap_clamp, vec2}, mesher::Mesher,
mesher::{Mesher, Vertex},
style::Style,
types::{GuiInput, PaintCmd}, types::{GuiInput, PaintCmd},
widgets::*, widgets::*,
FontDefinitions, Fonts, Mesh, RawInput, Texture, FontDefinitions, Fonts, Mesh, RawInput, Texture,
@ -18,98 +15,6 @@ struct Stats {
num_triangles: usize, num_triangles: usize,
} }
fn show_style(style: &mut Style, gui: &mut Region) {
if gui.add(Button::new("Reset style")).clicked {
*style = Default::default();
}
gui.add(Slider::f32(&mut style.item_spacing.x, 0.0, 10.0).text("item_spacing.x"));
gui.add(Slider::f32(&mut style.item_spacing.y, 0.0, 10.0).text("item_spacing.y"));
gui.add(Slider::f32(&mut style.window_padding.x, 0.0, 10.0).text("window_padding.x"));
gui.add(Slider::f32(&mut style.window_padding.y, 0.0, 10.0).text("window_padding.y"));
gui.add(Slider::f32(&mut style.indent, 0.0, 100.0).text("indent"));
gui.add(Slider::f32(&mut style.button_padding.x, 0.0, 20.0).text("button_padding.x"));
gui.add(Slider::f32(&mut style.button_padding.y, 0.0, 20.0).text("button_padding.y"));
gui.add(Slider::f32(&mut style.clickable_diameter, 0.0, 60.0).text("clickable_diameter"));
gui.add(Slider::f32(&mut style.start_icon_width, 0.0, 60.0).text("start_icon_width"));
gui.add(Slider::f32(&mut style.line_width, 0.0, 10.0).text("line_width"));
}
fn show_font_definitions(font_definitions: &mut FontDefinitions, gui: &mut Region) {
for (text_style, (_family, size)) in font_definitions {
// TODO: radiobutton for family
gui.add(Slider::f32(size, 4.0, 40.0).text(format!("{:?}", text_style)));
}
}
fn show_font_texture(texture: &Texture, gui: &mut Region) {
gui.add(label!(
"Font texture size: {} x {} (hover to zoom)",
texture.width,
texture.height
));
let mut size = vec2(texture.width as f32, texture.height as f32);
if size.x > gui.width() {
size *= gui.width() / size.x;
}
let interact = gui.reserve_space(size, None);
let rect = interact.rect;
let top_left = Vertex {
pos: rect.min(),
uv: (0, 0),
color: WHITE,
};
let bottom_right = Vertex {
pos: rect.max(),
uv: (texture.width as u16 - 1, texture.height as u16 - 1),
color: WHITE,
};
let mut mesh = Mesh::default();
mesh.add_rect(top_left, bottom_right);
gui.add_paint_cmd(PaintCmd::Mesh(mesh));
if let Some(mouse_pos) = gui.input().mouse_pos {
if interact.hovered {
show_popup(gui.data(), mouse_pos, |gui| {
let zoom_rect = gui.reserve_space(vec2(128.0, 128.0), None).rect;
let u = remap_clamp(
mouse_pos.x,
rect.min().x,
rect.max().x,
0.0,
texture.width as f32 - 1.0,
)
.round();
let v = remap_clamp(
mouse_pos.y,
rect.min().y,
rect.max().y,
0.0,
texture.height as f32 - 1.0,
)
.round();
let texel_radius = 32.0;
let u = clamp(u, texel_radius, texture.width as f32 - 1.0 - texel_radius);
let v = clamp(v, texel_radius, texture.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 mesh = Mesh::default();
mesh.add_rect(top_left, bottom_right);
gui.add_paint_cmd(PaintCmd::Mesh(mesh));
});
}
}
}
/// Encapsulates input, layout and painting for ease of use. /// Encapsulates input, layout and painting for ease of use.
pub struct Emigui { pub struct Emigui {
pub last_input: RawInput, pub last_input: RawInput,
@ -164,18 +69,16 @@ impl Emigui {
mesh mesh
} }
pub fn example(&mut self, region: &mut Region) { pub fn ui(&mut self, region: &mut Region) {
region.foldable("Style", |gui| { region.foldable("Style", |region| {
let mut style = self.data.style(); self.data.style_ui(region);
show_style(&mut style, gui);
self.data.set_style(style);
}); });
region.foldable("Fonts", |gui| { region.foldable("Fonts", |region| {
let old_font_definitions = self.data.fonts.definitions(); let old_font_definitions = self.data.fonts.definitions();
let mut new_font_definitions = old_font_definitions.clone(); let mut new_font_definitions = old_font_definitions.clone();
show_font_definitions(&mut new_font_definitions, gui); font_definitions_ui(&mut new_font_definitions, region);
show_font_texture(self.texture(), gui); self.data.fonts.texture().ui(region);
if *old_font_definitions != new_font_definitions { if *old_font_definitions != new_font_definitions {
let mut new_data = (*self.data).clone(); let mut new_data = (*self.data).clone();
let fonts = let fonts =
@ -185,25 +88,39 @@ impl Emigui {
} }
}); });
region.foldable("Stats", |gui| { region.foldable("Stats", |region| {
gui.add(label!( region.add(label!(
"Screen size: {} x {} points, pixels_per_point: {}", "Screen size: {} x {} points, pixels_per_point: {}",
gui.input().screen_size.x, region.input().screen_size.x,
gui.input().screen_size.y, region.input().screen_size.y,
gui.input().pixels_per_point, region.input().pixels_per_point,
)); ));
if let Some(mouse_pos) = gui.input().mouse_pos { if let Some(mouse_pos) = region.input().mouse_pos {
gui.add(label!("mouse_pos: {} x {}", mouse_pos.x, mouse_pos.y,)); region.add(label!("mouse_pos: {} x {}", mouse_pos.x, mouse_pos.y,));
} else { } else {
gui.add(label!("mouse_pos: None")); region.add(label!("mouse_pos: None"));
} }
gui.add(label!( region.add(label!(
"gui cursor: {} x {}", "region cursor: {} x {}",
gui.cursor().x, region.cursor().x,
gui.cursor().y, region.cursor().y,
)); ));
gui.add(label!("num_vertices: {}", self.stats.num_vertices)); region.add(label!("num_vertices: {}", self.stats.num_vertices));
gui.add(label!("num_triangles: {}", self.stats.num_triangles)); region.add(label!("num_triangles: {}", self.stats.num_triangles));
}); });
} }
} }
fn font_definitions_ui(font_definitions: &mut FontDefinitions, region: &mut Region) {
for (text_style, (_family, size)) in font_definitions.iter_mut() {
// TODO: radiobutton for family
region.add(
Slider::f32(size, 4.0, 40.0)
.precision(0)
.text(format!("{:?}", text_style)),
);
}
if region.add(Button::new("Reset fonts")).clicked {
*font_definitions = crate::fonts::default_font_definitions();
}
}

View file

@ -1,14 +1,7 @@
use emigui::{color::*, label, math::*, widgets::*, Align, Outline, PaintCmd, Region, TextStyle}; use crate::{color::*, label, math::*, widgets::*, Align, Outline, PaintCmd, Region};
pub fn show_value_gui(value: &mut usize, gui: &mut Region) { /// Showcase some region code
gui.add(Slider::usize(value, 1, 1000)); pub struct ExampleApp {
if gui.add(Button::new("Double it")).clicked {
*value *= 2;
}
gui.add(label!("Value: {}", value));
}
pub struct App {
checked: bool, checked: bool,
count: usize, count: usize,
radio: usize, radio: usize,
@ -23,9 +16,9 @@ pub struct App {
slider_value: usize, slider_value: usize,
} }
impl Default for App { impl Default for ExampleApp {
fn default() -> App { fn default() -> ExampleApp {
App { ExampleApp {
checked: true, checked: true,
radio: 0, radio: 0,
count: 0, count: 0,
@ -41,53 +34,53 @@ impl Default for App {
} }
} }
impl App { impl ExampleApp {
pub fn show_gui(&mut self, gui: &mut Region) { pub fn ui(&mut self, region: &mut Region) {
gui.add(label!("Emigui!").text_style(TextStyle::Heading)); region.foldable("About Emigui", |region| {
gui.add(label!("Emigui is an Immediate mode GUI written in Rust, compiled to WebAssembly, rendered with WebGL.")); region.add(label!("Emigui is an immediate mode GUI written in Rust, compiled to WebAssembly, rendered with WebGL."));
gui.add(Separator::new()); });
gui.foldable("Widget examples", |gui| { region.foldable("Widget examples", |region| {
gui.horizontal(Align::Min, |gui| { region.horizontal(Align::Min, |region| {
gui.add(label!("Text can have").text_color(srgba(110, 255, 110, 255))); region.add(label!("Text can have").text_color(srgba(110, 255, 110, 255)));
gui.add(label!("color").text_color(srgba(128, 140, 255, 255))); region.add(label!("color").text_color(srgba(128, 140, 255, 255)));
gui.add(label!("and tooltips (hover me)")).tooltip_text( region.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.", "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.",
); );
}); });
gui.add(Checkbox::new(&mut self.checked, "checkbox")); region.add(Checkbox::new(&mut self.checked, "checkbox"));
gui.horizontal(Align::Min, |gui| { region.horizontal(Align::Min, |region| {
if gui.add(radio(self.radio == 0, "First")).clicked { if region.add(radio(self.radio == 0, "First")).clicked {
self.radio = 0; self.radio = 0;
} }
if gui.add(radio(self.radio == 1, "Second")).clicked { if region.add(radio(self.radio == 1, "Second")).clicked {
self.radio = 1; self.radio = 1;
} }
if gui.add(radio(self.radio == 2, "Final")).clicked { if region.add(radio(self.radio == 2, "Final")).clicked {
self.radio = 2; self.radio = 2;
} }
}); });
gui.horizontal(Align::Min, |gui| { region.horizontal(Align::Min, |region| {
if gui if region
.add(Button::new("Click me")) .add(Button::new("Click me"))
.tooltip_text("This will just increase a counter.") .tooltip_text("This will just increase a counter.")
.clicked .clicked
{ {
self.count += 1; self.count += 1;
} }
gui.add(label!( region.add(label!(
"The button have been clicked {} times", "The button have been clicked {} times",
self.count self.count
)); ));
}); });
}); });
gui.foldable("Layouts", |gui| { region.foldable("Layouts", |region| {
gui.add(Slider::usize(&mut self.num_columns, 1, 10).text("Columns")); region.add(Slider::usize(&mut self.num_columns, 1, 10).text("Columns"));
gui.columns(self.num_columns, |cols| { region.columns(self.num_columns, |cols| {
for (i, col) in cols.iter_mut().enumerate() { for (i, col) in cols.iter_mut().enumerate() {
col.add(label!("Column {} out of {}", i + 1, self.num_columns)); col.add(label!("Column {} out of {}", i + 1, self.num_columns));
if i + 1 == self.num_columns { if i + 1 == self.num_columns {
@ -99,14 +92,14 @@ impl App {
}); });
}); });
gui.foldable("Test box rendering", |gui| { region.foldable("Test box rendering", |region| {
gui.add(Slider::f32(&mut self.size.x, 0.0, 500.0).text("width")); region.add(Slider::f32(&mut self.size.x, 0.0, 500.0).text("width"));
gui.add(Slider::f32(&mut self.size.y, 0.0, 500.0).text("height")); region.add(Slider::f32(&mut self.size.y, 0.0, 500.0).text("height"));
gui.add(Slider::f32(&mut self.corner_radius, 0.0, 50.0).text("corner_radius")); region.add(Slider::f32(&mut self.corner_radius, 0.0, 50.0).text("corner_radius"));
gui.add(Slider::f32(&mut self.stroke_width, 0.0, 10.0).text("stroke_width")); region.add(Slider::f32(&mut self.stroke_width, 0.0, 10.0).text("stroke_width"));
gui.add(Slider::usize(&mut self.num_boxes, 0, 5).text("num_boxes")); region.add(Slider::usize(&mut self.num_boxes, 0, 5).text("num_boxes"));
let pos = gui let pos = region
.reserve_space( .reserve_space(
vec2(self.size.x * (self.num_boxes as f32), self.size.y), vec2(self.size.x * (self.num_boxes as f32), self.size.y),
None, None,
@ -129,11 +122,19 @@ impl App {
}), }),
}); });
} }
gui.add_paint_cmds(cmds); region.add_paint_cmds(cmds);
}); });
gui.foldable("Slider example", |gui| { region.foldable("Slider example", |region| {
show_value_gui(&mut self.slider_value, gui); value_ui(&mut self.slider_value, region);
}); });
} }
} }
pub fn value_ui(value: &mut usize, region: &mut Region) {
region.add(Slider::usize(value, 1, 1000));
if region.add(Button::new("Double it")).clicked {
*value *= 2;
}
region.add(label!("Value: {}", value));
}

View file

@ -26,6 +26,15 @@ pub enum FontFamily {
pub type FontDefinitions = BTreeMap<TextStyle, (FontFamily, f32)>; pub type FontDefinitions = BTreeMap<TextStyle, (FontFamily, f32)>;
pub fn default_font_definitions() -> FontDefinitions {
let mut definitions = FontDefinitions::new();
definitions.insert(TextStyle::Body, (FontFamily::VariableWidth, 16.0));
definitions.insert(TextStyle::Button, (FontFamily::VariableWidth, 18.0));
definitions.insert(TextStyle::Heading, (FontFamily::VariableWidth, 28.0));
definitions.insert(TextStyle::Monospace, (FontFamily::Monospace, 13.0));
definitions
}
pub struct Fonts { pub struct Fonts {
pixels_per_point: f32, pixels_per_point: f32,
definitions: FontDefinitions, definitions: FontDefinitions,
@ -35,12 +44,7 @@ pub struct Fonts {
impl Fonts { impl Fonts {
pub fn new(pixels_per_point: f32) -> Fonts { pub fn new(pixels_per_point: f32) -> Fonts {
let mut definitions = FontDefinitions::new(); Fonts::from_definitions(default_font_definitions(), pixels_per_point)
definitions.insert(TextStyle::Body, (FontFamily::VariableWidth, 16.0));
definitions.insert(TextStyle::Button, (FontFamily::VariableWidth, 18.0));
definitions.insert(TextStyle::Heading, (FontFamily::VariableWidth, 28.0));
definitions.insert(TextStyle::Monospace, (FontFamily::Monospace, 13.0));
Fonts::from_definitions(definitions, pixels_per_point)
} }
pub fn from_definitions(definitions: FontDefinitions, pixels_per_point: f32) -> Fonts { pub fn from_definitions(definitions: FontDefinitions, pixels_per_point: f32) -> Fonts {

View file

@ -192,6 +192,14 @@ impl Data {
} }
} }
impl Data {
pub fn style_ui(&self, region: &mut Region) {
let mut style = self.style();
style.ui(region);
self.set_style(style);
}
}
/// Show a pop-over window /// Show a pop-over window
pub fn show_popup<F>(data: &Arc<Data>, window_pos: Vec2, add_contents: F) pub fn show_popup<F>(data: &Arc<Data>, window_pos: Vec2, add_contents: F)
where where

View file

@ -8,6 +8,7 @@ extern crate serde_derive;
pub mod color; pub mod color;
mod emigui; mod emigui;
pub mod example_app;
mod font; mod font;
mod fonts; mod fonts;
mod layout; mod layout;

View file

@ -86,3 +86,22 @@ impl Style {
(small_icon_rect, big_icon_rect) (small_icon_rect, big_icon_rect)
} }
} }
impl Style {
pub fn ui(&mut self, region: &mut crate::Region) {
use crate::widgets::{Button, Slider};
if region.add(Button::new("Reset style")).clicked {
*self = Default::default();
}
region.add(Slider::f32(&mut self.item_spacing.x, 0.0, 10.0).text("item_spacing.x"));
region.add(Slider::f32(&mut self.item_spacing.y, 0.0, 10.0).text("item_spacing.y"));
region.add(Slider::f32(&mut self.window_padding.x, 0.0, 10.0).text("window_padding.x"));
region.add(Slider::f32(&mut self.window_padding.y, 0.0, 10.0).text("window_padding.y"));
region.add(Slider::f32(&mut self.indent, 0.0, 100.0).text("indent"));
region.add(Slider::f32(&mut self.button_padding.x, 0.0, 20.0).text("button_padding.x"));
region.add(Slider::f32(&mut self.button_padding.y, 0.0, 20.0).text("button_padding.y"));
region.add(Slider::f32(&mut self.clickable_diameter, 0.0, 60.0).text("clickable_diameter"));
region.add(Slider::f32(&mut self.start_icon_width, 0.0, 60.0).text("start_icon_width"));
region.add(Slider::f32(&mut self.line_width, 0.0, 10.0).text("line_width"));
}
}

View file

@ -89,3 +89,79 @@ impl TextureAtlas {
(pos.0 as usize, pos.1 as usize) (pos.0 as usize, pos.1 as usize)
} }
} }
impl Texture {
pub fn ui(&self, region: &mut crate::Region) {
use crate::{
color::WHITE, label, layout::show_popup, math::*, widgets::Label, Mesh, PaintCmd,
Vertex,
};
region.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 > region.width() {
size *= region.width() / size.x;
}
let interact = region.reserve_space(size, None);
let rect = interact.rect;
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 mesh = Mesh::default();
mesh.add_rect(top_left, bottom_right);
region.add_paint_cmd(PaintCmd::Mesh(mesh));
if let Some(mouse_pos) = region.input().mouse_pos {
if interact.hovered {
show_popup(region.data(), mouse_pos, |region| {
let zoom_rect = region.reserve_space(vec2(128.0, 128.0), None).rect;
let u = remap_clamp(
mouse_pos.x,
rect.min().x,
rect.max().x,
0.0,
self.width as f32 - 1.0,
)
.round();
let v = remap_clamp(
mouse_pos.y,
rect.min().y,
rect.max().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 mesh = Mesh::default();
mesh.add_rect(top_left, bottom_right);
region.add_paint_cmd(PaintCmd::Mesh(mesh));
});
}
}
}
}

View file

@ -19,7 +19,7 @@ pub trait Widget {
pub struct Label { pub struct Label {
text: String, text: String,
text_style: TextStyle, text_style: TextStyle, // TODO: Option<TextStyle>, where None means "use the default for the region"
text_color: Option<Color>, text_color: Option<Color>,
} }
@ -331,6 +331,17 @@ impl<'a> Slider<'a> {
self.precision = precision; self.precision = precision;
self 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> { impl<'a> Widget for Slider<'a> {
@ -341,12 +352,8 @@ impl<'a> Widget for Slider<'a> {
if let Some(text) = &self.text { if let Some(text) = &self.text {
let text_on_top = self.text_on_top.unwrap_or_default(); let text_on_top = self.text_on_top.unwrap_or_default();
let text_color = self.text_color; let text_color = self.text_color;
let full_text = format!( let value = (self.get_set_value)(None);
"{}: {:.*}", let full_text = format!("{}: {:.*}", text, self.precision, value);
text,
self.precision,
(self.get_set_value)(None)
);
let id = Some(self.id.unwrap_or_else(|| make_id(text))); let id = Some(self.id.unwrap_or_else(|| make_id(text)));
let mut naked = self; let mut naked = self;
naked.id = id; naked.id = id;
@ -386,19 +393,19 @@ impl<'a> Widget for Slider<'a> {
if let Some(mouse_pos) = region.input().mouse_pos { if let Some(mouse_pos) = region.input().mouse_pos {
if interact.active { if interact.active {
(self.get_set_value)(Some(remap_clamp( self.set_value_f32(remap_clamp(
mouse_pos.x, mouse_pos.x,
interact.rect.min().x, interact.rect.min().x,
interact.rect.max().x, interact.rect.max().x,
min, min,
max, max,
))); ));
} }
} }
// Paint it: // Paint it:
{ {
let value = (self.get_set_value)(None); let value = self.get_value_f32();
let rect = interact.rect; let rect = interact.rect;
let thickness = rect.size().y; let thickness = rect.size().y;

View file

@ -4,6 +4,7 @@ use std::time::{Duration, Instant};
use { use {
emigui::{ emigui::{
example_app::ExampleApp,
label, label,
math::vec2, math::vec2,
widgets::{Button, Label}, widgets::{Button, Label},
@ -37,6 +38,8 @@ fn main() {
let mut frame_start = Instant::now(); let mut frame_start = Instant::now();
let mut example_app = ExampleApp::default();
while !quit { while !quit {
{ {
// Keep smooth frame rate. TODO: proper vsync // Keep smooth frame rate. TODO: proper vsync
@ -85,7 +88,8 @@ fn main() {
if region.add(Button::new("Quit")).clicked { if region.add(Button::new("Quit")).clicked {
quit = true; quit = true;
} }
emigui.example(&mut region); example_app.ui(&mut region);
emigui.ui(&mut region);
let mesh = emigui.paint(); let mesh = emigui.paint();
painter.paint(&display, mesh, emigui.texture()); painter.paint(&display, mesh, emigui.texture());
} }

View file

@ -7,17 +7,21 @@ extern crate emigui;
extern crate emigui_wasm; extern crate emigui_wasm;
use { use {
emigui::{color::srgba, label, widgets::Label, Emigui, RawInput}, emigui::{
color::srgba,
example_app::ExampleApp,
label,
widgets::{Label, Separator},
Align, Emigui, RawInput, TextStyle,
},
emigui_wasm::now_sec, emigui_wasm::now_sec,
}; };
use wasm_bindgen::prelude::*; use wasm_bindgen::prelude::*;
mod app;
#[wasm_bindgen] #[wasm_bindgen]
pub struct State { pub struct State {
app: app::App, example_app: ExampleApp,
emigui: Emigui, emigui: Emigui,
webgl_painter: emigui_wasm::webgl::Painter, webgl_painter: emigui_wasm::webgl::Painter,
everything_ms: f64, everything_ms: f64,
@ -26,7 +30,7 @@ pub struct State {
impl State { impl State {
fn new(canvas_id: &str, pixels_per_point: f32) -> Result<State, JsValue> { fn new(canvas_id: &str, pixels_per_point: f32) -> Result<State, JsValue> {
Ok(State { Ok(State {
app: Default::default(), example_app: Default::default(),
emigui: Emigui::new(pixels_per_point), emigui: Emigui::new(pixels_per_point),
webgl_painter: emigui_wasm::webgl::Painter::new(canvas_id)?, webgl_painter: emigui_wasm::webgl::Painter::new(canvas_id)?,
everything_ms: 0.0, everything_ms: 0.0,
@ -40,15 +44,23 @@ impl State {
let mut region = self.emigui.whole_screen_region(); let mut region = self.emigui.whole_screen_region();
let mut region = region.centered_column(region.width().min(480.0)); let mut region = region.centered_column(region.width().min(480.0));
self.app.show_gui(&mut region); region.add(label!("Emigui!").text_style(TextStyle::Heading));
self.emigui.example(&mut region); region.add(label!("Emigui is an immediate mode GUI written in Rust, compiled to WebAssembly, rendered with WebGL."));
region.add(label!(
"Everything you see is rendered as textured triangles. There is no DOM. There are not HTML elements."
));
region.add(Separator::new());
self.example_app.ui(&mut region);
self.emigui.ui(&mut region);
region.set_align(Align::Min);
region.add(label!("WebGl painter info:")); region.add(label!("WebGl painter info:"));
region.indent(|region| { region.indent(|region| {
region.add(label!(self.webgl_painter.debug_info())); region.add(label!(self.webgl_painter.debug_info()));
}); });
region.add(
region.add(label!("Everything: {:.1} ms", self.everything_ms)); label!("Everything: {:.1} ms", self.everything_ms).text_style(TextStyle::Monospace),
);
let bg_color = srgba(16, 16, 16, 255); let bg_color = srgba(16, 16, 16, 255);
let mesh = self.emigui.paint(); let mesh = self.emigui.paint();

6
start_server.sh Executable file
View file

@ -0,0 +1,6 @@
#!/bin/bash
set -eu
cd docs
echo "open localhost:8000"
python3 -m http.server 8000 --bind 127.0.0.1