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 {
*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,

View file

@ -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;
}
}

View file

@ -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<Id>,
@ -51,15 +56,25 @@ pub struct State {
// ----------------------------------------------------------------------------
struct TextFragment {
rect: Rect,
text: String,
}
type TextFragments = Vec<TextFragment>;
// ----------------------------------------------------------------------------
type Id = u64;
#[derive(Clone, Debug, Default)]
pub struct Layout {
pub commands: Vec<GuiCmd>,
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<GuiCmd>,
}
impl Layout {
@ -76,18 +91,12 @@ impl Layout {
pub fn button<S: Into<String>>(&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<S: Into<String>>(&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<S, F>(&mut self, label: S, add_contents: F) -> InteractInfo
pub fn foldable<S, F>(&mut self, text: S, add_contents: F) -> InteractInfo
where
S: Into<String>,
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<S: Into<String>>(&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<TextFragment>) {
for fragment in text {
self.commands.push(GuiCmd::Text {
pos: pos + fragment.rect.pos,
style: TextStyle::Label,
text: fragment.text,
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 {
Vec2 { x, y }
}

View file

@ -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<PaintCmd>, 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<PaintCmd>, 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<PaintCmd>, style: &Style, cmd: GuiCmd) {
}
GuiCmd::FoldableHeader {
interact,
label,
open,
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
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<PaintCmd>, 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,

View file

@ -91,10 +91,10 @@ pub enum TextStyle {
#[derive(Clone, Debug, Serialize)]
pub enum GuiCmd {
PaintCommands(Vec<PaintCmd>),
/// 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,