mirror of
https://github.com/emilk/egui.git
synced 2026-08-30 21:30:03 -04:00
Implement Window as collection of Floating + Frame + Resize
This commit is contained in:
166
emigui/src/containers/collapsing_header.rs
Normal file
166
emigui/src/containers/collapsing_header.rs
Normal file
@@ -0,0 +1,166 @@
|
||||
use crate::{layout::Direction, *};
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub(crate) struct State {
|
||||
pub open: bool,
|
||||
pub toggle_time: f64,
|
||||
}
|
||||
|
||||
impl Default for State {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
open: false,
|
||||
toggle_time: -std::f64::INFINITY,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CollapsingHeader {
|
||||
title: String,
|
||||
default_open: bool,
|
||||
}
|
||||
|
||||
impl CollapsingHeader {
|
||||
pub fn new(title: impl Into<String>) -> Self {
|
||||
Self {
|
||||
title: title.into(),
|
||||
default_open: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn default_open(mut self) -> Self {
|
||||
self.default_open = true;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl CollapsingHeader {
|
||||
pub fn show(self, region: &mut Region, add_contents: impl FnOnce(&mut Region)) -> GuiResponse {
|
||||
assert!(
|
||||
region.dir == Direction::Vertical,
|
||||
"Horizontal collapsing is unimplemented"
|
||||
);
|
||||
let Self {
|
||||
title,
|
||||
default_open,
|
||||
} = self;
|
||||
|
||||
let id = region.make_unique_id(&title);
|
||||
let text_style = TextStyle::Button;
|
||||
let font = ®ion.fonts()[text_style];
|
||||
let (title, text_size) = font.layout_multiline(&title, region.available_width());
|
||||
|
||||
let interact = region.reserve_space(
|
||||
vec2(
|
||||
region.available_width(),
|
||||
text_size.y + 2.0 * region.style.button_padding.y,
|
||||
),
|
||||
Some(id),
|
||||
);
|
||||
|
||||
let state = {
|
||||
let mut memory = region.ctx.memory.lock();
|
||||
let mut state = memory.collapsing_headers.entry(id).or_insert(State {
|
||||
open: default_open,
|
||||
..Default::default()
|
||||
});
|
||||
if interact.clicked {
|
||||
state.open = !state.open;
|
||||
state.toggle_time = region.ctx.input.time;
|
||||
}
|
||||
*state
|
||||
};
|
||||
|
||||
region.add_paint_cmd(PaintCmd::Rect {
|
||||
corner_radius: region.style.interact_corner_radius(&interact),
|
||||
fill_color: region.style.interact_fill_color(&interact),
|
||||
outline: region.style().interact_outline(&interact),
|
||||
rect: interact.rect,
|
||||
});
|
||||
|
||||
paint_icon(region, &state, &interact);
|
||||
|
||||
region.add_text(
|
||||
pos2(
|
||||
interact.rect.left() + region.style.indent,
|
||||
interact.rect.center().y - text_size.y / 2.0,
|
||||
),
|
||||
text_style,
|
||||
title,
|
||||
Some(region.style.interact_stroke_color(&interact)),
|
||||
);
|
||||
|
||||
let animation_time = region.style().animation_time;
|
||||
let time_since_toggle = (region.ctx.input.time - state.toggle_time) as f32;
|
||||
if time_since_toggle < animation_time {
|
||||
region.indent(id, |region| {
|
||||
// animation time
|
||||
|
||||
let max_height = if state.open {
|
||||
remap(
|
||||
time_since_toggle,
|
||||
0.0..=animation_time,
|
||||
// Get instant feedback, and we don't expect to get bigger than this
|
||||
50.0..=1500.0,
|
||||
)
|
||||
} else {
|
||||
remap_clamp(
|
||||
time_since_toggle,
|
||||
0.0..=animation_time,
|
||||
// TODO: state.open_height
|
||||
50.0..=0.0,
|
||||
)
|
||||
};
|
||||
|
||||
region
|
||||
.clip_rect
|
||||
.set_height(region.clip_rect.height().min(max_height));
|
||||
|
||||
add_contents(region);
|
||||
|
||||
region.child_bounds.max.y = region
|
||||
.child_bounds
|
||||
.max
|
||||
.y
|
||||
.min(region.child_bounds.min.y + max_height);
|
||||
});
|
||||
} else if state.open {
|
||||
region.indent(id, add_contents);
|
||||
}
|
||||
|
||||
region.response(interact)
|
||||
}
|
||||
}
|
||||
|
||||
fn paint_icon(region: &mut Region, state: &State, interact: &InteractInfo) {
|
||||
let stroke_color = region.style.interact_stroke_color(&interact);
|
||||
let stroke_width = region.style.interact_stroke_width(&interact);
|
||||
|
||||
let (mut small_icon_rect, _) = region.style.icon_rectangles(&interact.rect);
|
||||
small_icon_rect.set_center(pos2(
|
||||
interact.rect.left() + region.style.indent / 2.0,
|
||||
interact.rect.center().y,
|
||||
));
|
||||
|
||||
// Draw a minus:
|
||||
region.add_paint_cmd(PaintCmd::Line {
|
||||
points: vec![
|
||||
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:
|
||||
region.add_paint_cmd(PaintCmd::Line {
|
||||
points: vec![
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
104
emigui/src/containers/floating.rs
Normal file
104
emigui/src/containers/floating.rs
Normal file
@@ -0,0 +1,104 @@
|
||||
//! A Floating is a region that has no parent, it floats on the background.
|
||||
//! It is potentioally movable.
|
||||
//! It has no frame or own size.
|
||||
//! It is the foundation for a window
|
||||
|
||||
use std::{fmt::Debug, hash::Hash, sync::Arc};
|
||||
|
||||
use crate::*;
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct State {
|
||||
/// Last known pos
|
||||
pub pos: Pos2,
|
||||
|
||||
/// Last know size. Used for catching clicks.
|
||||
pub size: Vec2,
|
||||
}
|
||||
|
||||
// TODO: rename Floating to something else.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Floating {
|
||||
id: Id,
|
||||
movable: bool,
|
||||
default_pos: Option<Pos2>,
|
||||
}
|
||||
|
||||
impl Floating {
|
||||
pub fn new(id_source: impl Hash) -> Self {
|
||||
Self {
|
||||
id: Id::new(id_source),
|
||||
movable: true,
|
||||
default_pos: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn movable(mut self, movable: bool) -> Self {
|
||||
self.movable = movable;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn default_pos(mut self, default_pos: Pos2) -> Self {
|
||||
self.default_pos = Some(default_pos);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Floating {
|
||||
pub fn show(self, ctx: &Arc<Context>, add_contents: impl FnOnce(&mut Region)) {
|
||||
let default_pos = self.default_pos.unwrap_or_else(|| pos2(100.0, 100.0)); // TODO
|
||||
let id = ctx.register_unique_id(self.id, "Floating", default_pos);
|
||||
let layer = Layer::Window(id);
|
||||
|
||||
let (mut state, _is_new) = match ctx.memory.lock().get_floating(id) {
|
||||
Some(state) => (state, false),
|
||||
None => {
|
||||
let state = State {
|
||||
pos: default_pos,
|
||||
size: Vec2::zero(),
|
||||
};
|
||||
(state, true)
|
||||
}
|
||||
};
|
||||
state.pos = state.pos.round();
|
||||
|
||||
let mut region = Region::new(
|
||||
ctx.clone(),
|
||||
layer,
|
||||
id,
|
||||
Rect::from_min_size(state.pos, Vec2::infinity()),
|
||||
);
|
||||
add_contents(&mut region);
|
||||
state.size = region.bounding_size().ceil();
|
||||
|
||||
let rect = Rect::from_min_size(state.pos, state.size);
|
||||
let move_interact = ctx.interact(layer, &rect, Some(id.with("move")));
|
||||
|
||||
if move_interact.active {
|
||||
state.pos += ctx.input().mouse_move;
|
||||
}
|
||||
|
||||
// Constrain to screen:
|
||||
let margin = 32.0;
|
||||
state.pos = state.pos.max(pos2(margin - state.size.x, 0.0));
|
||||
state.pos = state.pos.min(pos2(
|
||||
ctx.input.screen_size.x - margin,
|
||||
ctx.input.screen_size.y - margin,
|
||||
));
|
||||
|
||||
state.pos = state.pos.round();
|
||||
|
||||
if move_interact.active || mouse_pressed_on_floating(ctx, id) {
|
||||
ctx.memory.lock().move_floating_to_top(id);
|
||||
}
|
||||
ctx.memory.lock().set_floating_state(id, state);
|
||||
}
|
||||
}
|
||||
|
||||
fn mouse_pressed_on_floating(ctx: &Context, id: Id) -> bool {
|
||||
if let Some(mouse_pos) = ctx.input.mouse_pos {
|
||||
ctx.input.mouse_pressed && ctx.memory.lock().layer_at(mouse_pos) == Layer::Window(id)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
44
emigui/src/containers/frame.rs
Normal file
44
emigui/src/containers/frame.rs
Normal file
@@ -0,0 +1,44 @@
|
||||
//! Frame container
|
||||
|
||||
use crate::*;
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Frame {}
|
||||
|
||||
impl Frame {
|
||||
pub fn show(self, region: &mut Region, add_contents: impl FnOnce(&mut Region)) {
|
||||
let style = region.style();
|
||||
let margin = style.window_padding;
|
||||
|
||||
let outer_pos = region.cursor();
|
||||
let inner_rect =
|
||||
Rect::from_min_size(outer_pos + margin, region.available_space() - 2.0 * margin);
|
||||
let where_to_put_background = region.paint_list_len();
|
||||
|
||||
let mut child_region = region.child_region(inner_rect);
|
||||
add_contents(&mut child_region);
|
||||
|
||||
// TODO: handle the last item_spacing in a nicer way
|
||||
let inner_size = child_region.bounding_size();
|
||||
let inner_size = inner_size.ceil(); // TODO: round to pixel
|
||||
|
||||
let outer_rect = Rect::from_min_size(outer_pos, margin + inner_size + margin);
|
||||
|
||||
let corner_radius = style.window.corner_radius;
|
||||
let fill_color = style.background_fill_color();
|
||||
region.insert_paint_cmd(
|
||||
where_to_put_background,
|
||||
PaintCmd::Rect {
|
||||
corner_radius,
|
||||
fill_color: Some(fill_color),
|
||||
outline: Some(Outline::new(1.0, color::WHITE)),
|
||||
rect: outer_rect,
|
||||
},
|
||||
);
|
||||
|
||||
// TODO: move up corsor?
|
||||
region
|
||||
.child_bounds
|
||||
.extend_with(child_region.child_bounds.max + margin);
|
||||
}
|
||||
}
|
||||
217
emigui/src/containers/resize.rs
Normal file
217
emigui/src/containers/resize.rs
Normal file
@@ -0,0 +1,217 @@
|
||||
#![allow(unused_variables)] // TODO
|
||||
use crate::*;
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct State {
|
||||
pub size: Vec2,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Resize {
|
||||
/// If false, we are no enabled
|
||||
resizable: bool,
|
||||
|
||||
// Will still try to stay within parent region bounds
|
||||
min_size: Vec2,
|
||||
max_size: Vec2,
|
||||
|
||||
default_size: Vec2,
|
||||
|
||||
// If true, won't allow you to make window so big that it creates spacing
|
||||
auto_shrink_width: bool,
|
||||
auto_shrink_height: bool,
|
||||
|
||||
// If true, won't allow you to resize smaller than that everything fits.
|
||||
expand_width_to_fit_content: bool,
|
||||
expand_height_to_fit_content: bool,
|
||||
|
||||
handle_offset: Vec2,
|
||||
}
|
||||
|
||||
impl Default for Resize {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
resizable: true,
|
||||
min_size: Vec2::splat(32.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,
|
||||
handle_offset: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Resize {
|
||||
pub fn default_height(mut self, height: f32) -> Self {
|
||||
self.default_size.y = height;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn default_size(mut self, default_size: Vec2) -> Self {
|
||||
self.default_size = default_size;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn min_size(mut self, min_size: Vec2) -> Self {
|
||||
self.min_size = min_size;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn max_size(mut self, max_size: Vec2) -> Self {
|
||||
self.max_size = max_size;
|
||||
self
|
||||
}
|
||||
|
||||
/// Can you resize it with the mouse?
|
||||
/// Note that a window can still auto-resize
|
||||
pub fn resizable(mut self, resizable: bool) -> Self {
|
||||
self.resizable = resizable;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn fixed_size(mut self, size: Vec2) -> Self {
|
||||
self.auto_shrink_width = false;
|
||||
self.auto_shrink_height = false;
|
||||
self.expand_width_to_fit_content = false;
|
||||
self.expand_height_to_fit_content = false;
|
||||
self.default_size = size;
|
||||
self.min_size = size;
|
||||
self.max_size = size;
|
||||
self.resizable = false;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn as_wide_as_possible(mut self) -> Self {
|
||||
self.min_size.x = f32::INFINITY;
|
||||
self
|
||||
}
|
||||
|
||||
/// true: prevent from resizing to smaller than contents.
|
||||
/// false: allow shrinking to smaller than contents.
|
||||
pub fn auto_expand(mut self, auto_expand: bool) -> Self {
|
||||
self.expand_width_to_fit_content = auto_expand;
|
||||
self.expand_height_to_fit_content = auto_expand;
|
||||
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
|
||||
}
|
||||
|
||||
pub fn auto_shrink_height(mut self, auto_shrink_height: bool) -> Self {
|
||||
self.auto_shrink_height = auto_shrink_height;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: a common trait for Things that follow this pattern
|
||||
impl Resize {
|
||||
pub fn show(mut self, region: &mut Region, add_contents: impl FnOnce(&mut Region)) {
|
||||
if !self.resizable {
|
||||
return add_contents(region);
|
||||
}
|
||||
|
||||
let id = region.make_child_id("scroll");
|
||||
self.min_size = self.min_size.min(region.available_space());
|
||||
self.max_size = self.max_size.min(region.available_space());
|
||||
self.max_size = self.max_size.max(self.min_size);
|
||||
|
||||
let (is_new, mut state) = match region.memory().resize.get(&id) {
|
||||
Some(state) => (false, state.clone()),
|
||||
None => {
|
||||
let default_size = self.default_size.clamp(self.min_size..=self.max_size);
|
||||
(true, State { size: default_size })
|
||||
}
|
||||
};
|
||||
|
||||
state.size = state.size.clamp(self.min_size..=self.max_size);
|
||||
|
||||
let position = region.cursor();
|
||||
|
||||
// Resize-corner:
|
||||
let corner_size = Vec2::splat(16.0); // TODO: style
|
||||
let corner_rect = Rect::from_min_size(
|
||||
position + state.size + self.handle_offset - corner_size,
|
||||
corner_size,
|
||||
);
|
||||
let corner_interact = region.interact_rect(&corner_rect, id.with("corner"));
|
||||
|
||||
if corner_interact.active {
|
||||
if let Some(mouse_pos) = region.input().mouse_pos {
|
||||
state.size = mouse_pos - position + 0.5 * corner_interact.rect.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.size = state.size.clamp(self.min_size..=self.max_size);
|
||||
state.size = state.size.max(self.min_size);
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
|
||||
let inner_rect = Rect::from_min_size(region.cursor(), state.size);
|
||||
let desired_size = {
|
||||
let mut contents_region = region.child_region(inner_rect);
|
||||
add_contents(&mut contents_region);
|
||||
contents_region.bounding_size()
|
||||
};
|
||||
let desired_size = desired_size.ceil(); // Avoid rounding errors in math
|
||||
|
||||
// ------------------------------
|
||||
|
||||
if self.auto_shrink_width {
|
||||
state.size.x = state.size.x.min(desired_size.x);
|
||||
}
|
||||
if self.auto_shrink_height {
|
||||
state.size.y = state.size.y.min(desired_size.y);
|
||||
}
|
||||
if self.expand_width_to_fit_content || is_new {
|
||||
state.size.x = state.size.x.max(desired_size.x);
|
||||
}
|
||||
if self.expand_height_to_fit_content || is_new {
|
||||
state.size.y = state.size.y.max(desired_size.y);
|
||||
}
|
||||
|
||||
state.size = state.size.max(self.min_size);
|
||||
// state.size = state.size.clamp(self.min_size..=self.max_size);
|
||||
state.size = state.size.round(); // TODO: round to pixels
|
||||
|
||||
region.reserve_space_without_padding(state.size);
|
||||
|
||||
// ------------------------------
|
||||
|
||||
paint_resize_corner(region, &corner_rect, &corner_interact);
|
||||
|
||||
if corner_interact.hovered || corner_interact.active {
|
||||
region.ctx().output.lock().cursor_icon = CursorIcon::ResizeNwSe;
|
||||
}
|
||||
|
||||
region.memory().resize.insert(id, state);
|
||||
}
|
||||
}
|
||||
|
||||
fn paint_resize_corner(region: &mut Region, rect: &Rect, interact: &InteractInfo) {
|
||||
let color = region.style().interact_stroke_color(&interact);
|
||||
let width = region.style().interact_stroke_width(&interact);
|
||||
|
||||
let corner = rect.right_bottom().round(); // TODO: round to pixels
|
||||
let mut w = 2.0;
|
||||
|
||||
while w < 12.0 {
|
||||
region.add_paint_cmd(PaintCmd::line_segment(
|
||||
(pos2(corner.x - w, corner.y), pos2(corner.x, corner.y - w)),
|
||||
color,
|
||||
width,
|
||||
));
|
||||
w += 4.0;
|
||||
}
|
||||
}
|
||||
160
emigui/src/containers/scroll_area.rs
Normal file
160
emigui/src/containers/scroll_area.rs
Normal file
@@ -0,0 +1,160 @@
|
||||
use crate::*;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub struct State {
|
||||
/// Positive offset means scrolling down/right
|
||||
pub offset: Vec2,
|
||||
}
|
||||
|
||||
pub struct ScrollArea {
|
||||
max_height: f32,
|
||||
}
|
||||
|
||||
impl Default for ScrollArea {
|
||||
fn default() -> Self {
|
||||
Self { max_height: 200.0 }
|
||||
}
|
||||
}
|
||||
|
||||
impl ScrollArea {
|
||||
pub fn max_height(mut self, max_height: f32) -> Self {
|
||||
self.max_height = max_height;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl ScrollArea {
|
||||
pub fn show(self, outer_region: &mut Region, add_contents: impl FnOnce(&mut Region)) {
|
||||
let ctx = outer_region.ctx().clone();
|
||||
|
||||
let scroll_area_id = outer_region.id.with("scroll_area");
|
||||
let mut state = ctx
|
||||
.memory
|
||||
.lock()
|
||||
.scroll_areas
|
||||
.get(&scroll_area_id)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
|
||||
// content: size of contents (generally large)
|
||||
// outer: size of scroll area including scroll bar(s)
|
||||
// inner: excluding scroll bar(s). The area we clip the contents to.
|
||||
|
||||
let scroll_bar_width = 16.0;
|
||||
|
||||
let outer_size = vec2(outer_region.available_width(), self.max_height);
|
||||
let outer_rect = Rect::from_min_size(outer_region.cursor, outer_size);
|
||||
|
||||
let inner_size = outer_size - vec2(scroll_bar_width, 0.0);
|
||||
let inner_rect = Rect::from_min_size(outer_region.cursor, inner_size);
|
||||
|
||||
let mut content_region =
|
||||
outer_region.child_region(Rect::from_min_size(outer_region.cursor(), inner_size));
|
||||
content_region.cursor -= state.offset;
|
||||
content_region.desired_rect = content_region.desired_rect.translate(-state.offset);
|
||||
add_contents(&mut content_region);
|
||||
let content_size = content_region.bounding_size();
|
||||
|
||||
let content_interact = ctx.interact(
|
||||
outer_region.layer,
|
||||
&inner_rect,
|
||||
Some(scroll_area_id.with("area")),
|
||||
);
|
||||
if content_interact.active {
|
||||
// Dragging scroll area to scroll:
|
||||
state.offset.y -= ctx.input.mouse_move.y;
|
||||
}
|
||||
|
||||
// TODO: check that nothing else is being inteacted with
|
||||
if ctx.contains_mouse_pos(outer_region.layer, &outer_rect)
|
||||
&& ctx.memory.lock().active_id.is_none()
|
||||
{
|
||||
state.offset.y -= ctx.input.scroll_delta.y;
|
||||
}
|
||||
|
||||
let show_scroll = content_size.y > inner_size.y;
|
||||
if show_scroll {
|
||||
let left = inner_rect.right() + 2.0;
|
||||
let right = outer_rect.right();
|
||||
let corner_radius = (right - left) / 2.0;
|
||||
let top = inner_rect.top();
|
||||
let bottom = inner_rect.bottom();
|
||||
|
||||
let outer_scroll_rect = Rect::from_min_max(
|
||||
pos2(left, inner_rect.top()),
|
||||
pos2(right, inner_rect.bottom()),
|
||||
);
|
||||
|
||||
let from_content =
|
||||
|content_y| remap_clamp(content_y, 0.0..=content_size.y, top..=bottom);
|
||||
|
||||
let handle_rect = Rect::from_min_max(
|
||||
pos2(left, from_content(state.offset.y)),
|
||||
pos2(right, from_content(state.offset.y + inner_rect.height())),
|
||||
);
|
||||
|
||||
// intentionally use same id for inside and outside of handle
|
||||
let interact_id = Some(scroll_area_id.with("vertical"));
|
||||
let handle_interact = ctx.interact(outer_region.layer, &handle_rect, interact_id);
|
||||
|
||||
if let Some(mouse_pos) = ctx.input.mouse_pos {
|
||||
if handle_interact.active {
|
||||
if inner_rect.top() <= mouse_pos.y && mouse_pos.y <= inner_rect.bottom() {
|
||||
state.offset.y +=
|
||||
ctx.input.mouse_move.y * content_size.y / inner_rect.height();
|
||||
}
|
||||
} else {
|
||||
// Check for mouse down outside handle:
|
||||
let scroll_bg_interact =
|
||||
ctx.interact(outer_region.layer, &outer_scroll_rect, interact_id);
|
||||
|
||||
if scroll_bg_interact.active {
|
||||
// Center scroll at mouse pos:
|
||||
let mpos_top = mouse_pos.y - handle_rect.height() / 2.0;
|
||||
state.offset.y = remap(mpos_top, top..=bottom, 0.0..=content_size.y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
state.offset.y = state.offset.y.max(0.0);
|
||||
state.offset.y = state.offset.y.min(content_size.y - inner_rect.height());
|
||||
|
||||
// Avoid frame-delay by calculating a new handle rect:
|
||||
let handle_rect = Rect::from_min_max(
|
||||
pos2(left, from_content(state.offset.y)),
|
||||
pos2(right, from_content(state.offset.y + inner_rect.height())),
|
||||
);
|
||||
|
||||
let style = outer_region.style();
|
||||
let handle_fill_color = style.interact_fill_color(&handle_interact);
|
||||
let handle_outline = style.interact_outline(&handle_interact);
|
||||
|
||||
outer_region.add_paint_cmd(PaintCmd::Rect {
|
||||
rect: outer_scroll_rect,
|
||||
corner_radius,
|
||||
fill_color: Some(color::BLACK),
|
||||
outline: None,
|
||||
});
|
||||
|
||||
outer_region.add_paint_cmd(PaintCmd::Rect {
|
||||
rect: handle_rect.expand(-2.0),
|
||||
corner_radius,
|
||||
fill_color: handle_fill_color,
|
||||
outline: handle_outline,
|
||||
});
|
||||
}
|
||||
|
||||
let size = content_size.min(content_region.clip_rect.size());
|
||||
outer_region.reserve_space_without_padding(size);
|
||||
|
||||
state.offset.y = state.offset.y.max(0.0);
|
||||
state.offset.y = state.offset.y.min(content_size.y - inner_rect.height());
|
||||
|
||||
outer_region
|
||||
.ctx()
|
||||
.memory
|
||||
.lock()
|
||||
.scroll_areas
|
||||
.insert(scroll_area_id, state);
|
||||
}
|
||||
}
|
||||
80
emigui/src/containers/window.rs
Normal file
80
emigui/src/containers/window.rs
Normal file
@@ -0,0 +1,80 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::{widgets::*, *};
|
||||
|
||||
use super::*;
|
||||
|
||||
// TODO: separate out resizing into a contained and reusable Resize-region.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Window {
|
||||
title: String,
|
||||
floating: Floating,
|
||||
frame: Frame,
|
||||
resize: Resize,
|
||||
}
|
||||
|
||||
impl Window {
|
||||
pub fn new(title: impl Into<String>) -> Self {
|
||||
let title = title.into();
|
||||
Self {
|
||||
title: title.clone(),
|
||||
floating: Floating::new(title),
|
||||
frame: Frame::default(),
|
||||
resize: Resize::default()
|
||||
.handle_offset(Vec2::splat(4.0))
|
||||
.auto_shrink_height(true),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn default_pos(mut self, default_pos: Pos2) -> Self {
|
||||
self.floating = self.floating.default_pos(default_pos);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn default_size(mut self, default_size: Vec2) -> Self {
|
||||
self.resize = self.resize.default_size(default_size);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn min_size(mut self, min_size: Vec2) -> Self {
|
||||
self.resize = self.resize.min_size(min_size);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn max_size(mut self, max_size: Vec2) -> Self {
|
||||
self.resize = self.resize.max_size(max_size);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn fixed_size(mut self, size: Vec2) -> Self {
|
||||
self.resize = self.resize.fixed_size(size);
|
||||
self
|
||||
}
|
||||
|
||||
/// Can you resize it with the mouse?
|
||||
/// Note that a window can still auto-resize
|
||||
pub fn resizable(mut self, resizable: bool) -> Self {
|
||||
self.resize = self.resize.resizable(resizable);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Window {
|
||||
pub fn show(self, ctx: &Arc<Context>, add_contents: impl FnOnce(&mut Region)) {
|
||||
let Window {
|
||||
title,
|
||||
floating,
|
||||
frame,
|
||||
resize,
|
||||
} = self;
|
||||
floating.show(ctx, |region| {
|
||||
frame.show(region, |region| {
|
||||
resize.show(region, |region| {
|
||||
region.add(Label::new(title).text_style(TextStyle::Heading));
|
||||
region.add(Separator::new().line_width(1.0)); // TODO: nicer way to split window title from contents
|
||||
add_contents(region);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user