[window] make resizing a bit smoother

This commit is contained in:
Emil Ernerfeldt 2020-06-03 21:14:47 +02:00
parent 1c9fdc3a48
commit d0f5181197
8 changed files with 81 additions and 70 deletions

10
Cargo.lock generated
View file

@ -348,7 +348,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]] [[package]]
name = "egui" name = "egui"
version = "0.1.0" version = "0.1.1"
dependencies = [ dependencies = [
"ahash 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", "ahash 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
"criterion 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)", "criterion 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
@ -363,7 +363,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"chrono 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)", "chrono 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)",
"clipboard 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", "clipboard 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
"egui 0.1.0", "egui 0.1.1",
"glium 0.24.0 (registry+https://github.com/rust-lang/crates.io-index)", "glium 0.24.0 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_json 1.0.53 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 1.0.53 (registry+https://github.com/rust-lang/crates.io-index)",
"webbrowser 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)", "webbrowser 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)",
@ -373,7 +373,7 @@ dependencies = [
name = "egui_wasm" name = "egui_wasm"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"egui 0.1.0", "egui 0.1.1",
"js-sys 0.3.39 (registry+https://github.com/rust-lang/crates.io-index)", "js-sys 0.3.39 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.110 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.110 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_json 1.0.53 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 1.0.53 (registry+https://github.com/rust-lang/crates.io-index)",
@ -390,7 +390,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
name = "example_glium" name = "example_glium"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"egui 0.1.0", "egui 0.1.1",
"egui_glium 0.1.0", "egui_glium 0.1.0",
"glium 0.24.0 (registry+https://github.com/rust-lang/crates.io-index)", "glium 0.24.0 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.110 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.110 (registry+https://github.com/rust-lang/crates.io-index)",
@ -401,7 +401,7 @@ dependencies = [
name = "example_wasm" name = "example_wasm"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"egui 0.1.0", "egui 0.1.1",
"egui_wasm 0.1.0", "egui_wasm 0.1.0",
"serde 1.0.110 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.110 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_json 1.0.53 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 1.0.53 (registry+https://github.com/rust-lang/crates.io-index)",

View file

@ -227,7 +227,7 @@ impl Prepared {
fn mouse_pressed_on_area(ctx: &Context, layer: Layer) -> bool { fn mouse_pressed_on_area(ctx: &Context, layer: Layer) -> bool {
if let Some(mouse_pos) = ctx.input().mouse.pos { if let Some(mouse_pos) = ctx.input().mouse.pos {
ctx.input().mouse.pressed && ctx.memory().layer_at(mouse_pos) == Some(layer) ctx.input().mouse.pressed && ctx.layer_at(mouse_pos) == Some(layer)
} else { } else {
false false
} }

View file

@ -25,9 +25,8 @@ pub struct Resize {
/// If false, we are no enabled /// If false, we are no enabled
resizable: bool, resizable: bool,
// Will still try to stay within parent ui bounds min_content_size: Vec2,
min_size: Vec2, min_desired_size: Vec2,
max_size: Vec2,
default_size: Vec2, default_size: Vec2,
@ -40,8 +39,8 @@ impl Default for Resize {
Self { Self {
id: None, id: None,
resizable: true, resizable: true,
min_size: Vec2::splat(16.0), min_content_size: Vec2::splat(16.0),
max_size: Vec2::infinity(), min_desired_size: vec2(200.0, 400.0),
default_size: vec2(280.0, 400.0), // TODO: perferred size for a resizable area (e.g. a window) default_size: vec2(280.0, 400.0), // TODO: perferred size for a resizable area (e.g. a window)
outline: true, outline: true,
handle_offset: Default::default(), handle_offset: Default::default(),
@ -84,13 +83,15 @@ impl Resize {
self self
} }
pub fn min_size(mut self, min_size: impl Into<Vec2>) -> Self { /// Won't shrink to smaller than this
self.min_size = min_size.into(); pub fn min_content_size(mut self, min_content_size: impl Into<Vec2>) -> Self {
self.min_content_size = min_content_size.into();
self self
} }
pub fn max_size(mut self, max_size: impl Into<Vec2>) -> Self { /// Won't shrink to smaller than this
self.max_size = max_size.into(); pub fn min_desired_size(mut self, min_desired_size: impl Into<Vec2>) -> Self {
self.min_desired_size = min_desired_size.into();
self self
} }
@ -114,17 +115,12 @@ impl Resize {
pub fn fixed_size(mut self, size: impl Into<Vec2>) -> Self { pub fn fixed_size(mut self, size: impl Into<Vec2>) -> Self {
let size = size.into(); let size = size.into();
self.default_size = size; self.default_size = size;
self.min_size = size; self.min_content_size = size;
self.max_size = size; self.min_desired_size = size;
self.resizable = false; self.resizable = false;
self self
} }
pub fn as_wide_as_possible(mut self) -> Self {
self.min_size.x = f32::INFINITY;
self
}
/// Offset the position of the resize handle by this much /// Offset the position of the resize handle by this much
pub fn handle_offset(mut self, handle_offset: impl Into<Vec2>) -> Self { pub fn handle_offset(mut self, handle_offset: impl Into<Vec2>) -> Self {
self.handle_offset = handle_offset.into(); self.handle_offset = handle_offset.into();
@ -147,12 +143,9 @@ struct Prepared {
impl Resize { impl Resize {
fn begin(&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")); 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());
self.max_size = self.max_size.max(self.min_size);
let mut state = ui.memory().resize.get(&id).cloned().unwrap_or_else(|| { let mut state = ui.memory().resize.get(&id).cloned().unwrap_or_else(|| {
let default_size = self.default_size.clamp(self.min_size..=self.max_size); let default_size = self.default_size.max(self.min_content_size);
State { State {
desired_size: default_size, desired_size: default_size,
@ -161,7 +154,7 @@ impl Resize {
} }
}); });
state.desired_size = state.desired_size.clamp(self.min_size..=self.max_size); state.desired_size = state.desired_size.max(self.min_desired_size);
let position = ui.available().min; let position = ui.available().min;
@ -176,14 +169,8 @@ impl Resize {
if corner_interact.active { if corner_interact.active {
if let Some(mouse_pos) = ui.input().mouse.pos { if let Some(mouse_pos) = ui.input().mouse.pos {
// This is the desired size. We may not be able to achieve it.
state.desired_size = mouse_pos - position + 0.5 * corner_interact.rect.size() state.desired_size = mouse_pos - position + 0.5 * corner_interact.rect.size()
- self.handle_offset; - self.handle_offset;
// We don't clamp to max size, because we want to be able to push against outer bounds.
// For instance, if we are inside a bigger Resize region, we want to expand that.
// state.desired_size = state.desired_size.clamp(self.min_size..=self.max_size);
state.desired_size = state.desired_size.max(self.min_size);
} }
} }
Some(corner_interact) Some(corner_interact)
@ -193,11 +180,8 @@ impl Resize {
if let Some(requested_size) = state.requested_size.take() { if let Some(requested_size) = state.requested_size.take() {
state.desired_size = requested_size; state.desired_size = requested_size;
// We don't clamp to max size, because we want to be able to push against outer bounds.
// For instance, if we are inside a bigger Resize region, we want to expand that.
// state.desired_size = state.desired_size.clamp(self.min_size..=self.max_size);
state.desired_size = state.desired_size.max(self.min_size);
} }
state.desired_size = state.desired_size.max(self.min_desired_size);
// ------------------------------ // ------------------------------
@ -283,6 +267,19 @@ impl Resize {
} }
ui.memory().resize.insert(id, state); ui.memory().resize.insert(id, state);
if ui.ctx().style().debug_resize {
ui.ctx().debug_rect(
Rect::from_min_size(content_ui.top_left(), state.desired_size),
color::GREEN,
"desired_size",
);
ui.ctx().debug_rect(
Rect::from_min_size(content_ui.top_left(), state.last_content_size),
color::LIGHT_BLUE,
"last_content_size",
);
}
} }
} }

View file

@ -27,7 +27,10 @@ impl<'open> Window<'open> {
open: None, open: None,
area, area,
frame: None, frame: None,
resize: Resize::default().outline(false), resize: Resize::default()
.outline(false)
.min_content_size([96.0, 32.0])
.min_desired_size([96.0, 200.0]),
scroll: Some( scroll: Some(
ScrollArea::default() ScrollArea::default()
.always_show_scroll(false) .always_show_scroll(false)
@ -78,16 +81,6 @@ impl<'open> Window<'open> {
self.default_pos(rect.min).default_size(rect.size()) self.default_pos(rect.min).default_size(rect.size())
} }
pub fn min_size(mut self, min_size: impl Into<Vec2>) -> Self {
self.resize = self.resize.min_size(min_size);
self
}
pub fn max_size(mut self, max_size: impl Into<Vec2>) -> Self {
self.resize = self.resize.max_size(max_size);
self
}
pub fn fixed_size(mut self, size: impl Into<Vec2>) -> Self { pub fn fixed_size(mut self, size: impl Into<Vec2>) -> Self {
self.resize = self.resize.fixed_size(size); self.resize = self.resize.fixed_size(size);
self self
@ -165,8 +158,12 @@ impl<'open> Window<'open> {
// First interact (move etc) to avoid frame delay: // First interact (move etc) to avoid frame delay:
let last_frame_outer_rect = area.state().rect(); let last_frame_outer_rect = area.state().rect();
let interaction = if possible.movable || possible.resizable { let interaction = if possible.movable || possible.resizable {
let title_bar_height =
title_label.font_height(ctx.fonts()) + 1.0 * ctx.style().item_spacing.y; // this could be better
let margins = 2.0 * frame.margin + vec2(0.0, title_bar_height);
interact( interact(
ctx, ctx,
margins,
possible, possible,
area_layer, area_layer,
area.state_mut(), area.state_mut(),
@ -295,6 +292,7 @@ impl WindowInteraction {
fn interact( fn interact(
ctx: &Context, ctx: &Context,
margins: Vec2,
possible: PossibleInteractions, possible: PossibleInteractions,
area_layer: Layer, area_layer: Layer,
area_state: &mut area::State, area_state: &mut area::State,
@ -302,7 +300,6 @@ fn interact(
resize_id: Id, resize_id: Id,
rect: Rect, rect: Rect,
) -> Option<WindowInteraction> { ) -> Option<WindowInteraction> {
let pre_resize = ctx.round_rect_to_pixels(rect);
let window_interaction = window_interaction( let window_interaction = window_interaction(
ctx, ctx,
possible, possible,
@ -317,12 +314,11 @@ fn interact(
area_state.pos = new_rect.min; area_state.pos = new_rect.min;
if window_interaction.is_resize() {
let mut resize_state = ctx.memory().resize.get(&resize_id).cloned().unwrap(); let mut resize_state = ctx.memory().resize.get(&resize_id).cloned().unwrap();
// resize_state.size += new_rect.size() - pre_resize.size(); resize_state.requested_size = Some(new_rect.size() - margins);
// resize_state.size = new_rect.size() - some margin;
resize_state.requested_size =
Some(resize_state.desired_size + new_rect.size() - pre_resize.size());
ctx.memory().resize.insert(resize_id, resize_state); ctx.memory().resize.insert(resize_id, resize_state);
}
ctx.memory().areas.move_to_top(area_layer); ctx.memory().areas.move_to_top(area_layer);
Some(window_interaction) Some(window_interaction)
@ -400,7 +396,7 @@ fn resize_hover(
rect: Rect, rect: Rect,
) -> Option<WindowInteraction> { ) -> Option<WindowInteraction> {
if let Some(mouse_pos) = ctx.input().mouse.pos { if let Some(mouse_pos) = ctx.input().mouse.pos {
if let Some(top_layer) = ctx.memory().layer_at(mouse_pos) { if let Some(top_layer) = ctx.layer_at(mouse_pos) {
if top_layer != area_layer && top_layer.order != Order::Background { if top_layer != area_layer && top_layer.order != Order::Background {
return None; // Another window is on top here return None; // Another window is on top here
} }
@ -411,8 +407,8 @@ fn resize_hover(
return None; return None;
} }
let side_interact_radius = 5.0; // TODO: from style let side_interact_radius = ctx.style().resize_interact_radius_side;
let corner_interact_radius = 10.0; // TODO let corner_interact_radius = ctx.style().resize_interact_radius_corner;
if rect.expand(side_interact_radius).contains(mouse_pos) { if rect.expand(side_interact_radius).contains(mouse_pos) {
let (mut left, mut right, mut top, mut bottom) = Default::default(); let (mut left, mut right, mut top, mut bottom) = Default::default();
if possible.resizable { if possible.resizable {
@ -531,7 +527,7 @@ fn show_title_bar(
collapsing: &mut collapsing_header::State, collapsing: &mut collapsing_header::State,
) -> TitleBar { ) -> TitleBar {
let title_bar_and_rect = ui.inner_layout(Layout::horizontal(Align::Center), |ui| { let title_bar_and_rect = ui.inner_layout(Layout::horizontal(Align::Center), |ui| {
ui.set_desired_height(title_label.font_height(ui)); ui.set_desired_height(title_label.font_height(ui.fonts()));
let item_spacing = ui.style().item_spacing; let item_spacing = ui.style().item_spacing;
let button_size = ui.style().start_icon_width; let button_size = ui.style().start_icon_width;

View file

@ -258,10 +258,17 @@ impl Context {
} }
} }
// ---------------------------------------------------------------------
pub fn layer_at(&self, pos: Pos2) -> Option<Layer> {
let resize_interact_radius_side = self.style().resize_interact_radius_side;
self.memory().layer_at(pos, resize_interact_radius_side)
}
pub fn contains_mouse(&self, layer: Layer, clip_rect: Rect, rect: Rect) -> bool { pub fn contains_mouse(&self, layer: Layer, clip_rect: Rect, rect: Rect) -> bool {
let rect = rect.intersect(clip_rect); let rect = rect.intersect(clip_rect);
if let Some(mouse_pos) = self.input.mouse.pos { if let Some(mouse_pos) = self.input.mouse.pos {
rect.contains(mouse_pos) && self.memory().layer_at(mouse_pos) == Some(layer) rect.contains(mouse_pos) && self.layer_at(mouse_pos) == Some(layer)
} else { } else {
false false
} }
@ -399,7 +406,7 @@ impl Context {
); );
} }
pub fn debug_rect(&self, rect: Rect, name: impl Into<String>) { pub fn debug_rect(&self, rect: Rect, color: Color, name: impl Into<String>) {
let text = format!("{} {:?}", name.into(), rect); let text = format!("{} {:?}", name.into(), rect);
let layer = Layer::debug(); let layer = Layer::debug();
self.add_paint_cmd( self.add_paint_cmd(
@ -407,13 +414,13 @@ impl Context {
PaintCmd::Rect { PaintCmd::Rect {
corner_radius: 0.0, corner_radius: 0.0,
fill: None, fill: None,
outline: Some(LineStyle::new(1.0, color::RED)), outline: Some(LineStyle::new(2.0, color)),
rect, rect,
}, },
); );
let align = (Align::Min, Align::Min); let align = (Align::Min, Align::Min);
let text_style = TextStyle::Monospace; let text_style = TextStyle::Monospace;
self.floating_text(layer, rect.min, text, text_style, align, Some(color::RED)); self.floating_text(layer, rect.min, text, text_style, align, Some(color));
} }
/// Show some text anywhere on screen. /// Show some text anywhere on screen.

View file

@ -112,9 +112,8 @@ impl Memory {
self.areas.end_frame() self.areas.end_frame()
} }
/// TODO: call once at the start of the frame for the current mouse pos pub fn layer_at(&self, pos: Pos2, resize_interact_radius_side: f32) -> Option<Layer> {
pub fn layer_at(&self, pos: Pos2) -> Option<Layer> { self.areas.layer_at(pos, resize_interact_radius_side)
self.areas.layer_at(pos)
} }
} }
@ -139,13 +138,14 @@ impl Areas {
} }
} }
/// TODO: call once at the start of the frame for the current mouse pos pub fn layer_at(&self, pos: Pos2, resize_interact_radius_side: f32) -> Option<Layer> {
pub fn layer_at(&self, pos: Pos2) -> Option<Layer> {
for layer in self.order.iter().rev() { for layer in self.order.iter().rev() {
if self.is_visible(layer) { if self.is_visible(layer) {
if let Some(state) = self.areas.get(&layer.id) { if let Some(state) = self.areas.get(&layer.id) {
if state.interactable { if state.interactable {
let rect = Rect::from_min_size(state.pos, state.size); let rect = Rect::from_min_size(state.pos, state.size);
// Allow us to resize by dragging just outside the window:
let rect = rect.expand(resize_interact_radius_side);
if rect.contains(pos) { if rect.contains(pos) {
return Some(*layer); return Some(*layer);
} }

View file

@ -25,6 +25,12 @@ pub struct Style {
/// The text starts after this many pixels. /// The text starts after this many pixels.
pub start_icon_width: f32, pub start_icon_width: f32,
/// Mouse must be the close to the side of a window to resize
pub resize_interact_radius_side: f32,
/// Mouse must be the close to the corner of a window to resize
pub resize_interact_radius_corner: f32,
// ----------------------------------------------- // -----------------------------------------------
// Purely visual: // Purely visual:
pub interact: Interact, pub interact: Interact,
@ -60,6 +66,7 @@ pub struct Style {
// ----------------------------------------------- // -----------------------------------------------
// Debug rendering: // Debug rendering:
pub debug_widget_rects: bool, pub debug_widget_rects: bool,
pub debug_resize: bool,
} }
impl Default for Style { impl Default for Style {
@ -71,6 +78,8 @@ impl Default for Style {
indent: 21.0, indent: 21.0,
clickable_diameter: 22.0, clickable_diameter: 22.0,
start_icon_width: 14.0, start_icon_width: 14.0,
resize_interact_radius_side: 5.0,
resize_interact_radius_corner: 10.0,
interact: Default::default(), interact: Default::default(),
text_color: gray(160, 255), text_color: gray(160, 255),
line_width: 1.0, line_width: 1.0,
@ -84,6 +93,7 @@ impl Default for Style {
menu_bar: MenuBar::default(), menu_bar: MenuBar::default(),
clip_rect_margin: 3.0, clip_rect_margin: 3.0,
debug_widget_rects: false, debug_widget_rects: false,
debug_resize: false,
} }
} }
} }
@ -220,6 +230,7 @@ impl Style {
} }
ui.add(Checkbox::new(&mut self.debug_widget_rects, "Paint debug rectangles around widgets")); ui.add(Checkbox::new(&mut self.debug_widget_rects, "Paint debug rectangles around widgets"));
ui.add(Checkbox::new(&mut self.debug_resize, "Debug Resize"));
ui.add(Slider::f32(&mut self.item_spacing.x, 0.0..=10.0).text("item_spacing.x").precision(0)); ui.add(Slider::f32(&mut self.item_spacing.x, 0.0..=10.0).text("item_spacing.x").precision(0));
ui.add(Slider::f32(&mut self.item_spacing.y, 0.0..=10.0).text("item_spacing.y").precision(0)); ui.add(Slider::f32(&mut self.item_spacing.y, 0.0..=10.0).text("item_spacing.y").precision(0));

View file

@ -81,8 +81,8 @@ impl Label {
} }
} }
pub fn font_height(&self, ui: &Ui) -> f32 { pub fn font_height(&self, fonts: &Fonts) -> f32 {
ui.fonts()[self.text_style].height() fonts[self.text_style].height()
} }
// TODO: this should return a LabelLayout which has a paint method. // TODO: this should return a LabelLayout which has a paint method.