From a4fc56b441134d6c7b416350c16228fb2c2ee4db Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Thu, 27 Dec 2018 23:26:05 +0100 Subject: [PATCH] Better text layout --- src/app.rs | 5 +- src/emgui.rs | 4 +- src/layout.rs | 135 +++++++++++++++++++++++++++++++------------------- src/math.rs | 10 ++++ src/style.rs | 38 ++------------ src/types.rs | 5 +- 6 files changed, 108 insertions(+), 89 deletions(-) diff --git a/src/app.rs b/src/app.rs index 0e6f7925..2623282a 100644 --- a/src/app.rs +++ b/src/app.rs @@ -90,10 +90,13 @@ impl GuiSettings for crate::layout::LayoutOptions { if gui.button("Reset LayoutOptions").clicked { *self = Default::default(); } + gui.slider_f32("char_size.x", &mut self.char_size.x, 0.0, 20.0); + gui.slider_f32("char_size.y", &mut self.char_size.y, 0.0, 20.0); gui.slider_f32("item_spacing.x", &mut self.item_spacing.x, 0.0, 10.0); gui.slider_f32("item_spacing.y", &mut self.item_spacing.y, 0.0, 10.0); gui.slider_f32("width", &mut self.width, 0.0, 1000.0); - gui.slider_f32("button_height", &mut self.button_height, 0.0, 60.0); + gui.slider_f32("button_padding.x", &mut self.button_padding.x, 0.0, 20.0); + gui.slider_f32("button_padding.y", &mut self.button_padding.y, 0.0, 20.0); gui.slider_f32( "checkbox_radio_height", &mut self.checkbox_radio_height, diff --git a/src/emgui.rs b/src/emgui.rs index ace896b0..310d0ef7 100644 --- a/src/emgui.rs +++ b/src/emgui.rs @@ -15,10 +15,10 @@ impl Emgui { // TODO: this should be nicer self.layout.commands.clear(); - self.layout.cursor = vec2(32.0, 32.0); + self.layout.cursor = vec2(0.0, 0.0); self.layout.input = gui_input; if !gui_input.mouse_down { - self.layout.state.active_id = None; + self.layout.memory.active_id = None; } } diff --git a/src/layout.rs b/src/layout.rs index 7a15b7a4..4cdfa1a4 100644 --- a/src/layout.rs +++ b/src/layout.rs @@ -6,17 +6,21 @@ use crate::{math::*, types::*}; #[derive(Clone, Copy, Debug, Serialize)] pub struct LayoutOptions { + /// The width and height of a single character (including any spacing). + /// All text is monospace! + pub char_size: Vec2, + // Horizontal and vertical spacing between widgets pub item_spacing: Vec2, /// Indent foldable regions etc by this much. pub indent: f32, - /// Default width of buttons, sliders etc + /// Default width of sliders, foldout categories etc. TODO: percentage of parent? pub width: f32, - /// Height of a button - pub button_height: f32, + /// Button size is text size plus this on each side + pub button_padding: Vec2, /// Height of a checkbox and radio button pub checkbox_radio_height: f32, @@ -28,10 +32,11 @@ pub struct LayoutOptions { impl Default for LayoutOptions { fn default() -> Self { LayoutOptions { - item_spacing: Vec2 { x: 8.0, y: 4.0 }, + char_size: vec2(7.2, 14.0), + item_spacing: vec2(5.0, 3.0), indent: 21.0, width: 200.0, - button_height: 24.0, + button_padding: vec2(8.0, 8.0), checkbox_radio_height: 24.0, slider_height: 32.0, } @@ -41,7 +46,7 @@ impl Default for LayoutOptions { // ---------------------------------------------------------------------------- #[derive(Clone, Debug, Default)] -pub struct State { +pub struct Memory { /// The widget being interacted with (e.g. dragged, in case of a slider). pub active_id: Option, @@ -51,15 +56,25 @@ pub struct State { // ---------------------------------------------------------------------------- +struct TextFragment { + rect: Rect, + text: String, +} + +type TextFragments = Vec; + +// ---------------------------------------------------------------------------- + type Id = u64; #[derive(Clone, Debug, Default)] pub struct Layout { - pub commands: Vec, - pub cursor: Vec2, - pub input: GuiInput, pub layout_options: LayoutOptions, - pub state: State, + pub input: GuiInput, + pub cursor: Vec2, + id: Id, + pub memory: Memory, + pub commands: Vec, } impl Layout { @@ -76,18 +91,12 @@ impl Layout { pub fn button>(&mut self, text: S) -> InteractInfo { let text: String = text.into(); let id = self.get_id(&text); - let (rect, interact) = self.reserve_space( - id, - Vec2 { - x: self.layout_options.width, - y: self.layout_options.button_height, - }, - ); - self.commands.push(GuiCmd::Button { - interact, - rect, - text, - }); + let (text, text_size) = self.layout_text(&text); + let text_cursor = self.cursor + self.layout_options.button_padding; + let (rect, interact) = + self.reserve_space(id, text_size + 2.0 * self.layout_options.button_padding); + self.commands.push(GuiCmd::Button { interact, rect }); + self.add_text(text_cursor, text); interact } @@ -115,10 +124,9 @@ impl Layout { pub fn label>(&mut self, text: S) { let text: String = text.into(); - for line in text.split('\n') { - self.text(self.cursor, TextStyle::Label, line); - self.cursor.y += 16.0; - } + let (text, text_size) = self.layout_text(&text); + self.add_text(self.cursor, text); + self.cursor.y += text_size.y; self.cursor.y += self.layout_options.item_spacing.y; } @@ -179,45 +187,48 @@ impl Layout { // ------------------------------------------------------------------------ // Areas: - pub fn foldable(&mut self, label: S, add_contents: F) -> InteractInfo + pub fn foldable(&mut self, text: S, add_contents: F) -> InteractInfo where S: Into, F: FnOnce(&mut Layout), { - let label: String = label.into(); - let id = self.get_id(&label); + let text: String = text.into(); + let id = self.get_id(&text); + let (text, text_size) = self.layout_text(&text); + let text_cursor = self.cursor + self.layout_options.button_padding; let (rect, interact) = self.reserve_space( id, - Vec2 { - x: self.layout_options.width, - y: self.layout_options.button_height, - }, + vec2( + self.layout_options.width, + text_size.y + 2.0 * self.layout_options.button_padding.y, + ), ); if interact.clicked { - if self.state.open_foldables.contains(&id) { - self.state.open_foldables.remove(&id); + if self.memory.open_foldables.contains(&id) { + self.memory.open_foldables.remove(&id); } else { - self.state.open_foldables.insert(id); + self.memory.open_foldables.insert(id); } } - let open = self.state.open_foldables.contains(&id); + let open = self.memory.open_foldables.contains(&id); self.commands.push(GuiCmd::FoldableHeader { interact, rect, - label, open, }); + let icon_width = 16.0; // TODO: this offset is ugly + self.add_text(text_cursor + vec2(icon_width, 0.0), text); if open { - // TODO: push/pop id stack + let old_id = self.id; + self.id = id; let old_x = self.cursor.x; self.cursor.x += self.layout_options.indent; add_contents(self); self.cursor.x = old_x; - - // TODO: paint background? + self.id = old_id; } interact @@ -239,9 +250,9 @@ impl Layout { let hovered = rect.contains(self.input.mouse_pos); let clicked = hovered && self.input.mouse_clicked; if clicked { - self.state.active_id = Some(id); + self.memory.active_id = Some(id); } - let active = self.state.active_id == Some(id); + let active = self.memory.active_id == Some(id); InteractInfo { hovered, @@ -253,16 +264,40 @@ impl Layout { fn get_id(&self, id_str: &str) -> Id { use std::hash::Hasher; let mut hasher = std::collections::hash_map::DefaultHasher::new(); + hasher.write_u64(self.id); hasher.write(id_str.as_bytes()); hasher.finish() } - fn text>(&mut self, pos: Vec2, style: TextStyle, text: S) { - self.commands.push(GuiCmd::Text { - pos, - style, - text: text.into(), - text_align: TextAlign::Start, - }); + fn layout_text(&self, text: &str) -> (TextFragments, Vec2) { + let char_size = self.layout_options.char_size; + let mut cursor_y = 0.0; + let mut max_width = 0.0; + let mut text_fragments = Vec::new(); + for line in text.split('\n') { + // TODO: break long lines + let line_width = char_size.x * (line.len() as f32); + + text_fragments.push(TextFragment { + rect: Rect::from_min_size(vec2(0.0, cursor_y), vec2(line_width, char_size.y)), + text: line.into(), + }); + + cursor_y += char_size.y; + max_width = line_width.max(max_width); + } + let bounding_size = vec2(max_width, cursor_y); + (text_fragments, bounding_size) + } + + fn add_text(&mut self, pos: Vec2, text: Vec) { + for fragment in text { + self.commands.push(GuiCmd::Text { + pos: pos + fragment.rect.pos, + style: TextStyle::Label, + text: fragment.text, + text_align: TextAlign::Start, + }); + } } } diff --git a/src/math.rs b/src/math.rs index 47193884..eff4bce7 100644 --- a/src/math.rs +++ b/src/math.rs @@ -34,6 +34,16 @@ impl std::ops::Mul for Vec2 { } } +impl std::ops::Mul for f32 { + type Output = Vec2; + fn mul(self, vec: Vec2) -> Vec2 { + Vec2 { + x: self * vec.x, + y: self * vec.y, + } + } +} + pub fn vec2(x: f32, y: f32) -> Vec2 { Vec2 { x, y } } diff --git a/src/style.rs b/src/style.rs index d176a156..0c8f34dd 100644 --- a/src/style.rs +++ b/src/style.rs @@ -19,7 +19,9 @@ impl Default for Style { Style { debug_rects: false, line_width: 2.0, - font_name: "Palatino".to_string(), + // font_name: "Palatino".to_string(), + font_name: "Courier".to_string(), + // font_name: "Courier New".to_string(), font_size: 12.0, } } @@ -91,11 +93,7 @@ fn debug_rect(rect: Rect) -> PaintCmd { fn translate_cmd(out_commands: &mut Vec, style: &Style, cmd: GuiCmd) { match cmd { GuiCmd::PaintCommands(mut commands) => out_commands.append(&mut commands), - GuiCmd::Button { - interact, - rect, - text, - } => { + GuiCmd::Button { interact, rect } => { out_commands.push(PaintCmd::Rect { corner_radius: 5.0, fill_color: Some(style.interact_fill_color(&interact)), @@ -103,19 +101,6 @@ fn translate_cmd(out_commands: &mut Vec, style: &Style, cmd: GuiCmd) { pos: rect.pos, size: rect.size, }); - // TODO: clip-rect of text - out_commands.push(PaintCmd::Text { - fill_color: style.interact_stroke_color(&interact), - font_name: style.font_name.clone(), - font_size: style.font_size, - pos: Vec2 { - x: rect.center().x, - y: rect.center().y - 6.0, - }, - text, - text_align: TextAlign::Center, - }); - if style.debug_rects { out_commands.push(debug_rect(rect)); } @@ -167,7 +152,6 @@ fn translate_cmd(out_commands: &mut Vec, style: &Style, cmd: GuiCmd) { } GuiCmd::FoldableHeader { interact, - label, open, rect, } => { @@ -184,7 +168,7 @@ fn translate_cmd(out_commands: &mut Vec, style: &Style, cmd: GuiCmd) { // TODO: paint a little triangle or arrow or something instead of this - let (small_icon_rect, _, rest_rect) = style.icon_rectangles(&rect); + let (small_icon_rect, _, _) = style.icon_rectangles(&rect); // Draw a minus: out_commands.push(PaintCmd::Line { points: vec![ @@ -205,18 +189,6 @@ fn translate_cmd(out_commands: &mut Vec, style: &Style, cmd: GuiCmd) { width: style.line_width, }); } - - out_commands.push(PaintCmd::Text { - fill_color: stroke_color, - font_name: style.font_name.clone(), - font_size: style.font_size, - pos: Vec2 { - x: rest_rect.min().x, - y: rect.center().y - style.font_size / 2.0, - }, - text: label, - text_align: TextAlign::Start, - }); } GuiCmd::RadioButton { checked, diff --git a/src/types.rs b/src/types.rs index cf282f81..a6313460 100644 --- a/src/types.rs +++ b/src/types.rs @@ -91,10 +91,10 @@ pub enum TextStyle { #[derive(Clone, Debug, Serialize)] pub enum GuiCmd { PaintCommands(Vec), + /// The background for a button Button { interact: InteractInfo, rect: Rect, - text: String, }, Checkbox { checked: bool, @@ -102,12 +102,11 @@ pub enum GuiCmd { rect: Rect, text: String, }, - // The header for a foldable region + /// The header button background for a foldable region FoldableHeader { interact: InteractInfo, open: bool, rect: Rect, - label: String, }, RadioButton { checked: bool,