From 14db237b1d5f0457289aa09531f556aef794072e Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Wed, 29 Apr 2020 21:25:49 +0200 Subject: [PATCH] Basic text input support --- Cargo.lock | 62 ++++++++++++++++++ README.md | 6 +- docs/index.html | 57 ++++++++++++++++ emigui/README.md | 9 ++- emigui/src/emigui.rs | 13 ++-- emigui/src/example_app.rs | 14 +++- emigui/src/lib.rs | 1 + emigui/src/memory.rs | 3 + emigui/src/region.rs | 14 +++- emigui/src/style.rs | 5 ++ emigui/src/types.rs | 59 ++++++++++++++--- emigui/src/widgets.rs | 4 +- emigui/src/widgets/text_edit.rs | 113 ++++++++++++++++++++++++++++++++ example_glium/Cargo.toml | 1 + example_glium/src/main.rs | 102 +++++++++++++++++++++++++--- 15 files changed, 434 insertions(+), 29 deletions(-) create mode 100644 emigui/src/widgets/text_edit.rs diff --git a/Cargo.lock b/Cargo.lock index 92331f52..b6ffa22f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -95,6 +95,26 @@ dependencies = [ "libc 0.2.69 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "clipboard" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "clipboard-win 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "objc 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)", + "objc-foundation 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "objc_id 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "x11-clipboard 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "clipboard-win" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "cloudabi" version = "0.0.3" @@ -189,6 +209,7 @@ dependencies = [ name = "example_glium" version = "0.1.0" dependencies = [ + "clipboard 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", "emigui 0.1.0", "emigui_glium 0.1.0", "glium 0.24.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -436,6 +457,24 @@ dependencies = [ "malloc_buf 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "objc-foundation" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "block 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "objc 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)", + "objc_id 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "objc_id" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "objc 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "ordered-float" version = "1.0.2" @@ -915,6 +954,14 @@ dependencies = [ "x11-dl 2.18.5 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "x11-clipboard" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "xcb 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "x11-dl" version = "2.18.5" @@ -926,6 +973,15 @@ dependencies = [ "pkg-config 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "xcb" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.69 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "xdg" version = "2.2.0" @@ -951,6 +1007,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum cc 1.0.52 (registry+https://github.com/rust-lang/crates.io-index)" = "c3d87b23d6a92cd03af510a5ade527033f6aa6fa92161e2d5863a907d4c5e31d" "checksum cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" "checksum cgl 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "55e7ec0b74fe5897894cbc207092c577e87c52f8a59e8ca8d97ef37551f60a49" +"checksum clipboard 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "25a904646c0340239dcf7c51677b33928bf24fdf424b79a57909c0109075b2e7" +"checksum clipboard-win 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e3a093d6fed558e5fe24c3dfc85a68bb68f1c824f440d3ba5aca189e2998786b" "checksum cloudabi 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" "checksum cocoa 0.18.5 (registry+https://github.com/rust-lang/crates.io-index)" = "1706996401131526e36b3b49f0c4d912639ce110996f3ca144d78946727bce54" "checksum core-foundation 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)" = "25b9e03f145fd4f2bf705e07b900cd41fc636598fe5dc452fd0db1441c3f496d" @@ -985,6 +1043,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum nix 0.14.1 (registry+https://github.com/rust-lang/crates.io-index)" = "6c722bee1037d430d0f8e687bbdbf222f27cc6e4e68d5caf630857bb2b6dbdce" "checksum num-traits 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "c62be47e61d1842b9170f0fdeec8eba98e60e90e5446449a0545e5152acd7096" "checksum objc 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)" = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +"checksum objc-foundation 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" +"checksum objc_id 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" "checksum ordered-float 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "18869315e81473c951eb56ad5558bbc56978562d3ecfb87abb7a1e944cea4518" "checksum osmesa-sys 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "88cfece6e95d2e717e0872a7f53a8684712ad13822a7979bc760b9c77ec0013b" "checksum parking_lot 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)" = "d3a704eb390aafdc107b0e392f56a82b668e3a71366993b5340f5833fd62505e" @@ -1041,6 +1101,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum winapi-util 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" "checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" "checksum winit 0.19.5 (registry+https://github.com/rust-lang/crates.io-index)" = "1e96eb4bb472fa43e718e8fa4aef82f86cd9deac9483a1e1529230babdb394a8" +"checksum x11-clipboard 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "89bd49c06c9eb5d98e6ba6536cf64ac9f7ee3a009b2f53996d405b3944f6bcea" "checksum x11-dl 2.18.5 (registry+https://github.com/rust-lang/crates.io-index)" = "2bf981e3a5b3301209754218f962052d4d9ee97e478f4d26d4a6eced34c1fef8" +"checksum xcb 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)" = "5e917a3f24142e9ff8be2414e36c649d47d6cc2ba81f16201cdef96e533e02de" "checksum xdg 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d089681aa106a86fade1b0128fb5daf07d5867a509ab036d99988dec80429a57" "checksum xml-rs 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)" = "2bb76e5c421bbbeb8924c60c030331b345555024d56261dae8f3e786ed817c23" diff --git a/README.md b/README.md index e2b300ff..23dabb03 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,11 @@ An immediate mode GUI library written in Rust. For web apps or native apps. ## Goals: -* Easy to use +* Lightweight +* Short, conveniant syntax +* Responsive (60 Hz without breaking a sweat) +* Portable * Platform independent (the same code works on the web and as a native app) -* Responsive ## How it works: Loop: diff --git a/docs/index.html b/docs/index.html index 9a4070ad..6ef4633e 100644 --- a/docs/index.html +++ b/docs/index.html @@ -86,6 +86,7 @@ var g_is_touch = false; // we don't know yet var g_scroll_delta_x = 0; var g_scroll_delta_y = 0; + var g_events = []; function pixels_per_point() { return window.devicePixelRatio || 1.0; @@ -106,9 +107,11 @@ screen_size: { x: window.innerWidth, y: window.innerHeight }, pixels_per_point: pixels_per_point(), time: window.performance.now() / 1000.0, + events: g_events, }; g_scroll_delta_x = 0; g_scroll_delta_y = 0; + g_events = []; return input; } @@ -210,6 +213,37 @@ event.preventDefault(); }); + document.addEventListener("keydown", function (event) { + console.log(`keydown: '${event.key}'`); + var key = translate_key(event.key); + if (key) { + g_events.push({ "key": { "key": key, 'pressed': true } }); + } else { + g_events.push({ "text": event.key }); + } + invalidate(); + event.stopPropagation(); + event.preventDefault(); + }); + + // document.addEventListener("keypress", function (event) { + // console.log(`keypress: ${event.key} ${JSON.stringify(event)}`); + // invalidate(); + // event.stopPropagation(); + // event.preventDefault(); + // }); + + document.addEventListener("keyup", function (event) { + // console.log(`keyup: ${event.key} ${JSON.stringify(event)}`); + var key = translate_key(event.key); + if (key) { + g_events.push({ "key": { "key": key, 'pressed': false } }); + } + invalidate(); + event.stopPropagation(); + event.preventDefault(); + }); + if (!ANIMATION_FRAME) { window.addEventListener("load", invalidate); window.addEventListener("pagehide", invalidate); @@ -219,6 +253,29 @@ paint_and_schedule(); } + + function translate_key(key) { + if (key == "Alt") { return "alt"; } + if (key == "Backspace") { return "backspace"; } + if (key == "Control") { return "control"; } + if (key == "Delete") { return "delete"; } + if (key == "ArrowDown") { return "down"; } + if (key == "End") { return "end"; } + if (key == "Escape") { return "escape"; } + if (key == "Home") { return "home"; } + if (key == "Help") { return "insert"; } + if (key == "ArrowLeft") { return "left"; } + if (key == "Meta") { return "logo"; } + if (key == "PageDown") { return "page_down"; } + if (key == "PageUp") { return "page_up"; } + if (key == "Enter") { return "return"; } + if (key == "ArrowRight") { return "right"; } + if (key == "Shift") { return "shift"; } + // if (key == " ") { return "space"; } + if (key == "Tab") { return "tab"; } + if (key == "ArrowUp") { return "up"; } + return null; + } diff --git a/emigui/README.md b/emigui/README.md index 99a70280..0a9fe6fb 100644 --- a/emigui/README.md +++ b/emigui/README.md @@ -22,8 +22,14 @@ This is the core library crate Emigui. It is fully platform independent without * [ ] Kinetic scrolling * [x] Add support for clicking links * [ ] Menu bar (File, Edit, etc) -* [ ] One-line TextField +* [ ] Text input + * [x] Input events (key presses) + * [x] Text focus + * [ ] Cursor movement + * [ ] Text selection * [ ] Clipboard copy/paste + * [ ] Move focus with tab + * [ ] Handle leading/trailing space * [ ] Color picker * [ ] Style editor * [ ] Table with resizable columns @@ -45,6 +51,7 @@ Add extremely quick animations for some things, maybe 2-3 frames. For instance: * [x] Use clip rectangles when painting * [x] Use clip rectangles when interacting * [x] Adjust clip rects so edges of child widgets aren't clipped +* [ ] Use HW clip rects ### Modularity * [x] `trait Widget` (`Label`, `Slider`, `Checkbox`, ...) diff --git a/emigui/src/emigui.rs b/emigui/src/emigui.rs index 9bd33ebd..2e4ae935 100644 --- a/emigui/src/emigui.rs +++ b/emigui/src/emigui.rs @@ -149,15 +149,16 @@ fn font_definitions_ui(font_definitions: &mut FontDefinitions, region: &mut Regi impl RawInput { pub fn ui(&self, region: &mut Region) { // TODO: simpler way to show values, e.g. `region.value("Mouse Pos:", self.mouse_pos); + // TODO: easily change default font! region.add(label!("mouse_down: {}", self.mouse_down)); region.add(label!("mouse_pos: {:.1?}", self.mouse_pos)); region.add(label!("scroll_delta: {:?}", self.scroll_delta)); region.add(label!("screen_size: {:?}", self.screen_size)); region.add(label!("pixels_per_point: {}", self.pixels_per_point)); region.add(label!("time: {:.3} s", self.time)); - region.add(label!("text: {:?}", self.text)); - // region.add(label!("dropped_files: {}", self.dropped_files)); - // region.add(label!("hovered_files: {}", self.hovered_files)); + region.add(label!("events: {:?}", self.events)); + region.add(label!("dropped_files: {:?}", self.dropped_files)); + region.add(label!("hovered_files: {:?}", self.hovered_files)); } } @@ -172,8 +173,8 @@ impl GuiInput { region.add(label!("screen_size: {:?}", self.screen_size)); region.add(label!("pixels_per_point: {}", self.pixels_per_point)); region.add(label!("time: {}", self.time)); - region.add(label!("text: {:?}", self.text)); - // region.add(label!("dropped_files: {}", self.dropped_files)); - // region.add(label!("hovered_files: {}", self.hovered_files)); + region.add(label!("events: {:?}", self.events)); + region.add(label!("dropped_files: {:?}", self.dropped_files)); + region.add(label!("hovered_files: {:?}", self.hovered_files)); } } diff --git a/emigui/src/example_app.rs b/emigui/src/example_app.rs index d1151ff8..d738af36 100644 --- a/emigui/src/example_app.rs +++ b/emigui/src/example_app.rs @@ -5,6 +5,7 @@ pub struct ExampleApp { checked: bool, count: usize, radio: usize, + text_inputs: [String; 3], size: Vec2, corner_radius: f32, @@ -24,6 +25,8 @@ impl Default for ExampleApp { checked: true, radio: 0, count: 0, + text_inputs: Default::default(), + size: vec2(100.0, 50.0), corner_radius: 5.0, stroke_width: 2.0, @@ -52,7 +55,7 @@ impl ExampleApp { }); CollapsingHeader::new("Widgets") - // .default_open() + .default_open() .show(region, |region| { region.horizontal(Align::Min, |region| { region.add(label!("Text can have").text_color(srgba(110, 255, 110, 255))); @@ -94,6 +97,13 @@ impl ExampleApp { if region.add(Button::new("Double it")).clicked { self.slider_value *= 2; } + + for (i, text) in self.text_inputs.iter_mut().enumerate() { + region.horizontal(Align::Min, |region|{ + region.add(label!("Text input {}: ", i)); + region.add(TextEdit::new(text).id(i)); + }); // TODO: .tooltip_text("Enter text to edit me") + } }); region.collapsing("Layouts", |region| { @@ -151,7 +161,7 @@ impl ExampleApp { .show(region, |region| self.painting.ui(region)); CollapsingHeader::new("Resize") - .default_open() + // .default_open() .show(region, |region| { Resize::default() .default_height(200.0) diff --git a/emigui/src/lib.rs b/emigui/src/lib.rs index 77d60295..c3738bd9 100644 --- a/emigui/src/lib.rs +++ b/emigui/src/lib.rs @@ -40,4 +40,5 @@ pub use { style::Style, texture_atlas::Texture, types::*, + widgets::Widget, }; diff --git a/emigui/src/memory.rs b/emigui/src/memory.rs index d05211ef..4304302f 100644 --- a/emigui/src/memory.rs +++ b/emigui/src/memory.rs @@ -10,6 +10,9 @@ pub struct Memory { /// The widget being interacted with (e.g. dragged, in case of a slider). pub(crate) active_id: Option, + /// The widget with keyboard focus (i.e. a text input field). + pub(crate) kb_focus_id: Option, + // states of various types of widgets pub(crate) collapsing_headers: HashMap, pub(crate) scroll_areas: HashMap, diff --git a/emigui/src/region.rs b/emigui/src/region.rs index fbc6ca85..26b9835e 100644 --- a/emigui/src/region.rs +++ b/emigui/src/region.rs @@ -143,6 +143,10 @@ impl Region { self.ctx.memory.lock() } + pub fn output(&self) -> parking_lot::MutexGuard { + self.ctx.output.lock() + } + pub fn fonts(&self) -> &Fonts { &*self.ctx.fonts } @@ -281,7 +285,7 @@ impl Region { }; add_contents(&mut child_region); let size = child_region.bounding_size(); - self.reserve_space_without_padding(size); + self.reserve_space(size, None); } /// Start a region with horizontal layout @@ -356,6 +360,14 @@ impl Region { self.ctx.contains_mouse(self.layer, &self.clip_rect, rect) } + pub fn has_kb_focus(&self, id: Id) -> bool { + self.memory().kb_focus_id == Some(id) + } + + pub fn request_kb_focus(&self, id: Id) { + self.memory().kb_focus_id = Some(id); + } + // ------------------------------------------------------------------------ pub fn add(&mut self, widget: impl Widget) -> GuiResponse { diff --git a/emigui/src/style.rs b/emigui/src/style.rs index 660a3346..7e5ec704 100644 --- a/emigui/src/style.rs +++ b/emigui/src/style.rs @@ -28,6 +28,9 @@ pub struct Style { /// For stuff like check marks in check boxes. pub line_width: f32, + pub cursor_blink_hz: f32, + pub text_cursor_width: f32, + // TODO: add ability to disable animations! /// How many seconds a typical animation should last pub animation_time: f32, @@ -50,6 +53,8 @@ impl Default for Style { clickable_diameter: 22.0, start_icon_width: 16.0, line_width: 1.0, + cursor_blink_hz: 1.0, + text_cursor_width: 2.0, animation_time: 1.0 / 20.0, window: Window::default(), } diff --git a/emigui/src/types.rs b/emigui/src/types.rs index 86a109eb..199ae5ed 100644 --- a/emigui/src/types.rs +++ b/emigui/src/types.rs @@ -30,17 +30,17 @@ pub struct RawInput { /// Time in seconds. Relative to whatever. Used for animation. pub time: f64, - /// Text input, e.g. via keyboard or paste action - pub text: String, - /// Files has been dropped into the window. pub dropped_files: Vec, /// Someone is threatening to drop these on us. pub hovered_files: Vec, + + /// In-order events received this frame + pub events: Vec, } -/// What the gui maintains +/// What emigui maintains #[derive(Clone, Debug, Default)] pub struct GuiInput { /// Is the button currently down? @@ -73,14 +73,52 @@ pub struct GuiInput { /// Time in seconds. Relative to whatever. Used for animation. pub time: f64, - /// Text input, e.g. via keyboard or paste action - pub text: String, - /// Files has been dropped into the window. pub dropped_files: Vec, /// Someone is threatening to drop these on us. pub hovered_files: Vec, + + /// In-order events received this frame + pub events: Vec, +} + +#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum Event { + Copy, + Cut, + /// Text input, e.g. via keyboard or paste action + Text(String), + Key { + key: Key, + pressed: bool, + }, +} + +#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum Key { + Alt, + Backspace, + Control, + Delete, + Down, + End, + Escape, + Home, + Insert, + Left, + /// Windows key or Mac Command key + Logo, + PageDown, + PageUp, + Return, + Right, + Shift, + // Space, + Tab, + Up, } impl GuiInput { @@ -99,9 +137,9 @@ impl GuiInput { screen_size: new.screen_size, pixels_per_point: new.pixels_per_point, time: new.time, - text: new.text.clone(), dropped_files: new.dropped_files.clone(), hovered_files: new.hovered_files.clone(), + events: new.events.clone(), } } } @@ -109,8 +147,12 @@ impl GuiInput { #[derive(Clone, Default, Serialize)] pub struct Output { pub cursor_icon: CursorIcon, + /// If set, open this url. pub open_url: Option, + + /// Response to Event::Copy or Event::Cut. Ignore if empty. + pub copied_text: String, } #[derive(Clone, Copy, Serialize)] @@ -120,6 +162,7 @@ pub enum CursorIcon { /// Pointing hand, used for e.g. web links PointingHand, ResizeNwSe, + Text, } impl Default for CursorIcon { diff --git a/emigui/src/widgets.rs b/emigui/src/widgets.rs index 49cb4748..75d39a65 100644 --- a/emigui/src/widgets.rs +++ b/emigui/src/widgets.rs @@ -7,6 +7,9 @@ use crate::{ *, }; +mod text_edit; +pub use text_edit::*; + // ---------------------------------------------------------------------------- /// Anything implementing Widget can be added to a Region with Region::add @@ -336,7 +339,6 @@ impl<'a> Slider<'a> { } } - // TODO: use range syntax pub fn f32(value: &'a mut f32, range: RangeInclusive) -> Self { Slider { precision: 3, diff --git a/emigui/src/widgets/text_edit.rs b/emigui/src/widgets/text_edit.rs new file mode 100644 index 00000000..fc2533cc --- /dev/null +++ b/emigui/src/widgets/text_edit.rs @@ -0,0 +1,113 @@ +use crate::*; + +#[derive(Debug)] +pub struct TextEdit<'t> { + text: &'t mut String, + id: Option, + text_style: TextStyle, // TODO: Option, where None means "use the default for the region" + text_color: Option, +} + +impl<'t> TextEdit<'t> { + pub fn new(text: &'t mut String) -> Self { + TextEdit { + text, + id: None, + text_style: TextStyle::Body, + text_color: Default::default(), + } + } + + pub fn id(mut self, id_source: impl std::hash::Hash) -> Self { + self.id = Some(Id::new(id_source)); + self + } + + pub fn text_style(mut self, text_style: TextStyle) -> Self { + self.text_style = text_style; + self + } + + pub fn text_color(mut self, text_color: Color) -> Self { + self.text_color = Some(text_color); + self + } +} + +impl<'t> Widget for TextEdit<'t> { + fn add_to(self, region: &mut Region) -> GuiResponse { + let id = region.make_child_id(self.id); + + let font = ®ion.fonts()[self.text_style]; + let line_spacing = font.line_spacing(); + let (text, text_size) = font.layout_multiline(self.text.as_str(), region.available_width()); + let desired_size = text_size.max(vec2(region.available_width(), line_spacing)); + let interact = region.reserve_space(desired_size, Some(id)); + + if interact.clicked { + region.request_kb_focus(id); + } + if interact.hovered { + region.output().cursor_icon = CursorIcon::Text; + } + let has_kb_focus = region.has_kb_focus(id); + + if has_kb_focus { + for event in ®ion.input().events { + match event { + Event::Copy | Event::Cut => { + // TODO: cut + region.ctx().output.lock().copied_text = self.text.clone(); + } + Event::Text(text) => { + if text == "\u{7f}" { + // backspace + } else { + *self.text += text; + } + } + Event::Key { key, pressed: true } => { + match key { + Key::Backspace => { + self.text.pop(); // TODO: unicode aware + } + _ => {} + } + } + _ => {} + } + } + } + + region.add_paint_cmd(PaintCmd::Rect { + rect: interact.rect, + corner_radius: 0.0, + // fill_color: Some(color::BLACK), + fill_color: region.style().interact_fill_color(&interact), + // fill_color: Some(region.style().background_fill_color()), + outline: None, //Some(Outline::new(1.0, color::WHITE)), + }); + + if has_kb_focus { + let cursor_blink_hz = region.style().cursor_blink_hz; + let show_cursor = + (region.input().time * cursor_blink_hz as f64 * 3.0).floor() as i64 % 3 != 0; + if show_cursor { + let cursor_pos = if let Some(last) = text.last() { + interact.rect.min + vec2(last.max_x(), last.y_offset) + } else { + interact.rect.min + }; + region.add_paint_cmd(PaintCmd::line_segment( + (cursor_pos, cursor_pos + vec2(0.0, line_spacing)), + color::WHITE, + region.style().text_cursor_width, + )); + } + } + + region.add_text(interact.rect.min, self.text_style, text, self.text_color); + + region.response(interact) + } +} diff --git a/example_glium/Cargo.toml b/example_glium/Cargo.toml index 2e55e886..7ed4e41d 100644 --- a/example_glium/Cargo.toml +++ b/example_glium/Cargo.toml @@ -8,5 +8,6 @@ edition = "2018" emigui = { path = "../emigui" } emigui_glium = { path = "../emigui_glium" } +clipboard = "0.5" glium = "0.24" webbrowser = "0.5" diff --git a/example_glium/src/main.rs b/example_glium/src/main.rs index 3e15fbcb..3c0fc197 100644 --- a/example_glium/src/main.rs +++ b/example_glium/src/main.rs @@ -3,11 +3,14 @@ use std::time::{Duration, Instant}; use { + clipboard::{ClipboardContext, ClipboardProvider}, emigui::{containers::*, example_app::ExampleApp, widgets::*, *}, emigui_glium::Painter, - glium::glutin, + glium::glutin::{self, VirtualKeyCode}, }; +// TODO: move more code into emigui_glium care + fn main() { let mut events_loop = glutin::EventsLoop::new(); let window = glutin::WindowBuilder::new().with_title("Emigui example"); @@ -47,6 +50,14 @@ fn main() { let mut example_app = ExampleApp::default(); + let mut clipboard: Option = match ClipboardContext::new() { + Ok(clipboard) => Some(clipboard), + Err(err) => { + eprintln!("Failed to initialize clipboard: {}", err); + None + } + }; + while running { { // Keep smooth frame rate. TODO: proper vsync @@ -60,12 +71,15 @@ fn main() { { raw_input.time = start_time.elapsed().as_nanos() as f64 * 1e-9; raw_input.scroll_delta = vec2(0.0, 0.0); - raw_input.text.clear(); raw_input.dropped_files.clear(); raw_input.hovered_files.clear(); - events_loop.poll_events(|event| input_event(event, &mut raw_input, &mut running)); + raw_input.events.clear(); + events_loop.poll_events(|event| { + input_event(event, clipboard.as_mut(), &mut raw_input, &mut running) + }); } + let emigui_start = Instant::now(); emigui.begin_frame(raw_input.clone()); // TODO: avoid clone let mut region = emigui.background_region(); let mut region = region.centered_column(region.available_width().min(480.0)); @@ -117,6 +131,7 @@ fn main() { CursorIcon::Default => glutin::MouseCursor::Default, CursorIcon::PointingHand => glutin::MouseCursor::Hand, CursorIcon::ResizeNwSe => glutin::MouseCursor::NwseResize, + CursorIcon::Text => glutin::MouseCursor::Text, }; if let Some(url) = output.open_url { @@ -125,11 +140,24 @@ fn main() { } } + if !output.copied_text.is_empty() { + if let Some(clipboard) = clipboard.as_mut() { + if let Err(err) = clipboard.set_contents(output.copied_text) { + eprintln!("Copy/Cut error: {}", err); + } + } + } + display.gl_window().set_cursor(cursor); } } -fn input_event(event: glutin::Event, raw_input: &mut RawInput, running: &mut bool) { +fn input_event( + event: glutin::Event, + clipboard: Option<&mut ClipboardContext>, + raw_input: &mut RawInput, + running: &mut bool, +) { use glutin::WindowEvent::*; match event { glutin::Event::WindowEvent { event, .. } => match event { @@ -151,12 +179,39 @@ fn input_event(event: glutin::Event, raw_input: &mut RawInput, running: &mut boo raw_input.mouse_pos = None; } ReceivedCharacter(ch) => { - raw_input.text.push(ch); + raw_input.events.push(Event::Text(ch.to_string())); } KeyboardInput { input, .. } => { - if input.virtual_keycode == Some(glutin::VirtualKeyCode::Q) && input.modifiers.logo - { - *running = false; + if let Some(virtual_keycode) = input.virtual_keycode { + // TODO: If mac + if input.modifiers.logo && virtual_keycode == VirtualKeyCode::Q { + *running = false; + } + + match virtual_keycode { + VirtualKeyCode::Paste => { + if let Some(clipboard) = clipboard { + match clipboard.get_contents() { + Ok(contents) => { + raw_input.events.push(Event::Text(contents)); + } + Err(err) => { + eprintln!("Paste error: {}", err); + } + } + } + } + VirtualKeyCode::Copy => raw_input.events.push(Event::Copy), + VirtualKeyCode::Cut => raw_input.events.push(Event::Cut), + _ => { + if let Some(key) = translate_virtual_key_code(virtual_keycode) { + raw_input.events.push(Event::Key { + key, + pressed: input.state == glutin::ElementState::Pressed, + }); + } + } + } } } MouseWheel { delta, .. } => { @@ -178,3 +233,34 @@ fn input_event(event: glutin::Event, raw_input: &mut RawInput, running: &mut boo _ => (), } } + +fn translate_virtual_key_code(key: glutin::VirtualKeyCode) -> Option { + use VirtualKeyCode::*; + + Some(match key { + Escape => Key::Escape, + Insert => Key::Insert, + Home => Key::Home, + Delete => Key::Delete, + End => Key::End, + PageDown => Key::PageDown, + PageUp => Key::PageUp, + Left => Key::Left, + Up => Key::Up, + Right => Key::Right, + Down => Key::Down, + Back => Key::Backspace, + Return => Key::Return, + // Space => Key::Space, + Tab => Key::Tab, + + LAlt | RAlt => Key::Alt, + LShift | RShift => Key::Shift, + LControl | RControl => Key::Control, + LWin | RWin => Key::Logo, + + _ => { + return None; + } + }) +}