Browse Source

Add support for hyperlinks

pull/2/head
Emil Ernerfeldt 5 years ago
parent
commit
b39555bb23
  1. 17
      Cargo.lock
  2. 16
      docs/example_wasm.js
  3. BIN
      docs/example_wasm_bg.wasm
  4. 21
      docs/index.html
  5. 5
      emigui/README.md
  6. 1
      emigui/src/color.rs
  7. 31
      emigui/src/context.rs
  8. 22
      emigui/src/emigui.rs
  9. 5
      emigui/src/example_app.rs
  10. 2
      emigui/src/font.rs
  11. 2
      emigui/src/lib.rs
  12. 14
      emigui/src/region.rs
  13. 22
      emigui/src/types.rs
  14. 58
      emigui/src/widgets.rs
  15. 10
      emigui/src/window.rs
  16. 1
      example_glium/Cargo.toml
  17. 18
      example_glium/src/main.rs
  18. 21
      example_wasm/src/lib.rs

17
Cargo.lock

@ -187,6 +187,7 @@ dependencies = [
"emigui 0.1.0",
"emigui_glium 0.1.0",
"glium 0.24.0 (registry+https://github.com/rust-lang/crates.io-index)",
"webbrowser 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
@ -827,6 +828,20 @@ dependencies = [
"wasm-bindgen 0.2.60 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "webbrowser"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"widestring 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "widestring"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "winapi"
version = "0.3.8"
@ -994,6 +1009,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
"checksum wayland-scanner 0.21.13 (registry+https://github.com/rust-lang/crates.io-index)" = "bf3828c568714507315ee425a9529edc4a4aa9901409e373e9e0027e7622b79e"
"checksum wayland-sys 0.21.13 (registry+https://github.com/rust-lang/crates.io-index)" = "520ab0fd578017a0ee2206623ba9ef4afe5e8f23ca7b42f6acfba2f4e66b1628"
"checksum web-sys 0.3.37 (registry+https://github.com/rust-lang/crates.io-index)" = "2d6f51648d8c56c366144378a33290049eafdd784071077f6fe37dae64c1c4cb"
"checksum webbrowser 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "97d468a911faaaeb783693b004e1c62e0063e646b0afae5c146cd144e566e66d"
"checksum widestring 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "effc0e4ff8085673ea7b9b2e3c73f6bd4d118810c9009ed8f1e16bd96c331db6"
"checksum winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)" = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6"
"checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
"checksum winapi-util 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "fa515c5163a99cc82bab70fd3bfdd36d827be85de63737b40fcef2ce084a436e"

16
docs/example_wasm.js

@ -196,12 +196,20 @@ function _assertClass(instance, klass) {
/**
* @param {State} state
* @param {string} raw_input_json
* @returns {string}
*/
__exports.run_gui = function(state, raw_input_json) {
_assertClass(state, State);
var ptr0 = passStringToWasm0(raw_input_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
var len0 = WASM_VECTOR_LEN;
wasm.run_gui(state.ptr, ptr0, len0);
try {
_assertClass(state, State);
var ptr0 = passStringToWasm0(raw_input_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
var len0 = WASM_VECTOR_LEN;
wasm.run_gui(8, state.ptr, ptr0, len0);
var r0 = getInt32Memory0()[8 / 4 + 0];
var r1 = getInt32Memory0()[8 / 4 + 1];
return getStringFromWasm0(r0, r1);
} finally {
wasm.__wbindgen_free(r0, r1);
}
};
function isLikeNone(x) {

BIN
docs/example_wasm_bg.wasm

Binary file not shown.

21
docs/index.html

@ -56,8 +56,27 @@
if (g_wasm_app === null) {
g_wasm_app = wasm_bindgen.new_webgl_gui("canvas", pixels_per_point());
}
wasm_bindgen.run_gui(g_wasm_app, JSON.stringify(input));
let output = JSON.parse(wasm_bindgen.run_gui(g_wasm_app, JSON.stringify(input)));
// console.log(`output: ${JSON.stringify(output)}`);
document.body.style.cursor = from_emigui_cursor(output.cursor_icon);
if (output.open_url) {
window.open(output.open_url, "_self");
}
}
function from_emigui_cursor(cursor) {
if (cursor == "no_drop") { return "no-drop"; }
else if (cursor == "not_allowed") { return "not-allowed"; }
else if (cursor == "resize_nw_se") { return "nwse-resize"; }
else if (cursor == "pointing_hand") { return "pointer"; }
// TODO: more
else {
// default, help, pointer, progress, wait, cell, crosshair, text, alias, copy, move, grab, grabbing,
return cursor;
}
}
// ----------------------------------------------------------------------------
var g_mouse_pos = null;
var g_mouse_down = false;

5
emigui/README.md

@ -19,6 +19,7 @@ This is the core library crate Emigui. It is fully platform independent without
* [x] Scroll-wheel input
* [x] Drag background to scroll
* [ ] Kinetic scrolling
* [x] Add support for clicking links
* [ ] Menu bar (File, Edit, etc)
* [ ] One-line TextField
* [ ] Clipboard copy/paste
@ -28,8 +29,7 @@ This is the core library crate Emigui. It is fully platform independent without
### Web version:
* [x] Scroll input
* [ ] Add support for clicking links
* [ ] Change to resize cursor on hover
* [x] Change to resize cursor on hover
### Animations
Add extremely quick animations for some things, maybe 2-3 frames. For instance:
@ -44,6 +44,7 @@ Add extremely quick animations for some things, maybe 2-3 frames. For instance:
### Other
* [ ] Generalize Layout so we can create grid layouts etc
* [ ] Persist UI state in external storage
* [ ] Pixel-perfect rendering (round positions to nearest pixel).
* [ ] Build in a profiler which tracks which region in which window takes up CPU.
* [ ] Draw as flame graph
* [ ] Draw as hotmap

1
emigui/src/color.rs

@ -36,3 +36,4 @@ pub const BLACK: Color = srgba(0, 0, 0, 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 LIGHT_BLUE: Color = srgba(140, 160, 255, 255);

31
emigui/src/context.rs

@ -4,18 +4,6 @@ use parking_lot::Mutex;
use crate::{layout::align_rect, *};
#[derive(Clone, Copy)]
pub enum CursorIcon {
Default,
ResizeNorthWestSouthEast,
}
impl Default for CursorIcon {
fn default() -> Self {
CursorIcon::Default
}
}
/// Contains the input, style and output of all GUI commands.
pub struct Context {
/// The default style for new regions
@ -25,8 +13,7 @@ pub struct Context {
pub(crate) memory: Mutex<Memory>,
pub(crate) graphics: Mutex<GraphicLayers>,
/// Set each frame to what the mouse cursor should look like.
pub cursor_icon: Mutex<CursorIcon>,
pub output: Mutex<Output>,
/// Used to debug name clashes of e.g. windows
used_ids: Mutex<HashMap<Id, Pos2>>,
@ -41,7 +28,7 @@ impl Clone for Context {
input: self.input,
memory: Mutex::new(self.memory.lock().clone()),
graphics: Mutex::new(self.graphics.lock().clone()),
cursor_icon: Mutex::new(self.cursor_icon.lock().clone()),
output: Mutex::new(self.output.lock().clone()),
used_ids: Mutex::new(self.used_ids.lock().clone()),
}
}
@ -55,11 +42,16 @@ impl Context {
input: Default::default(),
memory: Default::default(),
graphics: Default::default(),
cursor_icon: Default::default(),
output: Default::default(),
used_ids: Default::default(),
}
}
/// 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 input(&self) -> &GuiInput {
&self.input
}
@ -73,10 +65,13 @@ impl Context {
}
// TODO: move
pub fn new_frame(&mut self, gui_input: GuiInput) {
pub fn begin_frame(&mut self, gui_input: GuiInput) {
self.used_ids.lock().clear();
self.input = gui_input;
*self.cursor_icon.lock() = CursorIcon::Default;
}
pub fn end_frame(&self) -> Output {
std::mem::take(&mut self.output.lock())
}
pub fn drain_paint_lists(&self) -> Vec<(Rect, PaintCmd)> {

22
emigui/src/emigui.rs

@ -32,7 +32,7 @@ impl Emigui {
self.ctx.fonts.texture()
}
pub fn new_frame(&mut self, new_input: RawInput) {
pub fn begin_frame(&mut self, new_input: RawInput) {
if !self.last_input.mouse_down || self.last_input.mouse_pos.is_none() {
self.ctx.memory.lock().active_id = None;
}
@ -42,17 +42,17 @@ impl Emigui {
// TODO: avoid this clone
let mut new_ctx = (*self.ctx).clone();
new_ctx.new_frame(gui_input);
new_ctx.begin_frame(gui_input);
self.ctx = Arc::new(new_ctx);
}
/// A region for the entire screen, behind any windows.
pub fn background_region(&mut self) -> Region {
let rect = Rect::from_min_size(Default::default(), self.ctx.input.screen_size);
Region::new(self.ctx.clone(), Layer::Background, Id::background(), rect)
pub fn end_frame(&mut self) -> (Output, PaintBatches) {
let output = self.ctx.end_frame();
let paint_batches = self.paint();
(output, paint_batches)
}
pub fn paint(&mut self) -> PaintBatches {
fn paint(&mut self) -> PaintBatches {
self.mesher_options.aa_size = 1.0 / self.last_input.pixels_per_point;
let paint_commands = self.ctx.drain_paint_lists();
let batches = mesh_paint_commands(&self.mesher_options, &self.ctx.fonts, paint_commands);
@ -65,6 +65,14 @@ impl Emigui {
batches
}
/// A region for the entire screen, behind any windows.
pub fn background_region(&mut self) -> Region {
let rect = Rect::from_min_size(Default::default(), self.ctx.input.screen_size);
Region::new(self.ctx.clone(), Layer::Background, Id::background(), rect)
}
}
impl Emigui {
pub fn ui(&mut self, region: &mut Region) {
region.collapsing("Style", |region| {
region.add(Checkbox::new(

5
emigui/src/example_app.rs

@ -43,6 +43,11 @@ impl ExampleApp {
region.add(label!(
"Emigui is an experimental immediate mode GUI written in Rust."
));
region.horizontal(Align::Min, |region| {
region.add_label("Project home page:");
region.add_hyperlink("https://github.com/emilk/emigui/");
});
});
CollapsingHeader::new("Widgets")

2
emigui/src/font.rs

@ -158,7 +158,7 @@ impl Font {
(point * self.pixels_per_point).round() / self.pixels_per_point
}
/// In points
/// Height of one line of text. In points
pub fn line_spacing(&self) -> f32 {
self.scale_in_pixels / self.pixels_per_point
}

2
emigui/src/lib.rs

@ -31,7 +31,7 @@ pub use {
crate::emigui::Emigui,
collapsing_header::CollapsingHeader,
color::Color,
context::{Context, CursorIcon},
context::Context,
fonts::{FontDefinitions, Fonts, TextStyle},
id::Id,
layers::*,

14
emigui/src/region.rs

@ -109,6 +109,10 @@ impl Region {
.extend(cmds.drain(..).map(|cmd| (clip_rect, cmd)));
}
pub fn round_to_pixel(&self, point: f32) -> f32 {
self.ctx.round_to_pixel(point)
}
/// Options for this region, and any child regions we may spawn.
pub fn style(&self) -> &Style {
&self.style
@ -183,7 +187,7 @@ impl Region {
// draw a grey line on the left to mark the region
let line_start = child_rect.min() - indent * 0.5;
let line_start = line_start.round();
let line_start = line_start.round(); // TODO: round to pixel instead
let line_end = pos2(line_start.x, line_start.y + size.y - 8.0);
self.add_paint_cmd(PaintCmd::Line {
points: vec![line_start, line_end],
@ -272,7 +276,7 @@ impl Region {
Rect::from_min_max(pos, pos2(pos.x + column_width, self.desired_rect.bottom()));
Region {
id: self.make_child_region_id(&("column", col_idx)),
id: self.make_child_id(&("column", col_idx)),
dir: Direction::Vertical,
..self.child_region(child_rect)
}
@ -303,6 +307,10 @@ impl Region {
self.add(Label::new(text))
}
pub fn add_hyperlink(&mut self, url: impl Into<String>) -> GuiResponse {
self.add(Hyperlink::new(url))
}
pub fn collapsing<S, F>(&mut self, text: S, add_contents: F) -> GuiResponse
where
S: Into<String>,
@ -365,7 +373,7 @@ impl Region {
self.id.with(&Id::from_pos(self.cursor))
}
pub fn make_child_region_id<H: Hash>(&self, child_id: &H) -> Id {
pub fn make_child_id<H: Hash>(&self, child_id: &H) -> Id {
self.id.with(child_id)
}

22
emigui/src/types.rs

@ -84,6 +84,28 @@ impl GuiInput {
}
}
#[derive(Clone, Default, Serialize)]
pub struct Output {
pub cursor_icon: CursorIcon,
/// If set, open this url.
pub open_url: Option<String>,
}
#[derive(Clone, Copy, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum CursorIcon {
Default,
/// Pointing hand, used for e.g. web links
PointingHand,
ResizeNwSe,
}
impl Default for CursorIcon {
fn default() -> Self {
CursorIcon::Default
}
}
// ----------------------------------------------------------------------------
#[derive(Clone, Copy, Debug, Default, Serialize)]

58
emigui/src/widgets.rs

@ -59,6 +59,62 @@ impl Widget for Label {
// ----------------------------------------------------------------------------
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,
}
}
}
impl Widget for Hyperlink {
fn add_to(self, region: &mut Region) -> GuiResponse {
let color = color::LIGHT_BLUE;
let text_style = TextStyle::Body;
let id = region.make_child_id(&self.url);
let font = &region.fonts()[text_style];
let line_spacing = font.line_spacing();
// TODO: underline
let (text, text_size) = font.layout_multiline(&self.text, region.available_width());
let interact = region.reserve_space(text_size, Some(id));
if interact.hovered {
region.ctx().output.lock().cursor_icon = CursorIcon::PointingHand;
}
if interact.clicked {
region.ctx().output.lock().open_url = Some(self.url.clone());
}
if interact.hovered {
// Underline:
for fragment in &text {
let pos = interact.rect.min();
let y = pos.y + fragment.y_offset + line_spacing;
let y = region.round_to_pixel(y);
let min_x = pos.x + fragment.min_x();
let max_x = pos.x + fragment.max_x();
region.add_paint_cmd(PaintCmd::Line {
points: vec![pos2(min_x, y), pos2(max_x, y)],
color,
width: region.style().line_width,
});
}
}
region.add_text(interact.rect.min(), text_style, text, Some(color));
region.response(interact)
}
}
// ----------------------------------------------------------------------------
pub struct Button {
text: String,
text_color: Option<Color>,
@ -416,7 +472,7 @@ impl<'a> Widget for Slider<'a> {
let value = self.get_value_f32();
let rect = interact.rect;
let rail_radius = (height / 8.0).round().max(2.0);
let rail_radius = region.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),

10
emigui/src/window.rs

@ -139,6 +139,8 @@ impl Window {
}
};
state.outer_pos = state.outer_pos.round(); // TODO: round to pixel
let min_inner_size = self.min_size;
let max_inner_size = self
.max_size
@ -227,16 +229,16 @@ impl Window {
state.outer_pos += ctx.input().mouse_move;
}
if corner_interact.hovered || corner_interact.active {
ctx.output.lock().cursor_icon = CursorIcon::ResizeNwSe;
}
state = State {
outer_pos: state.outer_pos,
inner_size: new_inner_size,
outer_rect: outer_rect,
};
if corner_interact.hovered || corner_interact.active {
*ctx.cursor_icon.lock() = CursorIcon::ResizeNorthWestSouthEast;
}
if win_interact.active || corner_interact.active || mouse_pressed_on_window(ctx, id) {
ctx.memory.lock().move_window_to_top(id);
}

1
example_glium/Cargo.toml

@ -9,3 +9,4 @@ emigui = { path = "../emigui" }
emigui_glium = { path = "../emigui_glium" }
glium = "0.24"
webbrowser = "0.5"

18
example_glium/src/main.rs

@ -102,7 +102,7 @@ fn main() {
_ => (),
});
emigui.new_frame(raw_input);
emigui.begin_frame(raw_input);
let mut region = emigui.background_region();
let mut region = region.centered_column(region.available_width().min(480.0));
region.set_align(Align::Min);
@ -127,13 +127,21 @@ fn main() {
emigui.ui(region);
});
painter.paint_batches(&display, emigui.paint(), emigui.texture());
let (output, paint_batches) = emigui.end_frame();
painter.paint_batches(&display, paint_batches, emigui.texture());
let cursor = *emigui.ctx.cursor_icon.lock();
let cursor = match cursor {
let cursor = match output.cursor_icon {
CursorIcon::Default => glutin::MouseCursor::Default,
CursorIcon::ResizeNorthWestSouthEast => glutin::MouseCursor::NwseResize,
CursorIcon::PointingHand => glutin::MouseCursor::Hand,
CursorIcon::ResizeNwSe => glutin::MouseCursor::NwseResize,
};
if let Some(url) = output.open_url {
if let Err(err) = webbrowser::open(&url) {
eprintln!("Failed to open url: {}", err); // TODO show error in imgui
}
}
display.gl_window().set_cursor(cursor);
}
}

21
example_wasm/src/lib.rs

@ -38,10 +38,10 @@ impl State {
})
}
fn run(&mut self, raw_input: RawInput) -> Result<(), JsValue> {
fn run(&mut self, raw_input: RawInput) -> Result<Output, JsValue> {
let everything_start = now_sec();
self.emigui.new_frame(raw_input);
self.emigui.begin_frame(raw_input);
let mut region = self.emigui.background_region();
let mut region = region.centered_column(region.available_width().min(480.0));
@ -53,6 +53,10 @@ impl State {
);
region.add_label("This is not JavaScript. This is Rust, running at 60 FPS. This is the web page, reinvented with game tech.");
region.add_label("This is also work in progress, and not ready for production... yet :)");
region.horizontal(Align::Min, |region| {
region.add_label("Project home page:");
region.add_hyperlink("https://github.com/emilk/emigui/");
});
region.add(Separator::new());
region.set_align(Align::Min);
@ -88,20 +92,20 @@ impl State {
});
let bg_color = srgba(16, 16, 16, 255);
let batches = self.emigui.paint();
let result = self.webgl_painter.paint_batches(
let (output, batches) = self.emigui.end_frame();
self.webgl_painter.paint_batches(
bg_color,
batches,
self.emigui.texture(),
raw_input.pixels_per_point,
);
)?;
self.frame_times.push_back(now_sec() - everything_start);
while self.frame_times.len() > 30 {
self.frame_times.pop_front();
}
result
Ok(output)
}
}
@ -111,8 +115,9 @@ pub fn new_webgl_gui(canvas_id: &str, pixels_per_point: f32) -> Result<State, Js
}
#[wasm_bindgen]
pub fn run_gui(state: &mut State, raw_input_json: &str) -> Result<(), JsValue> {
pub fn run_gui(state: &mut State, raw_input_json: &str) -> Result<String, JsValue> {
// TODO: nicer interface than JSON
let raw_input: RawInput = serde_json::from_str(raw_input_json).unwrap();
state.run(raw_input)
let output = state.run(raw_input)?;
Ok(serde_json::to_string(&output).unwrap())
}

Loading…
Cancel
Save