Simplify and improve the default visual style

This commit is contained in:
Emil Ernerfeldt 2020-05-17 09:44:09 +02:00
parent 01568acef2
commit 037b22be7f
17 changed files with 393 additions and 189 deletions

View file

@ -53,6 +53,7 @@ pub const fn additive_gray(l: u8) -> Color {
pub const TRANSPARENT: Color = srgba(0, 0, 0, 0);
pub const BLACK: Color = srgba(0, 0, 0, 255);
pub const LIGHT_GRAY: Color = srgba(220, 220, 220, 255);
pub const GRAY: Color = srgba(160, 160, 160, 255);
pub const WHITE: Color = srgba(255, 255, 255, 255);
pub const RED: Color = srgba(255, 0, 0, 255);
pub const GREEN: Color = srgba(0, 255, 0, 255);

View file

@ -63,7 +63,8 @@ impl CollapsingHeader {
let text_pos = available.min + vec2(ui.style().indent, 0.0);
let galley = label.layout(available.width() - ui.style().indent, ui);
let text_max_x = text_pos.x + galley.size.x;
let desired_width = available.width().max(text_max_x - available.left());
let desired_width = text_max_x - available.left();
let desired_width = desired_width.max(available.width());
let interact = ui.reserve_space(
vec2(
@ -87,9 +88,19 @@ impl CollapsingHeader {
*state
};
let animation_time = ui.style().animation_time;
let time_since_toggle = (ui.input().time - state.toggle_time) as f32;
let time_since_toggle = time_since_toggle + ui.input().dt; // Instant feedback
let openness = if state.open {
remap_clamp(time_since_toggle, 0.0..=animation_time, 0.0..=1.0)
} else {
remap_clamp(time_since_toggle, 0.0..=animation_time, 1.0..=0.0)
};
let animate = time_since_toggle < animation_time;
let where_to_put_background = ui.paint_list_len();
paint_icon(ui, &state, &interact);
paint_icon(ui, &interact, openness);
ui.add_galley(
text_pos,
@ -102,18 +113,14 @@ impl CollapsingHeader {
where_to_put_background,
PaintCmd::Rect {
corner_radius: ui.style().interact(&interact).corner_radius,
fill_color: ui.style().interact(&interact).fill_color,
outline: ui.style().interact(&interact).outline,
fill_color: ui.style().interact(&interact).bg_fill_color,
outline: None,
rect: interact.rect,
},
);
ui.expand_to_include_child(interact.rect); // TODO: remove, just a test
let animation_time = ui.style().animation_time;
let time_since_toggle = (ui.input().time - state.toggle_time) as f32;
let time_since_toggle = time_since_toggle + ui.input().dt; // Instant feedback
let animate = time_since_toggle < animation_time;
if animate {
ui.indent(id, |child_ui| {
let max_height = if state.open {
@ -154,7 +161,7 @@ impl CollapsingHeader {
}
}
fn paint_icon(ui: &mut Ui, state: &State, interact: &InteractInfo) {
fn paint_icon(ui: &mut Ui, interact: &InteractInfo, openness: f32) {
let stroke_color = ui.style().interact(interact).stroke_color;
let stroke_width = ui.style().interact(interact).stroke_width;
@ -164,25 +171,24 @@ fn paint_icon(ui: &mut Ui, state: &State, interact: &InteractInfo) {
interact.rect.center().y,
));
// Draw a minus:
ui.add_paint_cmd(PaintCmd::LineSegment {
points: [
pos2(small_icon_rect.left(), small_icon_rect.center().y),
pos2(small_icon_rect.right(), small_icon_rect.center().y),
],
color: stroke_color,
width: stroke_width,
});
if !state.open {
// Draw it as a plus:
ui.add_paint_cmd(PaintCmd::LineSegment {
points: [
pos2(small_icon_rect.center().x, small_icon_rect.top()),
pos2(small_icon_rect.center().x, small_icon_rect.bottom()),
],
color: stroke_color,
width: stroke_width,
});
// Draw a pointy triangle arrow:
let rect = Rect::from_center_size(
small_icon_rect.center(),
vec2(small_icon_rect.width(), small_icon_rect.height()) * 0.75,
);
let mut points = [rect.left_top(), rect.right_top(), rect.center_bottom()];
let rotation = Vec2::angled(remap(openness, 0.0..=1.0, -TAU / 4.0..=0.0));
for p in &mut points {
let v = *p - rect.center();
let v = rotation.rotate_other(v);
*p = rect.center() + v;
}
// }
ui.add_paint_cmd(PaintCmd::Path {
path: mesher::Path::from_point_loop(&points),
closed: true,
fill_color: None,
outline: Some(Outline::new(stroke_width, stroke_color)),
});
}

View file

@ -15,16 +15,25 @@ impl Frame {
Self {
margin: style.window_padding,
corner_radius: style.window.corner_radius,
fill_color: Some(style.background_fill_color()),
fill_color: Some(style.background_fill_color),
outline: Some(Outline::new(1.0, color::WHITE)),
}
}
pub fn menu_bar(_style: &Style) -> Self {
Self {
margin: Vec2::splat(1.0),
corner_radius: 0.0,
fill_color: None,
outline: Some(Outline::new(0.5, color::white(128))),
}
}
pub fn menu(style: &Style) -> Self {
Self {
margin: Vec2::splat(1.0),
corner_radius: 2.0,
fill_color: Some(style.background_fill_color()),
fill_color: Some(style.background_fill_color),
outline: Some(Outline::new(1.0, color::white(128))),
}
}
@ -33,7 +42,7 @@ impl Frame {
Self {
margin: style.window_padding,
corner_radius: 5.0,
fill_color: Some(style.background_fill_color()),
fill_color: Some(style.background_fill_color),
outline: Some(Outline::new(1.0, color::white(128))),
}
}

View file

@ -21,13 +21,16 @@ impl Default for BarState {
}
pub fn bar(ui: &mut Ui, add_contents: impl FnOnce(&mut Ui)) -> InteractInfo {
ui.horizontal(|ui| {
Frame::default().show(ui, |ui| {
ui.inner_layout(Layout::horizontal(Align::Center), |ui| {
Frame::menu_bar(ui.style()).show(ui, |ui| {
let mut style = ui.style().clone();
style.button_padding = vec2(2.0, 0.0);
style.interact.inactive.fill_color = None;
style.interact.inactive.outline = None;
style.interact.hovered.fill_color = None;
// style.interact.active.bg_fill_color = None;
style.interact.active.rect_outline = None;
// style.interact.hovered.bg_fill_color = None;
style.interact.hovered.rect_outline = None;
style.interact.inactive.bg_fill_color = None;
style.interact.inactive.rect_outline = None;
ui.set_style(style);
// Take full width and fixed height:
@ -55,7 +58,7 @@ pub fn menu(ui: &mut Ui, title: impl Into<String>, add_contents: impl FnOnce(&mu
let mut button = Button::new(title);
if bar_state.open_menu == Some(menu_id) {
button = button.fill_color(ui.style().interact.active.fill_color);
button = button.fill_color(Some(ui.style().interact.active.fill_color));
}
let button_interact = ui.add(button);
@ -75,11 +78,12 @@ pub fn menu(ui: &mut Ui, title: impl Into<String>, add_contents: impl FnOnce(&mu
resize.show(ui, |ui| {
let mut style = ui.style().clone();
style.button_padding = vec2(2.0, 0.0);
style.interact.inactive.fill_color = None;
style.interact.inactive.outline = None;
style.interact.active.corner_radius = 0.0;
style.interact.hovered.corner_radius = 0.0;
style.interact.inactive.corner_radius = 0.0;
// style.interact.active.bg_fill_color = None;
style.interact.active.rect_outline = None;
// style.interact.hovered.bg_fill_color = None;
style.interact.hovered.rect_outline = None;
style.interact.inactive.bg_fill_color = None;
style.interact.inactive.rect_outline = None;
ui.set_style(style);
ui.set_layout(Layout::justified(Direction::Vertical));
add_contents(ui)

View file

@ -27,6 +27,7 @@ pub struct Resize {
expand_width_to_fit_content: bool,
expand_height_to_fit_content: bool,
outline: bool,
handle_offset: Vec2,
}
@ -34,13 +35,14 @@ impl Default for Resize {
fn default() -> Self {
Self {
resizable: true,
min_size: Vec2::splat(32.0),
min_size: Vec2::splat(16.0),
max_size: Vec2::infinity(),
default_size: vec2(f32::INFINITY, 200.0), // TODO
auto_shrink_width: false,
auto_shrink_height: false,
expand_width_to_fit_content: true,
expand_height_to_fit_content: true,
outline: true,
handle_offset: Default::default(),
}
}
@ -128,12 +130,6 @@ impl Resize {
self
}
/// Offset the position of the resize handle by this much
pub fn handle_offset(mut self, handle_offset: Vec2) -> Self {
self.handle_offset = handle_offset;
self
}
pub fn auto_shrink_width(mut self, auto_shrink_width: bool) -> Self {
self.auto_shrink_width = auto_shrink_width;
self
@ -143,6 +139,17 @@ impl Resize {
self.auto_shrink_height = auto_shrink_height;
self
}
/// Offset the position of the resize handle by this much
pub fn handle_offset(mut self, handle_offset: Vec2) -> Self {
self.handle_offset = handle_offset;
self
}
pub fn outline(mut self, outline: bool) -> Self {
self.outline = outline;
self
}
}
// TODO: a common trait for Things that follow this pattern
@ -240,6 +247,17 @@ impl Resize {
// ------------------------------
if self.outline && corner_interact.is_some() {
let rect = Rect::from_min_size(position, state.size);
let rect = rect.expand(2.0); // breathing room for content
ui.add_paint_cmd(PaintCmd::Rect {
rect,
corner_radius: 3.0,
fill_color: None,
outline: Some(ui.style().thin_outline),
});
}
if let Some(corner_interact) = corner_interact {
paint_resize_corner(ui, &corner_interact);

View file

@ -165,19 +165,19 @@ impl ScrollArea {
let style = outer_ui.style();
let handle_fill_color = style.interact(&handle_interact).fill_color;
let handle_outline = style.interact(&handle_interact).outline;
let handle_outline = style.interact(&handle_interact).rect_outline;
outer_ui.add_paint_cmd(PaintCmd::Rect {
rect: outer_scroll_rect,
corner_radius,
fill_color: Some(color::gray(0, 196)), // TODO style
fill_color: Some(outer_ui.style().dark_bg_color),
outline: None,
});
outer_ui.add_paint_cmd(PaintCmd::Rect {
rect: handle_rect.expand(-2.0),
corner_radius,
fill_color: handle_fill_color,
fill_color: Some(handle_fill_color),
outline: handle_outline,
});
}

View file

@ -28,11 +28,12 @@ impl<'open> Window<'open> {
area,
frame: None,
resize: Resize::default()
.handle_offset(Vec2::splat(4.0))
.auto_shrink_width(true)
.auto_expand_height(false)
.auto_expand_width(true)
.auto_shrink_height(false)
.auto_expand_height(false),
.auto_shrink_width(true)
.handle_offset(Vec2::splat(4.0))
.outline(false),
scroll: Some(
ScrollArea::default()
.always_show_scroll(false)
@ -137,28 +138,89 @@ impl<'open> Window<'open> {
let frame = frame.unwrap_or_else(|| Frame::window(&ctx.style()));
// TODO: easier way to compose these
area.show(ctx, |ui| {
frame.show(ui, |ui| {
resize.show(ui, |ui| {
ui.horizontal(|ui| {
// TODO: prettier close button, and to the right of the window
if let Some(open) = open {
if ui.add(Button::new("X")).clicked {
*open = false;
}
if true {
// TODO: easier way to compose these
area.show(ctx, |ui| {
frame.show(ui, |ui| {
resize.show(ui, |ui| {
show_title_bar(ui, title_label, open);
if let Some(scroll) = scroll {
scroll.show(ui, add_contents)
} else {
add_contents(ui)
}
ui.add(title_label);
});
ui.add(Separator::new().line_width(1.0)); // TODO: nicer way to split window title from contents
if let Some(scroll) = scroll {
scroll.show(ui, add_contents)
} else {
add_contents(ui)
}
})
})
})
})
} else {
// TODO: something like this, with collapsing contents
area.show(ctx, |ui| {
frame.show(ui, |ui| {
CollapsingHeader::new(title_label.text()).show(ui, |ui| {
resize.show(ui, |ui| {
if let Some(scroll) = scroll {
scroll.show(ui, add_contents)
} else {
add_contents(ui)
}
})
});
})
})
}
}
}
fn show_title_bar(ui: &mut Ui, title_label: Label, open: Option<&mut bool>) {
let button_size = ui.style().clickable_diameter;
// TODO: show collapse button
let title_rect = ui.add(title_label).rect;
if let Some(open) = open {
let close_max_x = title_rect.right() + ui.style().item_spacing.x + button_size;
let close_max_x = close_max_x.max(ui.rect_finite().right());
let close_rect = Rect::from_min_size(
pos2(
close_max_x - button_size,
title_rect.center().y - 0.5 * button_size,
),
Vec2::splat(button_size),
);
if close_button(ui, close_rect).clicked {
*open = false;
}
}
ui.add(Separator::new().line_width(1.0)); // TODO: nicer way to split window title from contents
}
fn close_button(ui: &mut Ui, rect: Rect) -> InteractInfo {
let close_id = ui.make_child_id("window_close_button");
let interact = ui.interact_rect(rect, close_id);
ui.expand_to_include_child(interact.rect);
// ui.add_paint_cmd(PaintCmd::Rect {
// corner_radius: ui.style().interact(&interact).corner_radius,
// fill_color: ui.style().interact(&interact).bg_fill_color,
// outline: ui.style().interact(&interact).rect_outline,
// rect: interact.rect,
// });
let rect = rect.expand(-4.0);
let stroke_color = ui.style().interact(&interact).stroke_color;
let stroke_width = ui.style().interact(&interact).stroke_width;
ui.add_paint_cmd(PaintCmd::line_segment(
[rect.left_top(), rect.right_bottom()],
stroke_color,
stroke_width,
));
ui.add_paint_cmd(PaintCmd::line_segment(
[rect.right_top(), rect.left_bottom()],
stroke_color,
stroke_width,
));
interact
}

View file

@ -196,6 +196,7 @@ impl Context {
fn paint(&self) -> PaintBatches {
let mut mesher_options = *self.mesher_options.lock();
mesher_options.aa_size = 1.0 / self.pixels_per_point();
mesher_options.aa_size *= 1.5; // Looks better, but TODO: should not be needed
let paint_commands = self.drain_paint_lists();
let num_primitives = paint_commands.len();
let batches = mesher::mesh_paint_commands(mesher_options, self.fonts(), paint_commands);
@ -429,7 +430,7 @@ impl Context {
text_style: TextStyle,
color: Option<Color>,
) {
let color = color.unwrap_or_else(|| self.style().text_color());
let color = color.unwrap_or_else(|| self.style().text_color);
self.add_paint_cmd(
layer,
PaintCmd::Text {

View file

@ -18,7 +18,6 @@ pub struct ExampleApp {
impl ExampleApp {
pub fn ui(&mut self, ui: &mut Ui) {
show_menu_bar(ui, &mut self.open_windows);
ui.add(Separator::new());
self.windows(ui.ctx());
}
@ -279,7 +278,8 @@ struct Widgets {
count: usize,
radio: usize,
slider_value: usize,
text_inputs: [String; 3],
single_line_text_input: String,
multiline_text_input: String,
}
impl Default for Widgets {
@ -289,7 +289,8 @@ impl Default for Widgets {
radio: 0,
count: 0,
slider_value: 100,
text_inputs: ["Hello".to_string(), "World".to_string(), "".to_string()],
single_line_text_input: "Hello World!".to_owned(),
multiline_text_input: "Text can both be so wide that it needs a linebreak, but you can also add manual linebreak by pressing enter, creating new paragraphs.\nThis is the start of the next paragraph.\n\nClick me to edit me!".to_owned(),
}
}
}
@ -318,7 +319,7 @@ impl Widgets {
}
});
ui.horizontal(|ui| {
ui.inner_layout(Layout::horizontal(Align::Center), |ui| {
if ui
.add(Button::new("Click me"))
.tooltip_text("This will just increase a counter.")
@ -334,12 +335,17 @@ impl Widgets {
self.slider_value *= 2;
}
for (i, text) in self.text_inputs.iter_mut().enumerate() {
ui.horizontal(|ui| {
ui.add(label!("Text input {}: ", i));
ui.add(TextEdit::new(text).id(i));
}); // TODO: .tooltip_text("Enter text to edit me")
}
ui.horizontal(|ui| {
ui.add(label!("Single line text input:"));
ui.add(
TextEdit::new(&mut self.single_line_text_input)
.multiline(false)
.id("single line"),
);
}); // TODO: .tooltip_text("Enter text to edit me")
ui.add(label!("Multiline text input:"));
ui.add(TextEdit::new(&mut self.multiline_text_input).id("multiline"));
}
}
@ -412,46 +418,42 @@ impl Painting {
self.lines.clear();
}
ui.add_custom_contents(vec2(f32::INFINITY, 200.0), |ui| {
let interact = ui.reserve_space(ui.available_finite().size(), Some(ui.id()));
let rect = interact.rect;
ui.set_clip_rect(ui.clip_rect().intersect(rect)); // Make sure we don't paint out of bounds
Resize::default()
.default_height(200.0)
.show(ui, |ui| self.content(ui));
}
if self.lines.is_empty() {
self.lines.push(vec![]);
}
fn content(&mut self, ui: &mut Ui) {
let interact = ui.reserve_space(ui.available_finite().size(), Some(ui.id()));
let rect = interact.rect;
ui.set_clip_rect(ui.clip_rect().intersect(rect)); // Make sure we don't paint out of bounds
let current_line = self.lines.last_mut().unwrap();
if self.lines.is_empty() {
self.lines.push(vec![]);
}
if interact.active {
if let Some(mouse_pos) = ui.input().mouse_pos {
let canvas_pos = mouse_pos - rect.min;
if current_line.last() != Some(&canvas_pos) {
current_line.push(canvas_pos);
}
}
} else if !current_line.is_empty() {
self.lines.push(vec![]);
}
let current_line = self.lines.last_mut().unwrap();
for line in &self.lines {
if line.len() >= 2 {
ui.add_paint_cmd(PaintCmd::LinePath {
points: line.iter().map(|p| rect.min + *p).collect(),
color: LIGHT_GRAY,
width: 2.0,
});
if interact.active {
if let Some(mouse_pos) = ui.input().mouse_pos {
let canvas_pos = mouse_pos - rect.min;
if current_line.last() != Some(&canvas_pos) {
current_line.push(canvas_pos);
}
}
} else if !current_line.is_empty() {
self.lines.push(vec![]);
}
// Frame it:
ui.add_paint_cmd(PaintCmd::Rect {
rect: ui.rect(),
corner_radius: 0.0,
fill_color: None,
outline: Some(Outline::new(1.0, WHITE)),
});
});
for line in &self.lines {
if line.len() >= 2 {
ui.add_paint_cmd(PaintCmd::LinePath {
points: line.iter().map(|p| rect.min + *p).collect(),
color: LIGHT_GRAY,
width: 2.0,
});
}
}
}
}

View file

@ -30,9 +30,9 @@ pub type FontDefinitions = BTreeMap<TextStyle, (FontFamily, f32)>;
pub fn default_font_definitions() -> FontDefinitions {
let mut definitions = FontDefinitions::new();
definitions.insert(TextStyle::Body, (FontFamily::VariableWidth, 16.0));
definitions.insert(TextStyle::Button, (FontFamily::VariableWidth, 18.0));
definitions.insert(TextStyle::Heading, (FontFamily::VariableWidth, 28.0));
definitions.insert(TextStyle::Body, (FontFamily::VariableWidth, 14.0));
definitions.insert(TextStyle::Button, (FontFamily::VariableWidth, 16.0));
definitions.insert(TextStyle::Heading, (FontFamily::VariableWidth, 24.0));
definitions.insert(TextStyle::Monospace, (FontFamily::Monospace, 13.0));
definitions
}

View file

@ -143,6 +143,12 @@ pub struct PathPoint {
pub struct Path(Vec<PathPoint>);
impl Path {
pub fn from_point_loop(points: &[Pos2]) -> Self {
let mut path = Self::default();
path.add_line_loop(points);
path
}
pub fn clear(&mut self) {
self.0.clear();
}
@ -153,7 +159,8 @@ impl Path {
}
pub fn add_circle(&mut self, center: Pos2, radius: f32) {
let n = 32; // TODO: parameter
let n = (radius * 2.0).round() as i32; // TODO: tweak a bit more
let n = clamp(n, 4..=64);
for i in 0..n {
let angle = remap(i as f32, 0.0..=n as f32, 0.0..=TAU);
let normal = vec2(angle.cos(), angle.sin());
@ -167,6 +174,7 @@ impl Path {
self.add_point(points[1], normal);
}
// TODO: make it clear it is an open (non-closed) thing.
pub fn add_line(&mut self, points: &[Pos2]) {
let n = points.len();
assert!(n >= 2);
@ -177,8 +185,8 @@ impl Path {
} else {
self.add_point(points[0], (points[1] - points[0]).normalized().rot90());
for i in 1..n - 1 {
let n0 = (points[i] - points[i - 1]).normalized().rot90();
let n1 = (points[i + 1] - points[i]).normalized().rot90();
let n0 = (points[i] - points[i - 1]).normalized().rot90(); // TODO: don't calculate each normal twice!
let n1 = (points[i + 1] - points[i]).normalized().rot90(); // TODO: don't calculate each normal twice!
let v = (n0 + n1) / 2.0;
let normal = v / v.length_sq();
self.add_point(points[i], normal); // TODO: handle VERY sharp turns better
@ -190,6 +198,20 @@ impl Path {
}
}
pub fn add_line_loop(&mut self, points: &[Pos2]) {
let n = points.len();
assert!(n >= 2);
// TODO: optimize
for i in 0..n {
let n0 = (points[i] - points[(i + n - 1) % n]).normalized().rot90();
let n1 = (points[(i + 1) % n] - points[i]).normalized().rot90();
let v = (n0 + n1) / 2.0;
let normal = v / v.length_sq();
self.add_point(points[i], normal); // TODO: handle VERY sharp turns better
}
}
pub fn add_rectangle(&mut self, rect: Rect) {
let min = rect.min;
let max = rect.max;
@ -229,7 +251,8 @@ impl Path {
/// quadrant 3 up rigth
/// 4 * TAU / 4 = right
pub fn add_circle_quadrant(&mut self, center: Pos2, radius: f32, quadrant: f32) {
let n = 8;
let n = (radius * 0.5).round() as i32; // TODO: tweak a bit more
let n = clamp(n, 2..=32);
const RIGHT_ANGLE: f32 = TAU / 4.0;
for i in 0..=n {
let angle = remap(
@ -570,6 +593,9 @@ pub fn mesh_command(
color,
} => {
galley.sanity_check();
let text_offset = vec2(0.0, 1.0); // Eye-balled for buttons. TODO: why is this needed?
let font = &fonts[text_style];
let mut chars = galley.text.chars();
for line in &galley.lines {
@ -577,7 +603,7 @@ pub fn mesh_command(
let c = chars.next().unwrap();
if let Some(glyph) = font.uv_rect(c) {
let mut top_left = Vertex {
pos: pos + glyph.offset + vec2(*x_offset, line.y_min),
pos: pos + glyph.offset + vec2(*x_offset, line.y_min) + text_offset,
uv: glyph.min,
color,
};

View file

@ -4,6 +4,7 @@ use serde_derive::{Deserialize, Serialize};
use crate::{color::*, math::*, types::*};
// TODO: split into Spacing and Style?
#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
pub struct Style {
/// Horizontal and vertical padding within a window frame.
@ -25,13 +26,24 @@ pub struct Style {
/// The text starts after this many pixels.
pub start_icon_width: f32,
pub interact: Interact,
// -----------------------------------------------
// Purely visual:
pub interact: Interact,
// TODO: an WidgetStyle ?
pub text_color: Color,
/// For stuff like check marks in check boxes.
pub line_width: f32,
pub thin_outline: Outline,
/// e.g. the background of windows
pub background_fill_color: Color,
/// e.g. the background of the slider or text edit
pub dark_bg_color: Color,
pub cursor_blink_hz: f32,
pub text_cursor_width: f32,
@ -59,12 +71,16 @@ impl Default for Style {
item_spacing: vec2(8.0, 4.0),
indent: 21.0,
clickable_diameter: 22.0,
start_icon_width: 16.0,
start_icon_width: 14.0,
interact: Default::default(),
text_color: gray(160, 255),
line_width: 1.0,
thin_outline: Outline::new(0.5, GRAY),
background_fill_color: gray(32, 250),
dark_bg_color: gray(0, 140),
cursor_blink_hz: 1.0,
text_cursor_width: 2.0,
animation_time: 1.0 / 20.0,
animation_time: 1.0 / 15.0,
window: Window::default(),
menu_bar: MenuBar::default(),
clip_rect_margin: 3.0,
@ -84,24 +100,58 @@ impl Default for Interact {
fn default() -> Self {
Self {
active: WidgetStyle {
fill_color: Some(srgba(120, 120, 200, 255)),
bg_fill_color: None,
fill_color: srgba(120, 120, 200, 255),
stroke_color: WHITE,
stroke_width: 2.0,
outline: Some(Outline::new(2.0, WHITE)),
rect_outline: Some(Outline::new(1.0, WHITE)),
corner_radius: 5.0,
},
hovered: WidgetStyle {
fill_color: Some(srgba(100, 100, 150, 255)),
bg_fill_color: None,
fill_color: srgba(100, 100, 150, 255),
stroke_color: WHITE,
stroke_width: 1.5,
outline: None,
rect_outline: Some(Outline::new(1.0, WHITE)),
corner_radius: 5.0,
},
inactive: WidgetStyle {
fill_color: Some(srgba(60, 60, 80, 255)),
bg_fill_color: None,
fill_color: srgba(60, 60, 80, 255),
stroke_color: gray(210, 255), // Mustn't look grayed out!
stroke_width: 1.0,
rect_outline: Some(Outline::new(0.5, WHITE)),
corner_radius: 0.0,
},
}
}
}
impl Interact {
pub fn classic() -> Self {
Self {
active: WidgetStyle {
bg_fill_color: Some(srgba(120, 120, 200, 255)),
fill_color: srgba(120, 120, 200, 255),
stroke_color: WHITE,
stroke_width: 2.0,
rect_outline: Some(Outline::new(2.0, WHITE)),
corner_radius: 5.0,
},
hovered: WidgetStyle {
bg_fill_color: Some(srgba(100, 100, 150, 255)),
fill_color: srgba(100, 100, 150, 255),
stroke_color: WHITE,
stroke_width: 1.5,
rect_outline: None,
corner_radius: 5.0,
},
inactive: WidgetStyle {
bg_fill_color: Some(srgba(60, 60, 80, 255)),
fill_color: srgba(60, 60, 80, 255),
stroke_color: gray(220, 255), // Mustn't look grayed out!
stroke_width: 1.0,
outline: None,
rect_outline: None,
corner_radius: 0.0,
},
}
@ -122,8 +172,12 @@ impl Interact {
#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
pub struct WidgetStyle {
/// Fill color of the interactive part of a component (button, slider grab, checkbox, ...)
pub fill_color: Option<Color>,
/// Background color of widget
pub bg_fill_color: Option<Color>,
/// Fill color of the interactive part of a component (slider grab, checkbox, ...)
/// When you need a fill_color.
pub fill_color: Color,
/// Stroke and text color of the interactive part of a component (button, slider grab, checkbox, ...)
pub stroke_color: Color,
@ -131,8 +185,9 @@ pub struct WidgetStyle {
/// For lines etc
pub stroke_width: f32,
/// For rectangles
pub outline: Option<Outline>,
/// For surrounding rectangle of things that need it,
/// like buttons, the box of the checkbox, etc.
pub rect_outline: Option<Outline>,
/// Button frames etdc
pub corner_radius: f32,
@ -163,15 +218,6 @@ impl Default for MenuBar {
}
impl Style {
/// e.g. the background of the slider
pub fn background_fill_color(&self) -> Color {
gray(34, 250)
}
pub fn text_color(&self) -> Color {
gray(255, 200)
}
/// Use this style for interactive things
pub fn interact(&self, interact: &InteractInfo) -> &WidgetStyle {
self.interact.style(interact)
@ -185,7 +231,9 @@ impl Style {
vec2(box_side, box_side),
);
let small_icon_rect = Rect::from_center_size(big_icon_rect.center(), vec2(10.0, 10.0));
let small_rect_side = 8.0; // TODO: make a parameter
let small_icon_rect =
Rect::from_center_size(big_icon_rect.center(), Vec2::splat(small_rect_side));
(small_icon_rect, big_icon_rect)
}

View file

@ -135,6 +135,7 @@ pub enum PaintCmd {
color: Color,
width: f32,
},
// TODO: remove. Just have Path.
LinePath {
points: Vec<Pos2>,
color: Color,

View file

@ -473,7 +473,7 @@ impl Ui {
text_style: TextStyle,
color: Option<Color>,
) {
let color = color.unwrap_or_else(|| self.style().text_color());
let color = color.unwrap_or_else(|| self.style().text_color);
self.add_paint_cmd(PaintCmd::Text {
pos,
galley,

View file

@ -226,13 +226,12 @@ impl Widget for Button {
let mut size = galley.size + 2.0 * padding;
size.y = size.y.max(ui.style().clickable_diameter);
let interact = ui.reserve_space(size, Some(id));
let mut text_cursor = interact.rect.left_center() + vec2(padding.x, -0.5 * galley.size.y);
text_cursor.y += 2.0; // TODO: why is this needed?
let fill_color = fill_color.or(ui.style().interact(&interact).fill_color);
let text_cursor = interact.rect.left_center() + vec2(padding.x, -0.5 * galley.size.y);
let bg_fill_color = fill_color.or(ui.style().interact(&interact).bg_fill_color);
ui.add_paint_cmd(PaintCmd::Rect {
corner_radius: ui.style().interact(&interact).corner_radius,
fill_color: fill_color,
outline: ui.style().interact(&interact).outline,
fill_color: bg_fill_color,
outline: ui.style().interact(&interact).rect_outline,
rect: interact.rect,
});
let stroke_color = ui.style().interact(&interact).stroke_color;
@ -286,9 +285,9 @@ impl<'a> Widget for Checkbox<'a> {
}
let (small_icon_rect, big_icon_rect) = ui.style().icon_rectangles(interact.rect);
ui.add_paint_cmd(PaintCmd::Rect {
corner_radius: 3.0,
fill_color: ui.style().interact(&interact).fill_color,
outline: None,
corner_radius: ui.style().interact(&interact).corner_radius,
fill_color: ui.style().interact(&interact).bg_fill_color,
outline: ui.style().interact(&interact).rect_outline,
rect: big_icon_rect,
});
@ -356,15 +355,15 @@ impl Widget for RadioButton {
let text_cursor =
interact.rect.min + ui.style().button_padding + vec2(ui.style().start_icon_width, 0.0);
let fill_color = ui.style().interact(&interact).fill_color;
let bg_fill_color = ui.style().interact(&interact).bg_fill_color;
let stroke_color = ui.style().interact(&interact).stroke_color;
let (small_icon_rect, big_icon_rect) = ui.style().icon_rectangles(interact.rect);
ui.add_paint_cmd(PaintCmd::Circle {
center: big_icon_rect.center(),
fill_color,
outline: None,
fill_color: bg_fill_color,
outline: ui.style().interact(&interact).rect_outline, // TODO
radius: big_icon_rect.width() / 2.0,
});
@ -373,7 +372,7 @@ impl Widget for RadioButton {
center: small_icon_rect.center(),
fill_color: Some(stroke_color),
outline: None,
radius: small_icon_rect.width() / 2.0,
radius: small_icon_rect.width() / 3.0,
});
}
@ -386,7 +385,7 @@ impl Widget for RadioButton {
// ----------------------------------------------------------------------------
pub struct Separator {
line_width: f32,
line_width: Option<f32>,
min_spacing: f32,
extra: f32,
color: Color,
@ -395,7 +394,7 @@ pub struct Separator {
impl Separator {
pub fn new() -> Self {
Self {
line_width: 2.0,
line_width: None,
min_spacing: 6.0,
extra: 0.0,
color: color::WHITE,
@ -403,7 +402,7 @@ impl Separator {
}
pub fn line_width(mut self, line_width: f32) -> Self {
self.line_width = line_width;
self.line_width = Some(line_width);
self
}
@ -426,26 +425,36 @@ impl Separator {
impl Widget for Separator {
fn ui(self, ui: &mut Ui) -> GuiResponse {
let Separator {
line_width,
min_spacing,
extra,
color,
} = self;
let line_width = line_width.unwrap_or_else(|| ui.style().line_width);
let available_space = ui.available_finite().size();
let extra = self.extra;
let (points, interact) = match ui.layout().dir() {
Direction::Horizontal => {
let interact = ui.reserve_space(vec2(self.min_spacing, available_space.y), None);
let interact = ui.reserve_space(vec2(min_spacing, available_space.y), None);
let r = &interact.rect;
(
[
pos2(interact.rect.center().x, interact.rect.top() - extra),
pos2(interact.rect.center().x, interact.rect.bottom() + extra),
pos2(r.center().x, r.top() - extra),
pos2(r.center().x, r.bottom() + extra),
],
interact,
)
}
Direction::Vertical => {
let interact = ui.reserve_space(vec2(available_space.x, self.min_spacing), None);
let interact = ui.reserve_space(vec2(available_space.x, min_spacing), None);
let r = &interact.rect;
(
[
pos2(interact.rect.left() - extra, interact.rect.center().y),
pos2(interact.rect.right() + extra, interact.rect.center().y),
pos2(r.left() - extra, r.center().y),
pos2(r.right() + extra, r.center().y),
],
interact,
)
@ -453,8 +462,8 @@ impl Widget for Separator {
};
ui.add_paint_cmd(PaintCmd::LineSegment {
points,
color: self.color,
width: self.line_width,
color: color,
width: line_width,
});
ui.response(interact)
}

View file

@ -176,14 +176,14 @@ impl<'a> Widget for Slider<'a> {
ui.add_paint_cmd(PaintCmd::Rect {
rect: rail_rect,
corner_radius: rail_radius,
fill_color: Some(ui.style().background_fill_color()),
fill_color: Some(ui.style().background_fill_color),
outline: Some(Outline::new(1.0, color::gray(200, 255))), // TODO
});
ui.add_paint_cmd(PaintCmd::Circle {
center: pos2(marker_center_x, rail_rect.center().y),
radius: handle_radius,
fill_color: ui.style().interact(&interact).fill_color,
fill_color: Some(ui.style().interact(&interact).fill_color),
outline: Some(Outline::new(
ui.style().interact(&interact).stroke_width,
ui.style().interact(&interact).stroke_color,

View file

@ -13,6 +13,7 @@ pub struct TextEdit<'t> {
id: Option<Id>,
text_style: TextStyle, // TODO: Option<TextStyle>, where None means "use the default for the current Ui"
text_color: Option<Color>,
multiline: bool,
}
impl<'t> TextEdit<'t> {
@ -22,6 +23,7 @@ impl<'t> TextEdit<'t> {
id: None,
text_style: TextStyle::Body,
text_color: Default::default(),
multiline: true,
}
}
@ -39,6 +41,11 @@ impl<'t> TextEdit<'t> {
self.text_color = Some(text_color);
self
}
pub fn multiline(mut self, multiline: bool) -> Self {
self.multiline = multiline;
self
}
}
impl<'t> Widget for TextEdit<'t> {
@ -48,6 +55,7 @@ impl<'t> Widget for TextEdit<'t> {
id,
text_style,
text_color,
multiline,
} = self;
let id = ui.make_child_id(id);
@ -57,7 +65,11 @@ impl<'t> Widget for TextEdit<'t> {
let font = &ui.fonts()[text_style];
let line_spacing = font.line_spacing();
let available_width = ui.available().width();
let mut galley = font.layout_multiline(text.as_str(), available_width);
let mut galley = if multiline {
font.layout_multiline(text.as_str(), available_width)
} else {
font.layout_single_line(text.as_str())
};
let desired_size = galley.size.max(vec2(available_width, line_spacing));
let interact = ui.reserve_space(desired_size, Some(id));
@ -95,19 +107,24 @@ impl<'t> Widget for TextEdit<'t> {
// layout again to avoid frame delay:
let font = &ui.fonts()[text_style];
galley = font.layout_multiline(text.as_str(), available_width);
galley = if multiline {
font.layout_multiline(text.as_str(), available_width)
} else {
font.layout_single_line(text.as_str())
};
// dbg!(&galley);
}
ui.add_paint_cmd(PaintCmd::Rect {
rect: interact.rect,
corner_radius: 0.0,
// fill_color: Some(color::BLACK),
fill_color: ui.style().interact(&interact).fill_color,
// fill_color: Some(ui.style().background_fill_color()),
outline: None, //Some(Outline::new(1.0, color::WHITE)),
});
{
let bg_rect = interact.rect.expand(2.0); // breathing room for content
ui.add_paint_cmd(PaintCmd::Rect {
rect: bg_rect,
corner_radius: ui.style().interact.style(&interact).corner_radius,
fill_color: Some(ui.style().dark_bg_color),
outline: ui.style().interact.style(&interact).rect_outline,
});
}
if has_kb_focus {
let cursor_blink_hz = ui.style().cursor_blink_hz;