diff --git a/emigui/src/containers/area.rs b/emigui/src/containers/area.rs index 4b220fcb8..b61beac73 100644 --- a/emigui/src/containers/area.rs +++ b/emigui/src/containers/area.rs @@ -91,15 +91,15 @@ impl Area { } } -struct Prepared { +pub(crate) struct Prepared { layer: Layer, - state: State, + pub(crate) state: State, movable: bool, - content_ui: Ui, + pub(crate) content_ui: Ui, } impl Area { - fn prepare(self, ctx: &Arc) -> Prepared { + pub(crate) fn begin(self, ctx: &Arc) -> Prepared { let Area { id, movable, @@ -138,18 +138,20 @@ impl Area { } pub fn show(self, ctx: &Arc, add_contents: impl FnOnce(&mut Ui)) -> InteractInfo { - let mut prepared = self.prepare(ctx); + let mut prepared = self.begin(ctx); add_contents(&mut prepared.content_ui); - Self::finish(ctx, prepared) + prepared.end(ctx) } +} - fn finish(ctx: &Arc, prepared: Prepared) -> InteractInfo { +impl Prepared { + pub(crate) fn end(self, ctx: &Arc) -> InteractInfo { let Prepared { layer, mut state, movable, content_ui, - } = prepared; + } = self; state.size = (content_ui.child_bounds().max - state.pos).ceil(); diff --git a/emigui/src/containers/collapsing_header.rs b/emigui/src/containers/collapsing_header.rs index f9af3e61e..c756c9a6f 100644 --- a/emigui/src/containers/collapsing_header.rs +++ b/emigui/src/containers/collapsing_header.rs @@ -168,7 +168,7 @@ struct Prepared { } impl CollapsingHeader { - fn prepare(self, ui: &mut Ui) -> Prepared { + fn begin(self, ui: &mut Ui) -> Prepared { assert!( ui.layout().dir() == Direction::Vertical, "Horizontal collapsing is unimplemented" @@ -185,7 +185,7 @@ impl CollapsingHeader { let available = ui.available_finite(); let text_pos = available.min + vec2(ui.style().indent, 0.0); - let galley = label.layout(available.width() - ui.style().indent, ui); + let galley = label.layout_width(ui, available.width() - ui.style().indent); let text_max_x = text_pos.x + galley.size.x; let desired_width = text_max_x - available.left(); let desired_width = desired_width.max(available.width()); @@ -240,7 +240,7 @@ impl CollapsingHeader { } pub fn show(self, ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) -> Option { - let Prepared { id, mut state } = self.prepare(ui); + let Prepared { id, mut state } = self.begin(ui); let r_interact = state.add_contents(ui, |ui| ui.indent(id, add_contents).0); let ret = r_interact.map(|ri| ri.0); ui.memory().collapsing_headers.insert(id, state); diff --git a/emigui/src/containers/frame.rs b/emigui/src/containers/frame.rs index 136b67146..bdae70093 100644 --- a/emigui/src/containers/frame.rs +++ b/emigui/src/containers/frame.rs @@ -17,7 +17,7 @@ impl Frame { margin: style.window_padding, corner_radius: style.window.corner_radius, fill_color: Some(style.background_fill_color), - outline: Some(Outline::new(1.0, color::WHITE)), + outline: style.interact.inactive.rect_outline, // becauce we can resize windows } } @@ -59,30 +59,58 @@ impl Frame { } } +pub struct Prepared { + pub frame: Frame, + outer_rect_bounds: Rect, + where_to_put_background: usize, + pub content_ui: Ui, +} + impl Frame { + pub fn begin(self, ui: &mut Ui) -> Prepared { + let outer_rect_bounds = ui.available(); + let inner_rect = outer_rect_bounds.shrink2(self.margin); + let where_to_put_background = ui.paint_list_len(); + let content_ui = ui.child_ui(inner_rect); + Prepared { + frame: self, + outer_rect_bounds, + where_to_put_background, + content_ui, + } + } + pub fn show(self, ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) -> R { - let Frame { - margin, - corner_radius, - fill_color, - outline, - } = self; + let mut prepared = self.begin(ui); + let ret = add_contents(&mut prepared.content_ui); + prepared.end(ui); + ret + } +} - let outer_rect = ui.available(); - let inner_rect = outer_rect.shrink2(margin); - let where_to_put_background = ui.paint_list_len(); +impl Prepared { + pub fn outer_rect(&self) -> Rect { + Rect::from_min_max( + self.outer_rect_bounds.min, + self.content_ui.child_bounds().max + self.frame.margin, + ) + } - let mut child_ui = ui.child_ui(inner_rect); - let ret = add_contents(&mut child_ui); + pub fn end(self, ui: &mut Ui) -> Rect { + let outer_rect = self.outer_rect(); - let outer_rect = Rect::from_min_max(outer_rect.min, child_ui.child_bounds().max + margin); + let Prepared { + frame, + where_to_put_background, + .. + } = self; ui.insert_paint_cmd( where_to_put_background, PaintCmd::Rect { - corner_radius, - fill_color, - outline, + corner_radius: frame.corner_radius, + fill_color: frame.fill_color, + outline: frame.outline, rect: outer_rect, }, ); @@ -90,6 +118,6 @@ impl Frame { ui.expand_to_include_child(outer_rect); // TODO: move cursor in parent ui - ret + outer_rect } } diff --git a/emigui/src/containers/resize.rs b/emigui/src/containers/resize.rs index d8ea082c4..fd8051626 100644 --- a/emigui/src/containers/resize.rs +++ b/emigui/src/containers/resize.rs @@ -177,7 +177,7 @@ struct Prepared { } impl Resize { - fn prepare(&mut self, ui: &mut Ui) -> Prepared { + fn begin(&mut self, ui: &mut Ui) -> Prepared { let id = self.id.unwrap_or_else(|| ui.make_child_id("resize")); self.min_size = self.min_size.min(ui.available().size()); self.max_size = self.max_size.min(ui.available().size()); @@ -267,13 +267,13 @@ impl Resize { } pub fn show(mut self, ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) -> R { - let mut prepared = self.prepare(ui); + let mut prepared = self.begin(ui); let ret = add_contents(&mut prepared.content_ui); - self.finish(ui, prepared); + self.end(ui, prepared); ret } - fn finish(self, ui: &mut Ui, prepared: Prepared) { + fn end(self, ui: &mut Ui, prepared: Prepared) { let Prepared { id, mut state, diff --git a/emigui/src/containers/scroll_area.rs b/emigui/src/containers/scroll_area.rs index 1452cd91d..63cc63463 100644 --- a/emigui/src/containers/scroll_area.rs +++ b/emigui/src/containers/scroll_area.rs @@ -54,7 +54,7 @@ struct Prepared { } impl ScrollArea { - fn prepare(self, ui: &mut Ui) -> Prepared { + fn begin(self, ui: &mut Ui) -> Prepared { let Self { max_height, always_show_scroll, @@ -110,13 +110,15 @@ impl ScrollArea { } pub fn show(self, ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) -> R { - let mut prepared = self.prepare(ui); + let mut prepared = self.begin(ui); let ret = add_contents(&mut prepared.content_ui); - Self::finish(ui, prepared); + prepared.end(ui); ret } +} - fn finish(ui: &mut Ui, prepared: Prepared) { +impl Prepared { + fn end(self, ui: &mut Ui) { let Prepared { id, mut state, @@ -124,7 +126,7 @@ impl ScrollArea { always_show_scroll, current_scroll_bar_width, content_ui, - } = prepared; + } = self; let content_size = content_ui.bounding_size(); diff --git a/emigui/src/containers/window.rs b/emigui/src/containers/window.rs index 1a1da3186..a3cd7f3e0 100644 --- a/emigui/src/containers/window.rs +++ b/emigui/src/containers/window.rs @@ -148,22 +148,31 @@ impl<'open> Window<'open> { return None; } - let movable = area.is_movable(); - let area = area.movable(false); // We move it manually - let resizable = resize.is_resizable(); - let resize = resize.resizable(false); // We move it manually - let window_id = Id::new(title_label.text()); let area_layer = area.layer(); let resize_id = window_id.with("resize"); let collapsing_id = window_id.with("collapsing"); + let possible = PossibleInteractions { + movable: area.is_movable(), + resizable: resize.is_resizable() + && collapsing_header::State::is_open(ctx, collapsing_id).unwrap_or_default(), + }; + + let area = area.movable(false); // We move it manually + let resize = resize.resizable(false); // We move it manually + let resize = resize.id(resize_id); let frame = frame.unwrap_or_else(|| Frame::window(&ctx.style())); - let full_interact = area.show(ctx, |ui| { - frame.show(ui, |ui| { + let mut area = area.begin(ctx); + { + // TODO: pick style for frame and title based on interaction + let mut frame = frame.begin(&mut area.content_ui); + { + let ui = &mut frame.content_ui; + let default_expanded = true; let mut collapsing = collapsing_header::State::from_memory_with_default_open( ui, @@ -182,7 +191,7 @@ impl<'open> Window<'open> { let content = collapsing .add_contents(ui, |ui| { resize.show(ui, |ui| { - ui.add(Separator::new().line_width(1.0)); // TODO: nicer way to split window title from contents + ui.add(Separator::new().line_width(0.5)); // TODO: nicer way to split window title from contents if let Some(scroll) = scroll { scroll.show(ui, add_contents) } else { @@ -198,42 +207,49 @@ impl<'open> Window<'open> { if let Some(open) = open { // Add close button now that we know our full width: - - let right = content - .map(|c| c.rect.right()) - .unwrap_or(title_bar.rect.right()); - - let button_size = ui.style().start_icon_width; - let button_rect = Rect::from_min_size( - pos2( - right - ui.style().item_spacing.x - button_size, - title_bar.rect.center().y - 0.5 * button_size, - ), - Vec2::splat(button_size), - ); - - if close_button(ui, button_rect).clicked { + if title_bar.close_button_ui(ui, &content).clicked { *open = false; } } - }) - }); - - let resizable = - resizable && collapsing_header::State::is_open(ctx, collapsing_id).unwrap_or_default(); - - if movable || resizable { - let possible = PossibleInteractions { movable, resizable }; - - interact( - ctx, - possible, - area_layer, - window_id, - resize_id, - full_interact.rect, - ); + title_bar.title_ui(ui); + } + + let outer_rect = frame.end(&mut area.content_ui); + + let interaction = if possible.movable || possible.resizable { + interact( + ctx, + possible, + area_layer, + &mut area.state, + window_id, + resize_id, + outer_rect, + ) + } else { + None + }; + + if let Some(interaction) = interaction { + paint_frame_interaction( + &mut area.content_ui, + outer_rect, + interaction, + ctx.style().interact.active, + ); + } else { + if let Some(hover_interaction) = resize_hover(ctx, possible, area_layer, outer_rect) + { + paint_frame_interaction( + &mut area.content_ui, + outer_rect, + hover_interaction, + ctx.style().interact.hovered, + ); + } + } } + let full_interact = area.end(ctx); Some(full_interact) } @@ -280,10 +296,11 @@ fn interact( ctx: &Context, possible: PossibleInteractions, area_layer: Layer, + area_state: &mut area::State, window_id: Id, resize_id: Id, rect: Rect, -) -> Option<()> { +) -> Option { let pre_resize = ctx.round_rect_to_pixels(rect); let window_interaction = window_interaction( ctx, @@ -297,9 +314,7 @@ fn interact( let new_rect = ctx.round_rect_to_pixels(new_rect); // TODO: add this to a Window state instead as a command "move here next frame" - let mut area_state = ctx.memory().areas.get(area_layer.id).unwrap(); area_state.pos = new_rect.min; - ctx.memory().areas.set_state(area_layer, area_state); let mut resize_state = ctx.memory().resize.get(&resize_id).cloned().unwrap(); // resize_state.size += new_rect.size() - pre_resize.size(); @@ -308,7 +323,7 @@ fn interact( ctx.memory().resize.insert(resize_id, resize_state); ctx.memory().areas.move_to_top(area_layer); - Some(()) + Some(window_interaction) } fn resize_window(ctx: &Context, window_interaction: &WindowInteraction) -> Option { @@ -446,16 +461,69 @@ fn resize_hover( } } +/// Fill in parts of the window frame when we resize by dragging that part +fn paint_frame_interaction( + ui: &mut Ui, + rect: Rect, + interaction: WindowInteraction, + style: style::WidgetStyle, +) { + let cr = ui.style().window.corner_radius; + let Rect { min, max } = rect; + + let mut path = Path::default(); + + if interaction.right && !interaction.bottom && !interaction.top { + path.add_line(&[pos2(max.x, min.y + cr), pos2(max.x, max.y - cr)]); + } + if interaction.right && interaction.bottom { + path.add_line(&[pos2(max.x, min.y + cr), pos2(max.x, max.y - cr)]); + path.add_circle_quadrant(pos2(max.x - cr, max.y - cr), cr, 0.0); + } + if interaction.bottom { + path.add_line(&[pos2(max.x - cr, max.y), pos2(min.x + cr, max.y)]); + } + if interaction.left && interaction.bottom { + path.add_circle_quadrant(pos2(min.x + cr, max.y - cr), cr, 1.0); + } + if interaction.left { + path.add_line(&[pos2(min.x, max.y - cr), pos2(min.x, min.y + cr)]); + } + if interaction.left && interaction.top { + path.add_circle_quadrant(pos2(min.x + cr, min.y + cr), cr, 2.0); + } + if interaction.top { + path.add_line(&[pos2(min.x + cr, min.y), pos2(max.x - cr, min.y)]); + } + if interaction.right && interaction.top { + path.add_circle_quadrant(pos2(max.x - cr, min.y + cr), cr, 3.0); + path.add_line(&[pos2(max.x, min.y + cr), pos2(max.x, max.y - cr)]); + } + ui.add_paint_cmd(PaintCmd::Path { + path, + closed: false, + fill_color: None, + outline: style.rect_outline, + }); +} + // ---------------------------------------------------------------------------- +struct TitleBar { + title_label: Label, + title_galley: font::Galley, + title_rect: Rect, + rect: Rect, +} + fn show_title_bar( ui: &mut Ui, title_label: Label, show_close_button: bool, collapsing_id: Id, collapsing: &mut collapsing_header::State, -) -> InteractInfo { - ui.inner_layout(Layout::horizontal(Align::Center), |ui| { +) -> TitleBar { + let tb_interact = ui.inner_layout(Layout::horizontal(Align::Center), |ui| { ui.set_desired_height(title_label.font_height(ui)); let item_spacing = ui.style().item_spacing; @@ -474,7 +542,8 @@ fn show_title_bar( collapsing.paint_icon(ui, &collapse_button_interact); } - let title_rect = ui.add(title_label).rect; + let title_galley = title_label.layout(ui); + let title_rect = ui.reserve_space(title_galley.size, None).rect; if show_close_button { // Reserve space for close button which will be added later: @@ -489,8 +558,41 @@ fn show_title_bar( ); ui.expand_to_include_child(close_rect); } - }) - .1 + + TitleBar { + title_label, + title_galley, + title_rect, + rect: Default::default(), // Will be filled in later + } + }); + + TitleBar { + rect: tb_interact.1.rect, + ..tb_interact.0 + } +} + +impl TitleBar { + pub fn title_ui(self, ui: &mut Ui) { + self.title_label + .paint_galley(ui, self.title_rect.min, self.title_galley); + } + + pub fn close_button_ui(&self, ui: &mut Ui, content: &Option) -> InteractInfo { + let right = content.map(|c| c.rect.right()).unwrap_or(self.rect.right()); + + let button_size = ui.style().start_icon_width; + let button_rect = Rect::from_min_size( + pos2( + right - ui.style().item_spacing.x - button_size, + self.rect.center().y - 0.5 * button_size, + ), + Vec2::splat(button_size), + ); + + close_button(ui, button_rect) + } } fn close_button(ui: &mut Ui, rect: Rect) -> InteractInfo { diff --git a/emigui/src/examples/app.rs b/emigui/src/examples/app.rs index 691057a34..8b3dff6d9 100644 --- a/emigui/src/examples/app.rs +++ b/emigui/src/examples/app.rs @@ -497,10 +497,10 @@ impl LayoutExample { pub fn ui(&mut self, ui: &mut Ui) { Resize::default() .default_size(vec2(200.0, 200.0)) - .show(ui, |ui| self.contents_ui(ui)); + .show(ui, |ui| self.content_ui(ui)); } - pub fn contents_ui(&mut self, ui: &mut Ui) { + pub fn content_ui(&mut self, ui: &mut Ui) { let layout = Layout::from_dir_align(self.dir, self.align); if self.reversed { ui.set_layout(layout.reverse()); diff --git a/emigui/src/paint/mesher.rs b/emigui/src/paint/mesher.rs index 4a400e402..519649352 100644 --- a/emigui/src/paint/mesher.rs +++ b/emigui/src/paint/mesher.rs @@ -163,6 +163,14 @@ impl Path { self.0.clear(); } + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + pub fn len(&self) -> usize { + self.0.len() + } + #[inline(always)] pub fn add_point(&mut self, pos: Pos2, normal: Vec2) { self.0.push(PathPoint { pos, normal }); @@ -252,13 +260,13 @@ impl Path { /// with x right, and y down (GUI coords) we have: /// angle = dir /// 0 * TAU / 4 = right - /// quadrant 0, right down - /// 1 * TAU / 4 = down - /// quadrant 1, down left + /// quadrant 0, right bottom + /// 1 * TAU / 4 = bottom + /// quadrant 1, left bottom /// 2 * TAU / 4 = left - /// quadrant 2 left up - /// 3 * TAU / 4 = up - /// quadrant 3 up rigth + /// quadrant 2 left top + /// 3 * TAU / 4 = top + /// quadrant 3 right top /// 4 * TAU / 4 = right pub fn add_circle_quadrant(&mut self, center: Pos2, radius: f32, quadrant: f32) { let n = (radius * 0.5).round() as i32; // TODO: tweak a bit more @@ -581,16 +589,18 @@ pub fn paint_command_into_triangles( fill_color, outline, } => { - if let Some(fill_color) = fill_color { - debug_assert!( - closed, - "You asked to fill a path that is not closed. That makes no sense." - ); - fill_closed_path(out, options, &path.0, fill_color); - } - if let Some(outline) = outline { - let typ = if closed { Closed } else { Open }; - paint_path_outline(out, options, typ, &path.0, outline.color, outline.width); + if path.len() >= 2 { + if let Some(fill_color) = fill_color { + debug_assert!( + closed, + "You asked to fill a path that is not closed. That makes no sense." + ); + fill_closed_path(out, options, &path.0, fill_color); + } + if let Some(outline) = outline { + let typ = if closed { Closed } else { Open }; + paint_path_outline(out, options, typ, &path.0, outline.color, outline.width); + } } } PaintCmd::Rect { diff --git a/emigui/src/style.rs b/emigui/src/style.rs index 168c0be4e..7e8427802 100644 --- a/emigui/src/style.rs +++ b/emigui/src/style.rs @@ -120,7 +120,7 @@ impl Default for Interact { 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)), + rect_outline: Some(Outline::new(1.0, white(128))), corner_radius: 0.0, }, } diff --git a/emigui/src/widgets.rs b/emigui/src/widgets.rs index 728d08e56..6c5bcbd98 100644 --- a/emigui/src/widgets.rs +++ b/emigui/src/widgets.rs @@ -63,7 +63,16 @@ impl Label { self } - pub fn layout(&self, max_width: f32, ui: &Ui) -> font::Galley { + pub fn layout(&self, ui: &Ui) -> font::Galley { + let max_width = if self.auto_shrink { + ui.available_finite().width() + } else { + ui.available().width() + }; + self.layout_width(ui, max_width) + } + + pub fn layout_width(&self, ui: &Ui, max_width: f32) -> font::Galley { let font = &ui.fonts()[self.text_style]; if self.multiline { font.layout_multiline(self.text.clone(), max_width) // TODO: avoid clone @@ -83,6 +92,10 @@ impl Label { // TODO: a paint method for painting anywhere in a ui. // This should be the easiest method of putting text anywhere. + + pub fn paint_galley(&self, ui: &mut Ui, pos: Pos2, galley: font::Galley) { + ui.add_galley(pos, galley, self.text_style, self.text_color); + } } /// Usage: label!("Foo: {}", bar) @@ -94,14 +107,9 @@ macro_rules! label { impl Widget for Label { fn ui(self, ui: &mut Ui) -> GuiResponse { - let max_width = if self.auto_shrink { - ui.available_finite().width() - } else { - ui.available().width() - }; - let galley = self.layout(max_width, ui); + let galley = self.layout(ui); let interact = ui.reserve_space(galley.size, None); - ui.add_galley(interact.rect.min, galley, self.text_style, self.text_color); + self.paint_galley(ui, interact.rect.min, galley); ui.response(interact) } }