diff --git a/emigui/src/color.rs b/emigui/src/color.rs index c3c673a4..5b97eef3 100644 --- a/emigui/src/color.rs +++ b/emigui/src/color.rs @@ -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); diff --git a/emigui/src/containers/collapsing_header.rs b/emigui/src/containers/collapsing_header.rs index 825744c3..79ab529c 100644 --- a/emigui/src/containers/collapsing_header.rs +++ b/emigui/src/containers/collapsing_header.rs @@ -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)), + }); } diff --git a/emigui/src/containers/frame.rs b/emigui/src/containers/frame.rs index b2d21bd0..76c0ce75 100644 --- a/emigui/src/containers/frame.rs +++ b/emigui/src/containers/frame.rs @@ -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))), } } diff --git a/emigui/src/containers/menu.rs b/emigui/src/containers/menu.rs index ff8a8c4f..8641f022 100644 --- a/emigui/src/containers/menu.rs +++ b/emigui/src/containers/menu.rs @@ -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, 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, 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) diff --git a/emigui/src/containers/resize.rs b/emigui/src/containers/resize.rs index 354569f4..64aa5385 100644 --- a/emigui/src/containers/resize.rs +++ b/emigui/src/containers/resize.rs @@ -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); diff --git a/emigui/src/containers/scroll_area.rs b/emigui/src/containers/scroll_area.rs index 65e7f734..9078f0a6 100644 --- a/emigui/src/containers/scroll_area.rs +++ b/emigui/src/containers/scroll_area.rs @@ -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, }); } diff --git a/emigui/src/containers/window.rs b/emigui/src/containers/window.rs index 131fd3f1..402e9758 100644 --- a/emigui/src/containers/window.rs +++ b/emigui/src/containers/window.rs @@ -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 +} diff --git a/emigui/src/context.rs b/emigui/src/context.rs index 2dad6dda..fbc69b0c 100644 --- a/emigui/src/context.rs +++ b/emigui/src/context.rs @@ -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, ) { - 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 { diff --git a/emigui/src/examples/app.rs b/emigui/src/examples/app.rs index 7f69f42d..2f838bb2 100644 --- a/emigui/src/examples/app.rs +++ b/emigui/src/examples/app.rs @@ -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, + }); + } + } } } diff --git a/emigui/src/fonts.rs b/emigui/src/fonts.rs index 9b87f5b7..2002edb5 100644 --- a/emigui/src/fonts.rs +++ b/emigui/src/fonts.rs @@ -30,9 +30,9 @@ pub type FontDefinitions = BTreeMap; 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 } diff --git a/emigui/src/mesher.rs b/emigui/src/mesher.rs index 06c594a1..f82d78a1 100644 --- a/emigui/src/mesher.rs +++ b/emigui/src/mesher.rs @@ -143,6 +143,12 @@ pub struct PathPoint { pub struct Path(Vec); 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, }; diff --git a/emigui/src/style.rs b/emigui/src/style.rs index 294f5a9b..b85593c2 100644 --- a/emigui/src/style.rs +++ b/emigui/src/style.rs @@ -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, + /// Background color of widget + pub bg_fill_color: Option, + + /// 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, + /// For surrounding rectangle of things that need it, + /// like buttons, the box of the checkbox, etc. + pub rect_outline: Option, /// 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) } diff --git a/emigui/src/types.rs b/emigui/src/types.rs index 136fa3de..2547f3bd 100644 --- a/emigui/src/types.rs +++ b/emigui/src/types.rs @@ -135,6 +135,7 @@ pub enum PaintCmd { color: Color, width: f32, }, + // TODO: remove. Just have Path. LinePath { points: Vec, color: Color, diff --git a/emigui/src/ui.rs b/emigui/src/ui.rs index 0b63d3f4..adb52103 100644 --- a/emigui/src/ui.rs +++ b/emigui/src/ui.rs @@ -473,7 +473,7 @@ impl Ui { text_style: TextStyle, color: Option, ) { - 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, diff --git a/emigui/src/widgets.rs b/emigui/src/widgets.rs index 1c8386ba..148f1d0e 100644 --- a/emigui/src/widgets.rs +++ b/emigui/src/widgets.rs @@ -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, 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) } diff --git a/emigui/src/widgets/slider.rs b/emigui/src/widgets/slider.rs index 0c606a50..419dccad 100644 --- a/emigui/src/widgets/slider.rs +++ b/emigui/src/widgets/slider.rs @@ -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, diff --git a/emigui/src/widgets/text_edit.rs b/emigui/src/widgets/text_edit.rs index 0aaca9b6..d07e9912 100644 --- a/emigui/src/widgets/text_edit.rs +++ b/emigui/src/widgets/text_edit.rs @@ -13,6 +13,7 @@ pub struct TextEdit<'t> { id: Option, text_style: TextStyle, // TODO: Option, where None means "use the default for the current Ui" text_color: Option, + 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;