use std::ops::RangeInclusive;
use crate::*;
#[derive(Clone, Copy, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
struct PanelState {
rect: Rect,
}
impl PanelState {
fn load(ctx: &Context, bar_id: Id) -> Option<Self> {
ctx.data().get_persisted(bar_id)
}
fn store(self, ctx: &Context, bar_id: Id) {
ctx.data().insert_persisted(bar_id, self);
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Side {
Left,
Right,
}
impl Side {
fn opposite(self) -> Self {
match self {
Side::Left => Self::Right,
Side::Right => Self::Left,
}
}
fn set_rect_width(self, rect: &mut Rect, width: f32) {
match self {
Side::Left => rect.max.x = rect.min.x + width,
Side::Right => rect.min.x = rect.max.x - width,
}
}
fn side_x(self, rect: Rect) -> f32 {
match self {
Side::Left => rect.left(),
Side::Right => rect.right(),
}
}
}
#[must_use = "You should call .show()"]
pub struct SidePanel {
side: Side,
id: Id,
frame: Option<Frame>,
resizable: bool,
default_width: f32,
width_range: RangeInclusive<f32>,
}
impl SidePanel {
pub fn left(id_source: impl std::hash::Hash) -> Self {
Self::new(Side::Left, id_source)
}
pub fn right(id_source: impl std::hash::Hash) -> Self {
Self::new(Side::Right, id_source)
}
pub fn new(side: Side, id_source: impl std::hash::Hash) -> Self {
Self {
side,
id: Id::new(id_source),
frame: None,
resizable: true,
default_width: 200.0,
width_range: 96.0..=f32::INFINITY,
}
}
pub fn resizable(mut self, resizable: bool) -> Self {
self.resizable = resizable;
self
}
pub fn default_width(mut self, default_width: f32) -> Self {
self.default_width = default_width;
self
}
pub fn min_width(mut self, min_width: f32) -> Self {
self.width_range = min_width..=(*self.width_range.end());
self
}
pub fn max_width(mut self, max_width: f32) -> Self {
self.width_range = (*self.width_range.start())..=max_width;
self
}
pub fn width_range(mut self, width_range: RangeInclusive<f32>) -> Self {
self.width_range = width_range;
self
}
pub fn frame(mut self, frame: Frame) -> Self {
self.frame = Some(frame);
self
}
}
impl SidePanel {
pub fn show_inside<R>(
self,
ui: &mut Ui,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> InnerResponse<R> {
self.show_inside_dyn(ui, Box::new(add_contents))
}
fn show_inside_dyn<'c, R>(
self,
ui: &mut Ui,
add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
) -> InnerResponse<R> {
let Self {
side,
id,
frame,
resizable,
default_width,
width_range,
} = self;
let available_rect = ui.available_rect_before_wrap();
let mut panel_rect = available_rect;
{
let mut width = default_width;
if let Some(state) = PanelState::load(ui.ctx(), id) {
width = state.rect.width();
}
width = clamp_to_range(width, width_range.clone()).at_most(available_rect.width());
side.set_rect_width(&mut panel_rect, width);
}
let mut resize_hover = false;
let mut is_resizing = false;
if resizable {
let resize_id = id.with("__resize");
if let Some(pointer) = ui.ctx().pointer_latest_pos() {
let we_are_on_top = ui
.ctx()
.layer_id_at(pointer)
.map_or(true, |top_layer_id| top_layer_id == ui.layer_id());
let resize_x = side.opposite().side_x(panel_rect);
let mouse_over_resize_line = we_are_on_top
&& panel_rect.y_range().contains(&pointer.y)
&& (resize_x - pointer.x).abs()
<= ui.style().interaction.resize_grab_radius_side;
if ui.input().pointer.any_pressed()
&& ui.input().pointer.any_down()
&& mouse_over_resize_line
{
ui.memory().set_dragged_id(resize_id);
}
is_resizing = ui.memory().is_being_dragged(resize_id);
if is_resizing {
let width = (pointer.x - side.side_x(panel_rect)).abs();
let width =
clamp_to_range(width, width_range.clone()).at_most(available_rect.width());
side.set_rect_width(&mut panel_rect, width);
}
let dragging_something_else =
ui.input().pointer.any_down() || ui.input().pointer.any_pressed();
resize_hover = mouse_over_resize_line && !dragging_something_else;
if resize_hover || is_resizing {
ui.output().cursor_icon = CursorIcon::ResizeHorizontal;
}
}
}
let mut panel_ui = ui.child_ui_with_id_source(panel_rect, Layout::top_down(Align::Min), id);
panel_ui.expand_to_include_rect(panel_rect);
let frame = frame.unwrap_or_else(|| Frame::side_top_panel(ui.style()));
let inner_response = frame.show(&mut panel_ui, |ui| {
ui.set_min_height(ui.max_rect().height()); ui.set_min_width(*width_range.start());
add_contents(ui)
});
let rect = inner_response.response.rect;
{
let mut cursor = ui.cursor();
match side {
Side::Left => {
cursor.min.x = rect.max.x + ui.spacing().item_spacing.x;
}
Side::Right => {
cursor.max.x = rect.min.x - ui.spacing().item_spacing.x;
}
}
ui.set_cursor(cursor);
}
ui.expand_to_include_rect(rect);
PanelState { rect }.store(ui.ctx(), id);
if resize_hover || is_resizing {
let stroke = if is_resizing {
ui.style().visuals.widgets.active.bg_stroke
} else {
ui.style().visuals.widgets.hovered.bg_stroke
};
let resize_layer = LayerId::new(Order::Foreground, Id::new("panel_resize"));
let resize_x = side.opposite().side_x(rect);
ui.ctx()
.layer_painter(resize_layer)
.vline(resize_x, rect.y_range(), stroke);
}
inner_response
}
pub fn show<R>(
self,
ctx: &Context,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> InnerResponse<R> {
self.show_dyn(ctx, Box::new(add_contents))
}
fn show_dyn<'c, R>(
self,
ctx: &Context,
add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
) -> InnerResponse<R> {
let layer_id = LayerId::background();
let side = self.side;
let available_rect = ctx.available_rect();
let clip_rect = ctx.input().screen_rect();
let mut panel_ui = Ui::new(ctx.clone(), layer_id, self.id, available_rect, clip_rect);
let inner_response = self.show_inside_dyn(&mut panel_ui, add_contents);
let rect = inner_response.response.rect;
match side {
Side::Left => ctx
.frame_state()
.allocate_left_panel(Rect::from_min_max(available_rect.min, rect.max)),
Side::Right => ctx
.frame_state()
.allocate_right_panel(Rect::from_min_max(rect.min, available_rect.max)),
}
inner_response
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum TopBottomSide {
Top,
Bottom,
}
impl TopBottomSide {
fn opposite(self) -> Self {
match self {
TopBottomSide::Top => Self::Bottom,
TopBottomSide::Bottom => Self::Top,
}
}
fn set_rect_height(self, rect: &mut Rect, height: f32) {
match self {
TopBottomSide::Top => rect.max.y = rect.min.y + height,
TopBottomSide::Bottom => rect.min.y = rect.max.y - height,
}
}
fn side_y(self, rect: Rect) -> f32 {
match self {
TopBottomSide::Top => rect.top(),
TopBottomSide::Bottom => rect.bottom(),
}
}
}
#[must_use = "You should call .show()"]
pub struct TopBottomPanel {
side: TopBottomSide,
id: Id,
frame: Option<Frame>,
resizable: bool,
default_height: Option<f32>,
height_range: RangeInclusive<f32>,
}
impl TopBottomPanel {
pub fn top(id_source: impl std::hash::Hash) -> Self {
Self::new(TopBottomSide::Top, id_source)
}
pub fn bottom(id_source: impl std::hash::Hash) -> Self {
Self::new(TopBottomSide::Bottom, id_source)
}
pub fn new(side: TopBottomSide, id_source: impl std::hash::Hash) -> Self {
Self {
side,
id: Id::new(id_source),
frame: None,
resizable: false,
default_height: None,
height_range: 20.0..=f32::INFINITY,
}
}
pub fn resizable(mut self, resizable: bool) -> Self {
self.resizable = resizable;
self
}
pub fn default_height(mut self, default_height: f32) -> Self {
self.default_height = Some(default_height);
self
}
pub fn min_height(mut self, min_height: f32) -> Self {
self.height_range = min_height..=(*self.height_range.end());
self
}
pub fn max_height(mut self, max_height: f32) -> Self {
self.height_range = (*self.height_range.start())..=max_height;
self
}
pub fn height_range(mut self, height_range: RangeInclusive<f32>) -> Self {
self.height_range = height_range;
self
}
pub fn frame(mut self, frame: Frame) -> Self {
self.frame = Some(frame);
self
}
}
impl TopBottomPanel {
pub fn show_inside<R>(
self,
ui: &mut Ui,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> InnerResponse<R> {
self.show_inside_dyn(ui, Box::new(add_contents))
}
fn show_inside_dyn<'c, R>(
self,
ui: &mut Ui,
add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
) -> InnerResponse<R> {
let Self {
side,
id,
frame,
resizable,
default_height,
height_range,
} = self;
let available_rect = ui.available_rect_before_wrap();
let mut panel_rect = available_rect;
{
let mut height = if let Some(state) = PanelState::load(ui.ctx(), id) {
state.rect.height()
} else {
default_height.unwrap_or_else(|| ui.style().spacing.interact_size.y)
};
height = clamp_to_range(height, height_range.clone()).at_most(available_rect.height());
side.set_rect_height(&mut panel_rect, height);
}
let mut resize_hover = false;
let mut is_resizing = false;
if resizable {
let resize_id = id.with("__resize");
let latest_pos = ui.input().pointer.latest_pos();
if let Some(pointer) = latest_pos {
let we_are_on_top = ui
.ctx()
.layer_id_at(pointer)
.map_or(true, |top_layer_id| top_layer_id == ui.layer_id());
let resize_y = side.opposite().side_y(panel_rect);
let mouse_over_resize_line = we_are_on_top
&& panel_rect.x_range().contains(&pointer.x)
&& (resize_y - pointer.y).abs()
<= ui.style().interaction.resize_grab_radius_side;
if ui.input().pointer.any_pressed()
&& ui.input().pointer.any_down()
&& mouse_over_resize_line
{
ui.memory().interaction.drag_id = Some(resize_id);
}
is_resizing = ui.memory().interaction.drag_id == Some(resize_id);
if is_resizing {
let height = (pointer.y - side.side_y(panel_rect)).abs();
let height = clamp_to_range(height, height_range.clone())
.at_most(available_rect.height());
side.set_rect_height(&mut panel_rect, height);
}
let dragging_something_else =
ui.input().pointer.any_down() || ui.input().pointer.any_pressed();
resize_hover = mouse_over_resize_line && !dragging_something_else;
if resize_hover || is_resizing {
ui.output().cursor_icon = CursorIcon::ResizeVertical;
}
}
}
let mut panel_ui = ui.child_ui_with_id_source(panel_rect, Layout::top_down(Align::Min), id);
panel_ui.expand_to_include_rect(panel_rect);
let frame = frame.unwrap_or_else(|| Frame::side_top_panel(ui.style()));
let inner_response = frame.show(&mut panel_ui, |ui| {
ui.set_min_width(ui.max_rect().width()); ui.set_min_height(*height_range.start());
add_contents(ui)
});
let rect = inner_response.response.rect;
{
let mut cursor = ui.cursor();
match side {
TopBottomSide::Top => {
cursor.min.y = rect.max.y + ui.spacing().item_spacing.y;
}
TopBottomSide::Bottom => {
cursor.max.y = rect.min.y - ui.spacing().item_spacing.y;
}
}
ui.set_cursor(cursor);
}
ui.expand_to_include_rect(rect);
PanelState { rect }.store(ui.ctx(), id);
if resize_hover || is_resizing {
let stroke = if is_resizing {
ui.style().visuals.widgets.active.bg_stroke
} else {
ui.style().visuals.widgets.hovered.bg_stroke
};
let resize_layer = LayerId::new(Order::Foreground, Id::new("panel_resize"));
let resize_y = side.opposite().side_y(rect);
ui.ctx()
.layer_painter(resize_layer)
.hline(rect.x_range(), resize_y, stroke);
}
inner_response
}
pub fn show<R>(
self,
ctx: &Context,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> InnerResponse<R> {
self.show_dyn(ctx, Box::new(add_contents))
}
fn show_dyn<'c, R>(
self,
ctx: &Context,
add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
) -> InnerResponse<R> {
let layer_id = LayerId::background();
let available_rect = ctx.available_rect();
let side = self.side;
let clip_rect = ctx.input().screen_rect();
let mut panel_ui = Ui::new(ctx.clone(), layer_id, self.id, available_rect, clip_rect);
let inner_response = self.show_inside_dyn(&mut panel_ui, add_contents);
let rect = inner_response.response.rect;
match side {
TopBottomSide::Top => {
ctx.frame_state()
.allocate_top_panel(Rect::from_min_max(available_rect.min, rect.max));
}
TopBottomSide::Bottom => {
ctx.frame_state()
.allocate_bottom_panel(Rect::from_min_max(rect.min, available_rect.max));
}
}
inner_response
}
}
#[must_use = "You should call .show()"]
#[derive(Default)]
pub struct CentralPanel {
frame: Option<Frame>,
}
impl CentralPanel {
pub fn frame(mut self, frame: Frame) -> Self {
self.frame = Some(frame);
self
}
}
impl CentralPanel {
pub fn show_inside<R>(
self,
ui: &mut Ui,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> InnerResponse<R> {
self.show_inside_dyn(ui, Box::new(add_contents))
}
fn show_inside_dyn<'c, R>(
self,
ui: &mut Ui,
add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
) -> InnerResponse<R> {
let Self { frame } = self;
let panel_rect = ui.available_rect_before_wrap();
let mut panel_ui = ui.child_ui(panel_rect, Layout::top_down(Align::Min));
let frame = frame.unwrap_or_else(|| Frame::central_panel(ui.style()));
frame.show(&mut panel_ui, |ui| {
ui.expand_to_include_rect(ui.max_rect()); add_contents(ui)
})
}
pub fn show<R>(
self,
ctx: &Context,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> InnerResponse<R> {
self.show_dyn(ctx, Box::new(add_contents))
}
fn show_dyn<'c, R>(
self,
ctx: &Context,
add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
) -> InnerResponse<R> {
let available_rect = ctx.available_rect();
let layer_id = LayerId::background();
let id = Id::new("central_panel");
let clip_rect = ctx.input().screen_rect();
let mut panel_ui = Ui::new(ctx.clone(), layer_id, id, available_rect, clip_rect);
let inner_response = self.show_inside_dyn(&mut panel_ui, add_contents);
ctx.frame_state()
.allocate_central_panel(inner_response.response.rect);
inner_response
}
}
fn clamp_to_range(x: f32, range: RangeInclusive<f32>) -> f32 {
x.clamp(
range.start().min(*range.end()),
range.start().max(*range.end()),
)
}