Better text layout

This commit is contained in:
Emil Ernerfeldt 2018-12-27 23:26:05 +01:00
parent 641b72d6b1
commit a4fc56b441
6 changed files with 108 additions and 89 deletions

View file

@ -90,10 +90,13 @@ impl GuiSettings for crate::layout::LayoutOptions {
if gui.button("Reset LayoutOptions").clicked { if gui.button("Reset LayoutOptions").clicked {
*self = Default::default(); *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.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("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("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( gui.slider_f32(
"checkbox_radio_height", "checkbox_radio_height",
&mut self.checkbox_radio_height, &mut self.checkbox_radio_height,

View file

@ -15,10 +15,10 @@ impl Emgui {
// TODO: this should be nicer // TODO: this should be nicer
self.layout.commands.clear(); 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; self.layout.input = gui_input;
if !gui_input.mouse_down { if !gui_input.mouse_down {
self.layout.state.active_id = None; self.layout.memory.active_id = None;
} }
} }

View file

@ -6,17 +6,21 @@ use crate::{math::*, types::*};
#[derive(Clone, Copy, Debug, Serialize)] #[derive(Clone, Copy, Debug, Serialize)]
pub struct LayoutOptions { 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 // Horizontal and vertical spacing between widgets
pub item_spacing: Vec2, pub item_spacing: Vec2,
/// Indent foldable regions etc by this much. /// Indent foldable regions etc by this much.
pub indent: f32, pub indent: f32,
/// Default width of buttons, sliders etc /// Default width of sliders, foldout categories etc. TODO: percentage of parent?
pub width: f32, pub width: f32,
/// Height of a button /// Button size is text size plus this on each side
pub button_height: f32, pub button_padding: Vec2,
/// Height of a checkbox and radio button /// Height of a checkbox and radio button
pub checkbox_radio_height: f32, pub checkbox_radio_height: f32,
@ -28,10 +32,11 @@ pub struct LayoutOptions {
impl Default for LayoutOptions { impl Default for LayoutOptions {
fn default() -> Self { fn default() -> Self {
LayoutOptions { 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, indent: 21.0,
width: 200.0, width: 200.0,
button_height: 24.0, button_padding: vec2(8.0, 8.0),
checkbox_radio_height: 24.0, checkbox_radio_height: 24.0,
slider_height: 32.0, slider_height: 32.0,
} }
@ -41,7 +46,7 @@ impl Default for LayoutOptions {
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default)]
pub struct State { pub struct Memory {
/// The widget being interacted with (e.g. dragged, in case of a slider). /// The widget being interacted with (e.g. dragged, in case of a slider).
pub active_id: Option<Id>, pub active_id: Option<Id>,
@ -51,15 +56,25 @@ pub struct State {
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
struct TextFragment {
rect: Rect,
text: String,
}
type TextFragments = Vec<TextFragment>;
// ----------------------------------------------------------------------------
type Id = u64; type Id = u64;
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default)]
pub struct Layout { pub struct Layout {
pub commands: Vec<GuiCmd>,
pub cursor: Vec2,
pub input: GuiInput,
pub layout_options: LayoutOptions, pub layout_options: LayoutOptions,
pub state: State, pub input: GuiInput,
pub cursor: Vec2,
id: Id,
pub memory: Memory,
pub commands: Vec<GuiCmd>,
} }
impl Layout { impl Layout {
@ -76,18 +91,12 @@ impl Layout {
pub fn button<S: Into<String>>(&mut self, text: S) -> InteractInfo { pub fn button<S: Into<String>>(&mut self, text: S) -> InteractInfo {
let text: String = text.into(); let text: String = text.into();
let id = self.get_id(&text); let id = self.get_id(&text);
let (rect, interact) = self.reserve_space( let (text, text_size) = self.layout_text(&text);
id, let text_cursor = self.cursor + self.layout_options.button_padding;
Vec2 { let (rect, interact) =
x: self.layout_options.width, self.reserve_space(id, text_size + 2.0 * self.layout_options.button_padding);
y: self.layout_options.button_height, self.commands.push(GuiCmd::Button { interact, rect });
}, self.add_text(text_cursor, text);
);
self.commands.push(GuiCmd::Button {
interact,
rect,
text,
});
interact interact
} }
@ -115,10 +124,9 @@ impl Layout {
pub fn label<S: Into<String>>(&mut self, text: S) { pub fn label<S: Into<String>>(&mut self, text: S) {
let text: String = text.into(); let text: String = text.into();
for line in text.split('\n') { let (text, text_size) = self.layout_text(&text);
self.text(self.cursor, TextStyle::Label, line); self.add_text(self.cursor, text);
self.cursor.y += 16.0; self.cursor.y += text_size.y;
}
self.cursor.y += self.layout_options.item_spacing.y; self.cursor.y += self.layout_options.item_spacing.y;
} }
@ -179,45 +187,48 @@ impl Layout {
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
// Areas: // Areas:
pub fn foldable<S, F>(&mut self, label: S, add_contents: F) -> InteractInfo pub fn foldable<S, F>(&mut self, text: S, add_contents: F) -> InteractInfo
where where
S: Into<String>, S: Into<String>,
F: FnOnce(&mut Layout), F: FnOnce(&mut Layout),
{ {
let label: String = label.into(); let text: String = text.into();
let id = self.get_id(&label); 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( let (rect, interact) = self.reserve_space(
id, id,
Vec2 { vec2(
x: self.layout_options.width, self.layout_options.width,
y: self.layout_options.button_height, text_size.y + 2.0 * self.layout_options.button_padding.y,
}, ),
); );
if interact.clicked { if interact.clicked {
if self.state.open_foldables.contains(&id) { if self.memory.open_foldables.contains(&id) {
self.state.open_foldables.remove(&id); self.memory.open_foldables.remove(&id);
} else { } 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 { self.commands.push(GuiCmd::FoldableHeader {
interact, interact,
rect, rect,
label,
open, open,
}); });
let icon_width = 16.0; // TODO: this offset is ugly
self.add_text(text_cursor + vec2(icon_width, 0.0), text);
if open { if open {
// TODO: push/pop id stack let old_id = self.id;
self.id = id;
let old_x = self.cursor.x; let old_x = self.cursor.x;
self.cursor.x += self.layout_options.indent; self.cursor.x += self.layout_options.indent;
add_contents(self); add_contents(self);
self.cursor.x = old_x; self.cursor.x = old_x;
self.id = old_id;
// TODO: paint background?
} }
interact interact
@ -239,9 +250,9 @@ impl Layout {
let hovered = rect.contains(self.input.mouse_pos); let hovered = rect.contains(self.input.mouse_pos);
let clicked = hovered && self.input.mouse_clicked; let clicked = hovered && self.input.mouse_clicked;
if 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 { InteractInfo {
hovered, hovered,
@ -253,16 +264,40 @@ impl Layout {
fn get_id(&self, id_str: &str) -> Id { fn get_id(&self, id_str: &str) -> Id {
use std::hash::Hasher; use std::hash::Hasher;
let mut hasher = std::collections::hash_map::DefaultHasher::new(); let mut hasher = std::collections::hash_map::DefaultHasher::new();
hasher.write_u64(self.id);
hasher.write(id_str.as_bytes()); hasher.write(id_str.as_bytes());
hasher.finish() hasher.finish()
} }
fn text<S: Into<String>>(&mut self, pos: Vec2, style: TextStyle, text: S) { 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<TextFragment>) {
for fragment in text {
self.commands.push(GuiCmd::Text { self.commands.push(GuiCmd::Text {
pos, pos: pos + fragment.rect.pos,
style, style: TextStyle::Label,
text: text.into(), text: fragment.text,
text_align: TextAlign::Start, text_align: TextAlign::Start,
}); });
} }
} }
}

View file

@ -34,6 +34,16 @@ impl std::ops::Mul<f32> for Vec2 {
} }
} }
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,
}
}
}
pub fn vec2(x: f32, y: f32) -> Vec2 { pub fn vec2(x: f32, y: f32) -> Vec2 {
Vec2 { x, y } Vec2 { x, y }
} }

View file

@ -19,7 +19,9 @@ impl Default for Style {
Style { Style {
debug_rects: false, debug_rects: false,
line_width: 2.0, 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, font_size: 12.0,
} }
} }
@ -91,11 +93,7 @@ fn debug_rect(rect: Rect) -> PaintCmd {
fn translate_cmd(out_commands: &mut Vec<PaintCmd>, style: &Style, cmd: GuiCmd) { fn translate_cmd(out_commands: &mut Vec<PaintCmd>, style: &Style, cmd: GuiCmd) {
match cmd { match cmd {
GuiCmd::PaintCommands(mut commands) => out_commands.append(&mut commands), GuiCmd::PaintCommands(mut commands) => out_commands.append(&mut commands),
GuiCmd::Button { GuiCmd::Button { interact, rect } => {
interact,
rect,
text,
} => {
out_commands.push(PaintCmd::Rect { out_commands.push(PaintCmd::Rect {
corner_radius: 5.0, corner_radius: 5.0,
fill_color: Some(style.interact_fill_color(&interact)), fill_color: Some(style.interact_fill_color(&interact)),
@ -103,19 +101,6 @@ fn translate_cmd(out_commands: &mut Vec<PaintCmd>, style: &Style, cmd: GuiCmd) {
pos: rect.pos, pos: rect.pos,
size: rect.size, 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 { if style.debug_rects {
out_commands.push(debug_rect(rect)); out_commands.push(debug_rect(rect));
} }
@ -167,7 +152,6 @@ fn translate_cmd(out_commands: &mut Vec<PaintCmd>, style: &Style, cmd: GuiCmd) {
} }
GuiCmd::FoldableHeader { GuiCmd::FoldableHeader {
interact, interact,
label,
open, open,
rect, rect,
} => { } => {
@ -184,7 +168,7 @@ fn translate_cmd(out_commands: &mut Vec<PaintCmd>, style: &Style, cmd: GuiCmd) {
// TODO: paint a little triangle or arrow or something instead of this // 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: // Draw a minus:
out_commands.push(PaintCmd::Line { out_commands.push(PaintCmd::Line {
points: vec![ points: vec![
@ -205,18 +189,6 @@ fn translate_cmd(out_commands: &mut Vec<PaintCmd>, style: &Style, cmd: GuiCmd) {
width: style.line_width, 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 { GuiCmd::RadioButton {
checked, checked,

View file

@ -91,10 +91,10 @@ pub enum TextStyle {
#[derive(Clone, Debug, Serialize)] #[derive(Clone, Debug, Serialize)]
pub enum GuiCmd { pub enum GuiCmd {
PaintCommands(Vec<PaintCmd>), PaintCommands(Vec<PaintCmd>),
/// The background for a button
Button { Button {
interact: InteractInfo, interact: InteractInfo,
rect: Rect, rect: Rect,
text: String,
}, },
Checkbox { Checkbox {
checked: bool, checked: bool,
@ -102,12 +102,11 @@ pub enum GuiCmd {
rect: Rect, rect: Rect,
text: String, text: String,
}, },
// The header for a foldable region /// The header button background for a foldable region
FoldableHeader { FoldableHeader {
interact: InteractInfo, interact: InteractInfo,
open: bool, open: bool,
rect: Rect, rect: Rect,
label: String,
}, },
RadioButton { RadioButton {
checked: bool, checked: bool,