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 {
|
||||
*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,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
135
src/layout.rs
135
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<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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
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 {
|
||||
Vec2 { x, y }
|
||||
}
|
||||
|
|
38
src/style.rs
38
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<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,
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in a new issue