Better text layout
This commit is contained in:
parent
641b72d6b1
commit
a4fc56b441
6 changed files with 108 additions and 89 deletions
|
@ -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,
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
129
src/layout.rs
129
src/layout.rs
|
@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
10
src/math.rs
10
src/math.rs
|
@ -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 }
|
||||||
}
|
}
|
||||||
|
|
38
src/style.rs
38
src/style.rs
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Reference in a new issue