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":
ctx.fillStyle = styleFromColor(cmd.fill_color);
ctx.font = cmd.font_size + "px " + cmd.font_name;
ctx.textAlign = cmd.text_align;
ctx.textBaseline = "top";
ctx.textBaseline = "middle";
ctx.fillText(cmd.text, cmd.pos.x, cmd.pos.y);
return;
}

View file

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

View file

@ -56,7 +56,7 @@ impl GuiSettings for App {
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.slider_f32("width", &mut self.width, 0.0, 500.0);
@ -78,9 +78,9 @@ impl GuiSettings for App {
}]));
gui.foldable("LayoutOptions", |gui| {
let mut layout_options = gui.layout_options;
layout_options.show_gui(gui);
gui.layout_options = layout_options;
let mut options = gui.options;
options.show_gui(gui);
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("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("indent", &mut self.indent, 0.0, 100.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.y", &mut self.button_padding.y, 0.0, 20.0);
gui.slider_f32(
"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);
gui.slider_f32("start_icon_width", &mut self.start_icon_width, 0.0, 60.0);
}
}

View file

@ -22,23 +22,20 @@ pub struct LayoutOptions {
/// 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,
/// Height of a slider
pub slider_height: f32,
/// Checkboxed, radio button and foldables have an icon at the start.
/// The text starts after this many pixels.
pub start_icon_width: f32,
}
impl Default for LayoutOptions {
fn default() -> Self {
LayoutOptions {
char_size: vec2(7.2, 14.0),
item_spacing: vec2(5.0, 3.0),
item_spacing: vec2(8.0, 4.0),
indent: 21.0,
width: 200.0,
button_padding: vec2(8.0, 8.0),
checkbox_radio_height: 24.0,
slider_height: 32.0,
button_padding: vec2(5.0, 3.0),
start_icon_width: 20.0,
}
}
}
@ -69,7 +66,7 @@ type Id = u64;
#[derive(Clone, Debug, Default)]
pub struct Layout {
pub layout_options: LayoutOptions,
pub options: LayoutOptions,
pub input: GuiInput,
pub cursor: Vec2,
id: Id,
@ -92,23 +89,26 @@ impl Layout {
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 text_cursor = self.cursor + self.options.button_padding;
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.add_text(text_cursor, text);
interact
}
pub fn checkbox<S: Into<String>>(&mut self, label: S, checked: &mut bool) -> InteractInfo {
let label: String = label.into();
let id = self.get_id(&label);
pub fn checkbox<S: Into<String>>(&mut self, text: S, checked: &mut bool) -> InteractInfo {
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.options.button_padding + vec2(self.options.start_icon_width, 0.0);
let (rect, interact) = self.reserve_space(
id,
Vec2 {
x: self.layout_options.width,
y: self.layout_options.checkbox_radio_height,
},
self.options.button_padding
+ vec2(self.options.start_icon_width, 0.0)
+ text_size
+ self.options.button_padding,
);
if interact.clicked {
*checked = !*checked;
@ -117,8 +117,8 @@ impl Layout {
checked: *checked,
interact,
rect,
text: label,
});
self.add_text(text_cursor, text);
interact
}
@ -127,57 +127,68 @@ impl Layout {
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;
self.cursor.y += self.options.item_spacing.y;
}
/// A radio button
pub fn radio<S: Into<String>>(&mut self, label: S, checked: bool) -> InteractInfo {
let label: String = label.into();
let id = self.get_id(&label);
pub fn radio<S: Into<String>>(&mut self, text: S, checked: bool) -> InteractInfo {
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.options.button_padding + vec2(self.options.start_icon_width, 0.0);
let (rect, interact) = self.reserve_space(
id,
Vec2 {
x: self.layout_options.width,
y: self.layout_options.checkbox_radio_height,
},
self.options.button_padding
+ vec2(self.options.start_icon_width, 0.0)
+ text_size
+ self.options.button_padding,
);
self.commands.push(GuiCmd::RadioButton {
checked,
interact,
rect,
text: label,
});
self.add_text(text_cursor, text);
interact
}
pub fn slider_f32<S: Into<String>>(
&mut self,
label: S,
text: S,
value: &mut f32,
min: f32,
max: f32,
) -> InteractInfo {
debug_assert!(min <= max);
let label: String = label.into();
let id = self.get_id(&label);
let (rect, interact) = self.reserve_space(
let text: String = text.into();
let id = self.get_id(&text);
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,
Vec2 {
x: self.layout_options.width,
y: self.layout_options.slider_height,
x: self.options.width,
y: self.options.char_size.y,
},
);
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 {
interact,
label,
max,
min,
rect,
rect: slider_rect,
value: *value,
});
@ -195,12 +206,12 @@ impl Layout {
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 text_cursor = self.cursor + self.options.button_padding;
let (rect, interact) = self.reserve_space(
id,
vec2(
self.layout_options.width,
text_size.y + 2.0 * self.layout_options.button_padding.y,
self.options.width,
text_size.y + 2.0 * self.options.button_padding.y,
),
);
@ -218,14 +229,13 @@ impl Layout {
rect,
open,
});
let icon_width = 16.0; // TODO: this offset is ugly
self.add_text(text_cursor + vec2(icon_width, 0.0), text);
self.add_text(text_cursor + vec2(self.options.start_icon_width, 0.0), text);
if open {
let old_id = self.id;
self.id = id;
let old_x = self.cursor.x;
self.cursor.x += self.layout_options.indent;
self.cursor.x += self.options.indent;
add_contents(self);
self.cursor.x = old_x;
self.id = old_id;
@ -242,7 +252,7 @@ impl Layout {
size,
};
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)
}
@ -270,7 +280,7 @@ impl Layout {
}
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 max_width = 0.0;
let mut text_fragments = Vec::new();
@ -293,10 +303,9 @@ impl Layout {
fn add_text(&mut self, pos: Vec2, text: Vec<TextFragment>) {
for fragment in 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,
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
fn icon_rectangles(&self, rect: &Rect) -> (Rect, Rect, Rect) {
/// Returns small icon rectangle and big icon rectangle
fn icon_rectangles(&self, rect: &Rect) -> (Rect, Rect) {
let box_side = 16.0;
let big_icon_rect = Rect::from_center_size(
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 rest_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)
(small_icon_rect, big_icon_rect)
}
}
@ -109,9 +106,8 @@ fn translate_cmd(out_commands: &mut Vec<PaintCmd>, style: &Style, cmd: GuiCmd) {
checked,
interact,
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 {
corner_radius: 3.0,
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 {
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
let (small_icon_rect, _, _) = style.icon_rectangles(&rect);
let (small_icon_rect, _) = style.icon_rectangles(&rect);
// Draw a minus:
out_commands.push(PaintCmd::Line {
points: vec![
@ -178,7 +162,7 @@ fn translate_cmd(out_commands: &mut Vec<PaintCmd>, style: &Style, cmd: GuiCmd) {
color: stroke_color,
width: style.line_width,
});
if open {
if !open {
// Draw it as a plus:
out_commands.push(PaintCmd::Line {
points: vec![
@ -194,12 +178,11 @@ fn translate_cmd(out_commands: &mut Vec<PaintCmd>, style: &Style, cmd: GuiCmd) {
checked,
interact,
rect,
text,
} => {
let fill_color = style.interact_fill_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 {
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 {
out_commands.push(debug_rect(rect));
}
}
GuiCmd::Slider {
interact,
label,
max,
min,
rect,
value,
} => {
let thin_rect = Rect::from_min_size(
vec2(rect.min().x, lerp(rect.min().y, rect.max().y, 2.0 / 3.0)),
vec2(rect.size.x, 8.0),
);
let thin_rect = Rect::from_center_size(rect.center(), vec2(rect.size.x, 6.0));
let marker_center_x = remap_clamp(value, min, max, rect.min().x, rect.max().x);
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,
});
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 {
out_commands.push(debug_rect(rect));
}
@ -288,7 +242,6 @@ fn translate_cmd(out_commands: &mut Vec<PaintCmd>, style: &Style, cmd: GuiCmd) {
GuiCmd::Text {
pos,
text,
text_align,
style: 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,
font_name: style.font_name.clone(),
font_size: style.font_size,
pos: pos + vec2(0.0, style.font_size / 2.0 - 5.0), // TODO
pos,
text,
text_align,
});
}
}

View file

@ -74,14 +74,6 @@ pub struct InteractInfo {
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)]
#[serde(rename_all = "snake_case")]
pub enum TextStyle {
@ -100,7 +92,6 @@ pub enum GuiCmd {
checked: bool,
interact: InteractInfo,
rect: Rect,
text: String,
},
/// The header button background for a foldable region
FoldableHeader {
@ -112,21 +103,21 @@ pub enum GuiCmd {
checked: bool,
interact: InteractInfo,
rect: Rect,
text: String,
},
Slider {
interact: InteractInfo,
label: String,
max: f32,
min: f32,
rect: Rect,
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 {
pos: Vec2,
style: TextStyle,
text: String,
text_align: TextAlign,
},
}
@ -162,6 +153,9 @@ pub enum PaintCmd {
pos: 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 {
fill_color: Color,
/// Name, e.g. Palatino
@ -170,6 +164,5 @@ pub enum PaintCmd {
font_size: f32,
pos: Vec2,
text: String,
text_align: TextAlign,
},
}