Improve text layout

This commit is contained in:
Emil Ernerfeldt 2018-12-27 23:55:16 +01:00
parent a4fc56b441
commit 1c6fd220b2
7 changed files with 80 additions and 134 deletions

Binary file not shown.

View file

@ -63,8 +63,7 @@ function paint_command(canvas, cmd) {
case "text": case "text":
ctx.fillStyle = styleFromColor(cmd.fill_color); ctx.fillStyle = styleFromColor(cmd.fill_color);
ctx.font = cmd.font_size + "px " + cmd.font_name; ctx.font = cmd.font_size + "px " + cmd.font_name;
ctx.textAlign = cmd.text_align; ctx.textBaseline = "middle";
ctx.textBaseline = "top";
ctx.fillText(cmd.text, cmd.pos.x, cmd.pos.y); ctx.fillText(cmd.text, cmd.pos.x, cmd.pos.y);
return; return;
} }

View file

@ -56,7 +56,6 @@ interface Text {
pos: Vec2; pos: Vec2;
stroke_color: Color | null; stroke_color: Color | null;
text: string; text: string;
text_align: "start" | "center" | "end";
} }
type PaintCmd = Circle | Clear | Line | Rect | Text; type PaintCmd = Circle | Clear | Line | Rect | Text;
@ -132,8 +131,7 @@ function paint_command(canvas, cmd: PaintCmd) {
case "text": case "text":
ctx.fillStyle = styleFromColor(cmd.fill_color); ctx.fillStyle = styleFromColor(cmd.fill_color);
ctx.font = `${cmd.font_size}px ${cmd.font_name}`; ctx.font = `${cmd.font_size}px ${cmd.font_name}`;
ctx.textAlign = cmd.text_align; ctx.textBaseline = "middle";
ctx.textBaseline = "top";
ctx.fillText(cmd.text, cmd.pos.x, cmd.pos.y); ctx.fillText(cmd.text, cmd.pos.x, cmd.pos.y);
return; return;
} }

View file

@ -56,7 +56,7 @@ impl GuiSettings for App {
self.count += 1; self.count += 1;
} }
gui.label(format!("The button have been clicked {} times", self.count)); gui.label(format!("This is a multiline label.\nThe button have been clicked {} times.\nBelow are more options.", self.count));
gui.foldable("Box rendering options", |gui| { gui.foldable("Box rendering options", |gui| {
gui.slider_f32("width", &mut self.width, 0.0, 500.0); gui.slider_f32("width", &mut self.width, 0.0, 500.0);
@ -78,9 +78,9 @@ impl GuiSettings for App {
}])); }]));
gui.foldable("LayoutOptions", |gui| { gui.foldable("LayoutOptions", |gui| {
let mut layout_options = gui.layout_options; let mut options = gui.options;
layout_options.show_gui(gui); options.show_gui(gui);
gui.layout_options = layout_options; gui.options = options;
}); });
} }
} }
@ -94,16 +94,11 @@ impl GuiSettings for crate::layout::LayoutOptions {
gui.slider_f32("char_size.y", &mut self.char_size.y, 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("indent", &mut self.indent, 0.0, 100.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_padding.x", &mut self.button_padding.x, 0.0, 20.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("button_padding.y", &mut self.button_padding.y, 0.0, 20.0);
gui.slider_f32( gui.slider_f32("start_icon_width", &mut self.start_icon_width, 0.0, 60.0);
"checkbox_radio_height",
&mut self.checkbox_radio_height,
0.0,
60.0,
);
gui.slider_f32("slider_height", &mut self.slider_height, 0.0, 60.0);
} }
} }

View file

@ -22,23 +22,20 @@ pub struct LayoutOptions {
/// Button size is text size plus this on each side /// Button size is text size plus this on each side
pub button_padding: Vec2, pub button_padding: Vec2,
/// Height of a checkbox and radio button /// Checkboxed, radio button and foldables have an icon at the start.
pub checkbox_radio_height: f32, /// The text starts after this many pixels.
pub start_icon_width: f32,
/// Height of a slider
pub slider_height: f32,
} }
impl Default for LayoutOptions { impl Default for LayoutOptions {
fn default() -> Self { fn default() -> Self {
LayoutOptions { LayoutOptions {
char_size: vec2(7.2, 14.0), char_size: vec2(7.2, 14.0),
item_spacing: vec2(5.0, 3.0), item_spacing: vec2(8.0, 4.0),
indent: 21.0, indent: 21.0,
width: 200.0, width: 200.0,
button_padding: vec2(8.0, 8.0), button_padding: vec2(5.0, 3.0),
checkbox_radio_height: 24.0, start_icon_width: 20.0,
slider_height: 32.0,
} }
} }
} }
@ -69,7 +66,7 @@ type Id = u64;
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default)]
pub struct Layout { pub struct Layout {
pub layout_options: LayoutOptions, pub options: LayoutOptions,
pub input: GuiInput, pub input: GuiInput,
pub cursor: Vec2, pub cursor: Vec2,
id: Id, id: Id,
@ -92,23 +89,26 @@ impl Layout {
let text: String = text.into(); let text: String = text.into();
let id = self.get_id(&text); let id = self.get_id(&text);
let (text, text_size) = self.layout_text(&text); let (text, text_size) = self.layout_text(&text);
let text_cursor = self.cursor + self.layout_options.button_padding; let text_cursor = self.cursor + self.options.button_padding;
let (rect, interact) = let (rect, interact) =
self.reserve_space(id, text_size + 2.0 * self.layout_options.button_padding); self.reserve_space(id, text_size + 2.0 * self.options.button_padding);
self.commands.push(GuiCmd::Button { interact, rect }); self.commands.push(GuiCmd::Button { interact, rect });
self.add_text(text_cursor, text); self.add_text(text_cursor, text);
interact interact
} }
pub fn checkbox<S: Into<String>>(&mut self, label: S, checked: &mut bool) -> InteractInfo { pub fn checkbox<S: Into<String>>(&mut self, text: S, checked: &mut bool) -> InteractInfo {
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.options.button_padding + vec2(self.options.start_icon_width, 0.0);
let (rect, interact) = self.reserve_space( let (rect, interact) = self.reserve_space(
id, id,
Vec2 { self.options.button_padding
x: self.layout_options.width, + vec2(self.options.start_icon_width, 0.0)
y: self.layout_options.checkbox_radio_height, + text_size
}, + self.options.button_padding,
); );
if interact.clicked { if interact.clicked {
*checked = !*checked; *checked = !*checked;
@ -117,8 +117,8 @@ impl Layout {
checked: *checked, checked: *checked,
interact, interact,
rect, rect,
text: label,
}); });
self.add_text(text_cursor, text);
interact interact
} }
@ -127,57 +127,68 @@ impl Layout {
let (text, text_size) = self.layout_text(&text); let (text, text_size) = self.layout_text(&text);
self.add_text(self.cursor, text); self.add_text(self.cursor, text);
self.cursor.y += text_size.y; self.cursor.y += text_size.y;
self.cursor.y += self.layout_options.item_spacing.y; self.cursor.y += self.options.item_spacing.y;
} }
/// A radio button /// A radio button
pub fn radio<S: Into<String>>(&mut self, label: S, checked: bool) -> InteractInfo { pub fn radio<S: Into<String>>(&mut self, text: S, checked: bool) -> InteractInfo {
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.options.button_padding + vec2(self.options.start_icon_width, 0.0);
let (rect, interact) = self.reserve_space( let (rect, interact) = self.reserve_space(
id, id,
Vec2 { self.options.button_padding
x: self.layout_options.width, + vec2(self.options.start_icon_width, 0.0)
y: self.layout_options.checkbox_radio_height, + text_size
}, + self.options.button_padding,
); );
self.commands.push(GuiCmd::RadioButton { self.commands.push(GuiCmd::RadioButton {
checked, checked,
interact, interact,
rect, rect,
text: label,
}); });
self.add_text(text_cursor, text);
interact interact
} }
pub fn slider_f32<S: Into<String>>( pub fn slider_f32<S: Into<String>>(
&mut self, &mut self,
label: S, text: S,
value: &mut f32, value: &mut f32,
min: f32, min: f32,
max: f32, max: f32,
) -> InteractInfo { ) -> InteractInfo {
debug_assert!(min <= max); debug_assert!(min <= max);
let label: String = label.into(); let text: String = text.into();
let id = self.get_id(&label); let id = self.get_id(&text);
let (rect, interact) = self.reserve_space( let (text, text_size) = self.layout_text(&format!("{}: {:.3}", text, value));
self.add_text(self.cursor, text);
self.cursor.y += text_size.y;
let (slider_rect, interact) = self.reserve_space(
id, id,
Vec2 { Vec2 {
x: self.layout_options.width, x: self.options.width,
y: self.layout_options.slider_height, y: self.options.char_size.y,
}, },
); );
if interact.active { if interact.active {
*value = remap_clamp(self.input.mouse_pos.x, rect.min().x, rect.max().x, min, max); *value = remap_clamp(
self.input.mouse_pos.x,
slider_rect.min().x,
slider_rect.max().x,
min,
max,
);
} }
self.commands.push(GuiCmd::Slider { self.commands.push(GuiCmd::Slider {
interact, interact,
label,
max, max,
min, min,
rect, rect: slider_rect,
value: *value, value: *value,
}); });
@ -195,12 +206,12 @@ impl Layout {
let text: String = text.into(); let text: String = text.into();
let id = self.get_id(&text); let id = self.get_id(&text);
let (text, text_size) = self.layout_text(&text); let (text, text_size) = self.layout_text(&text);
let text_cursor = self.cursor + self.layout_options.button_padding; let text_cursor = self.cursor + self.options.button_padding;
let (rect, interact) = self.reserve_space( let (rect, interact) = self.reserve_space(
id, id,
vec2( vec2(
self.layout_options.width, self.options.width,
text_size.y + 2.0 * self.layout_options.button_padding.y, text_size.y + 2.0 * self.options.button_padding.y,
), ),
); );
@ -218,14 +229,13 @@ impl Layout {
rect, rect,
open, open,
}); });
let icon_width = 16.0; // TODO: this offset is ugly self.add_text(text_cursor + vec2(self.options.start_icon_width, 0.0), text);
self.add_text(text_cursor + vec2(icon_width, 0.0), text);
if open { if open {
let old_id = self.id; let old_id = self.id;
self.id = 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.options.indent;
add_contents(self); add_contents(self);
self.cursor.x = old_x; self.cursor.x = old_x;
self.id = old_id; self.id = old_id;
@ -242,7 +252,7 @@ impl Layout {
size, size,
}; };
let interact = self.interactive_rect(id, &rect); let interact = self.interactive_rect(id, &rect);
self.cursor.y += rect.size.y + self.layout_options.item_spacing.y; self.cursor.y += rect.size.y + self.options.item_spacing.y;
(rect, interact) (rect, interact)
} }
@ -270,7 +280,7 @@ impl Layout {
} }
fn layout_text(&self, text: &str) -> (TextFragments, Vec2) { fn layout_text(&self, text: &str) -> (TextFragments, Vec2) {
let char_size = self.layout_options.char_size; let char_size = self.options.char_size;
let mut cursor_y = 0.0; let mut cursor_y = 0.0;
let mut max_width = 0.0; let mut max_width = 0.0;
let mut text_fragments = Vec::new(); let mut text_fragments = Vec::new();
@ -293,10 +303,9 @@ impl Layout {
fn add_text(&mut self, pos: Vec2, text: Vec<TextFragment>) { fn add_text(&mut self, pos: Vec2, text: Vec<TextFragment>) {
for fragment in text { for fragment in text {
self.commands.push(GuiCmd::Text { self.commands.push(GuiCmd::Text {
pos: pos + fragment.rect.pos, pos: pos + vec2(fragment.rect.pos.x, fragment.rect.center().y),
style: TextStyle::Label, style: TextStyle::Label,
text: fragment.text, text: fragment.text,
text_align: TextAlign::Start,
}); });
} }
} }

View file

@ -59,8 +59,8 @@ impl Style {
} }
} }
/// Returns small icon rectangle, big icon rectangle, and the remaining rectangle /// Returns small icon rectangle and big icon rectangle
fn icon_rectangles(&self, rect: &Rect) -> (Rect, Rect, Rect) { fn icon_rectangles(&self, rect: &Rect) -> (Rect, Rect) {
let box_side = 16.0; let box_side = 16.0;
let big_icon_rect = Rect::from_center_size( let big_icon_rect = Rect::from_center_size(
vec2(rect.min().x + 4.0 + box_side * 0.5, rect.center().y), vec2(rect.min().x + 4.0 + box_side * 0.5, rect.center().y),
@ -69,10 +69,7 @@ impl Style {
let small_icon_rect = Rect::from_center_size(big_icon_rect.center(), vec2(10.0, 10.0)); let small_icon_rect = Rect::from_center_size(big_icon_rect.center(), vec2(10.0, 10.0));
let rest_rect = (small_icon_rect, big_icon_rect)
Rect::from_min_size(vec2(big_icon_rect.max().x + 4.0, rect.min().y), rect.size);
(small_icon_rect, big_icon_rect, rest_rect)
} }
} }
@ -109,9 +106,8 @@ fn translate_cmd(out_commands: &mut Vec<PaintCmd>, style: &Style, cmd: GuiCmd) {
checked, checked,
interact, interact,
rect, rect,
text,
} => { } => {
let (small_icon_rect, big_icon_rect, rest_rect) = style.icon_rectangles(&rect); let (small_icon_rect, big_icon_rect) = style.icon_rectangles(&rect);
out_commands.push(PaintCmd::Rect { out_commands.push(PaintCmd::Rect {
corner_radius: 3.0, corner_radius: 3.0,
fill_color: Some(style.interact_fill_color(&interact)), fill_color: Some(style.interact_fill_color(&interact)),
@ -134,18 +130,6 @@ fn translate_cmd(out_commands: &mut Vec<PaintCmd>, style: &Style, cmd: GuiCmd) {
}); });
} }
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: rest_rect.center().y - 4.0,
},
text,
text_align: TextAlign::Start,
});
if style.debug_rects { if style.debug_rects {
out_commands.push(debug_rect(rect)); out_commands.push(debug_rect(rect));
} }
@ -168,7 +152,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, _, _) = 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![
@ -178,7 +162,7 @@ fn translate_cmd(out_commands: &mut Vec<PaintCmd>, style: &Style, cmd: GuiCmd) {
color: stroke_color, color: stroke_color,
width: style.line_width, width: style.line_width,
}); });
if open { if !open {
// Draw it as a plus: // Draw it as a plus:
out_commands.push(PaintCmd::Line { out_commands.push(PaintCmd::Line {
points: vec![ points: vec![
@ -194,12 +178,11 @@ fn translate_cmd(out_commands: &mut Vec<PaintCmd>, style: &Style, cmd: GuiCmd) {
checked, checked,
interact, interact,
rect, rect,
text,
} => { } => {
let fill_color = style.interact_fill_color(&interact); let fill_color = style.interact_fill_color(&interact);
let stroke_color = style.interact_stroke_color(&interact); let stroke_color = style.interact_stroke_color(&interact);
let (small_icon_rect, big_icon_rect, rest_rect) = style.icon_rectangles(&rect); let (small_icon_rect, big_icon_rect) = style.icon_rectangles(&rect);
out_commands.push(PaintCmd::Circle { out_commands.push(PaintCmd::Circle {
center: big_icon_rect.center(), center: big_icon_rect.center(),
@ -217,35 +200,18 @@ fn translate_cmd(out_commands: &mut Vec<PaintCmd>, style: &Style, cmd: GuiCmd) {
}); });
} }
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 - 4.0,
},
text,
text_align: TextAlign::Start,
});
if style.debug_rects { if style.debug_rects {
out_commands.push(debug_rect(rect)); out_commands.push(debug_rect(rect));
} }
} }
GuiCmd::Slider { GuiCmd::Slider {
interact, interact,
label,
max, max,
min, min,
rect, rect,
value, value,
} => { } => {
let thin_rect = Rect::from_min_size( let thin_rect = Rect::from_center_size(rect.center(), vec2(rect.size.x, 6.0));
vec2(rect.min().x, lerp(rect.min().y, rect.max().y, 2.0 / 3.0)),
vec2(rect.size.x, 8.0),
);
let marker_center_x = remap_clamp(value, min, max, rect.min().x, rect.max().x); let marker_center_x = remap_clamp(value, min, max, rect.min().x, rect.max().x);
let marker_rect = Rect::from_center_size( let marker_rect = Rect::from_center_size(
@ -269,18 +235,6 @@ fn translate_cmd(out_commands: &mut Vec<PaintCmd>, style: &Style, cmd: GuiCmd) {
size: marker_rect.size, size: marker_rect.size,
}); });
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(
rect.min().x,
lerp(rect.min().y, rect.max().y, 1.0 / 3.0) - 5.0,
),
text: format!("{}: {:.3}", label, value),
text_align: TextAlign::Start,
});
if style.debug_rects { if style.debug_rects {
out_commands.push(debug_rect(rect)); out_commands.push(debug_rect(rect));
} }
@ -288,7 +242,6 @@ fn translate_cmd(out_commands: &mut Vec<PaintCmd>, style: &Style, cmd: GuiCmd) {
GuiCmd::Text { GuiCmd::Text {
pos, pos,
text, text,
text_align,
style: text_style, style: text_style,
} => { } => {
let fill_color = match text_style { let fill_color = match text_style {
@ -298,9 +251,8 @@ fn translate_cmd(out_commands: &mut Vec<PaintCmd>, style: &Style, cmd: GuiCmd) {
fill_color, fill_color,
font_name: style.font_name.clone(), font_name: style.font_name.clone(),
font_size: style.font_size, font_size: style.font_size,
pos: pos + vec2(0.0, style.font_size / 2.0 - 5.0), // TODO pos,
text, text,
text_align,
}); });
} }
} }

View file

@ -74,14 +74,6 @@ pub struct InteractInfo {
pub active: bool, pub active: bool,
} }
#[derive(Clone, Copy, Debug, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum TextAlign {
Start, // Test with arabic text
Center,
End,
}
#[derive(Clone, Copy, Debug, Serialize)] #[derive(Clone, Copy, Debug, Serialize)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum TextStyle { pub enum TextStyle {
@ -100,7 +92,6 @@ pub enum GuiCmd {
checked: bool, checked: bool,
interact: InteractInfo, interact: InteractInfo,
rect: Rect, rect: Rect,
text: String,
}, },
/// The header button background for a foldable region /// The header button background for a foldable region
FoldableHeader { FoldableHeader {
@ -112,21 +103,21 @@ pub enum GuiCmd {
checked: bool, checked: bool,
interact: InteractInfo, interact: InteractInfo,
rect: Rect, rect: Rect,
text: String,
}, },
Slider { Slider {
interact: InteractInfo, interact: InteractInfo,
label: String,
max: f32, max: f32,
min: f32, min: f32,
rect: Rect, rect: Rect,
value: f32, value: f32,
}, },
/// Paint a single line of mono-space text.
/// The text should start at the given position and flow to the right.
/// The text should be vertically centered at the given position.
Text { Text {
pos: Vec2, pos: Vec2,
style: TextStyle, style: TextStyle,
text: String, text: String,
text_align: TextAlign,
}, },
} }
@ -162,6 +153,9 @@ pub enum PaintCmd {
pos: Vec2, pos: Vec2,
size: Vec2, size: Vec2,
}, },
/// Paint a single line of mono-space text.
/// The text should start at the given position and flow to the right.
/// The text should be vertically centered at the given position.
Text { Text {
fill_color: Color, fill_color: Color,
/// Name, e.g. Palatino /// Name, e.g. Palatino
@ -170,6 +164,5 @@ pub enum PaintCmd {
font_size: f32, font_size: f32,
pos: Vec2, pos: Vec2,
text: String, text: String,
text_align: TextAlign,
}, },
} }