use emath::GuiRounding as _;
use epaint::CornerRadiusF32;
use crate::collapsing_header::CollapsingState;
use crate::*;
use super::scroll_area::{DragScroll, ScrollBarVisibility, ScrollSource};
use super::{Area, Frame, Resize, ScrollArea, area, resize};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub enum WindowDrag {
Off,
Anywhere,
TitleBar,
#[default]
OnTouch,
}
impl WindowDrag {
fn resolve(self, ctx: &Context) -> Self {
match self {
Self::OnTouch => {
if ctx.input(|i| i.has_touch_screen()) {
Self::Anywhere
} else {
Self::TitleBar
}
}
other => other,
}
}
}
#[must_use = "You should call .show()"]
pub struct Window<'a> {
title: Atoms<'a>,
open: Option<&'a mut bool>,
area: Area,
frame: Option<Frame>,
resize: Resize,
scroll: ScrollArea,
collapsible: bool,
default_open: bool,
with_title_bar: bool,
fade_out: bool,
auto_sized: bool,
drag_area: WindowDrag,
}
impl<'a> Window<'a> {
pub fn new(title: impl IntoAtoms<'a>) -> Self {
let title: Atoms<'_> = title.into_atoms();
let area = Area::new(Id::new(title.text())).kind(UiKind::Window);
Self {
title,
open: None,
area,
frame: None,
resize: Resize::default()
.with_stroke(false)
.min_size([96.0, 32.0])
.default_size([340.0, 420.0]), scroll: ScrollArea::neither().auto_shrink(false).content_margin(0.0),
collapsible: true,
default_open: true,
with_title_bar: true,
fade_out: true,
auto_sized: false,
drag_area: WindowDrag::default(),
}
}
pub fn from_viewport(id: ViewportId, viewport: ViewportBuilder) -> Self {
let ViewportBuilder {
title,
app_id,
inner_size,
min_inner_size,
max_inner_size,
resizable,
decorations,
title_shown,
minimize_button,
.. } = viewport;
let mut window = Self::new(title.or(app_id).unwrap_or_else(String::new)).id(Id::new(id));
if let Some(inner_size) = inner_size {
window = window.default_size(inner_size);
}
if let Some(min_inner_size) = min_inner_size {
window = window.min_size(min_inner_size);
}
if let Some(max_inner_size) = max_inner_size {
window = window.max_size(max_inner_size);
}
if let Some(resizable) = resizable {
window = window.resizable(resizable);
}
window = window.title_bar(decorations.unwrap_or(true) && title_shown.unwrap_or(true));
window = window.collapsible(minimize_button.unwrap_or(true));
window
}
#[inline]
pub fn id(mut self, id: Id) -> Self {
self.area = self.area.id(id);
self
}
#[inline]
pub fn open(mut self, open: &'a mut bool) -> Self {
self.open = Some(open);
self
}
#[inline]
pub fn enabled(mut self, enabled: bool) -> Self {
self.area = self.area.enabled(enabled);
self
}
#[inline]
pub fn interactable(mut self, interactable: bool) -> Self {
self.area = self.area.interactable(interactable);
self
}
#[inline]
pub fn movable(mut self, movable: bool) -> Self {
self.area = self.area.movable(movable);
self
}
#[inline]
pub fn drag_area(mut self, drag_area: WindowDrag) -> Self {
self.drag_area = drag_area;
self
}
#[inline]
pub fn order(mut self, order: Order) -> Self {
self.area = self.area.order(order);
self
}
#[inline]
pub fn fade_in(mut self, fade_in: bool) -> Self {
self.area = self.area.fade_in(fade_in);
self
}
#[inline]
pub fn fade_out(mut self, fade_out: bool) -> Self {
self.fade_out = fade_out;
self
}
#[inline]
pub fn mutate(mut self, mutate: impl Fn(&mut Self)) -> Self {
mutate(&mut self);
self
}
#[inline]
pub fn resize(mut self, mutate: impl Fn(Resize) -> Resize) -> Self {
self.resize = mutate(self.resize);
self
}
#[inline]
pub fn frame(mut self, frame: Frame) -> Self {
self.frame = Some(frame);
self
}
#[inline]
pub fn min_width(mut self, min_width: f32) -> Self {
self.resize = self.resize.min_width(min_width);
self
}
#[inline]
pub fn min_height(mut self, min_height: f32) -> Self {
self.resize = self.resize.min_height(min_height);
self
}
#[inline]
pub fn min_size(mut self, min_size: impl Into<Vec2>) -> Self {
self.resize = self.resize.min_size(min_size);
self
}
#[inline]
pub fn max_width(mut self, max_width: f32) -> Self {
self.resize = self.resize.max_width(max_width);
self
}
#[inline]
pub fn max_height(mut self, max_height: f32) -> Self {
self.resize = self.resize.max_height(max_height);
self
}
#[inline]
pub fn max_size(mut self, max_size: impl Into<Vec2>) -> Self {
self.resize = self.resize.max_size(max_size);
self
}
#[inline]
pub fn current_pos(mut self, current_pos: impl Into<Pos2>) -> Self {
self.area = self.area.current_pos(current_pos);
self
}
#[inline]
pub fn default_pos(mut self, default_pos: impl Into<Pos2>) -> Self {
self.area = self.area.default_pos(default_pos);
self
}
#[inline]
pub fn fixed_pos(mut self, pos: impl Into<Pos2>) -> Self {
self.area = self.area.fixed_pos(pos);
self
}
#[inline]
pub fn constrain(mut self, constrain: bool) -> Self {
self.area = self.area.constrain(constrain);
self
}
#[inline]
pub fn constrain_to(mut self, constrain_rect: Rect) -> Self {
self.area = self.area.constrain_to(constrain_rect);
self
}
#[inline]
pub fn pivot(mut self, pivot: Align2) -> Self {
self.area = self.area.pivot(pivot);
self
}
#[inline]
pub fn anchor(mut self, align: Align2, offset: impl Into<Vec2>) -> Self {
self.area = self.area.anchor(align, offset);
self
}
#[inline]
pub fn default_open(mut self, default_open: bool) -> Self {
self.default_open = default_open;
self
}
#[inline]
pub fn default_size(mut self, default_size: impl Into<Vec2>) -> Self {
let default_size: Vec2 = default_size.into();
self.resize = self.resize.default_size(default_size);
self.area = self.area.default_size(default_size);
self
}
#[inline]
pub fn default_width(mut self, default_width: f32) -> Self {
self.resize = self.resize.default_width(default_width);
self.area = self.area.default_width(default_width);
self
}
#[inline]
pub fn default_height(mut self, default_height: f32) -> Self {
self.resize = self.resize.default_height(default_height);
self.area = self.area.default_height(default_height);
self
}
#[inline]
pub fn fixed_size(mut self, size: impl Into<Vec2>) -> Self {
self.resize = self.resize.fixed_size(size);
self
}
pub fn default_rect(self, rect: Rect) -> Self {
self.default_pos(rect.min).default_size(rect.size())
}
pub fn fixed_rect(self, rect: Rect) -> Self {
self.fixed_pos(rect.min).fixed_size(rect.size())
}
#[inline]
pub fn resizable(mut self, resizable: impl Into<Vec2b>) -> Self {
let resizable = resizable.into();
self.resize = self.resize.resizable(resizable);
self
}
#[inline]
pub fn collapsible(mut self, collapsible: bool) -> Self {
self.collapsible = collapsible;
self
}
#[inline]
pub fn title_bar(mut self, title_bar: bool) -> Self {
self.with_title_bar = title_bar;
self
}
#[inline]
pub fn auto_sized(mut self) -> Self {
self.resize = self.resize.auto_sized();
self.scroll = ScrollArea::neither();
self.auto_sized = true;
self
}
#[inline]
pub fn scroll(mut self, scroll: impl Into<Vec2b>) -> Self {
self.scroll = self.scroll.scroll(scroll);
self
}
#[inline]
pub fn hscroll(mut self, hscroll: bool) -> Self {
self.scroll = self.scroll.hscroll(hscroll);
self
}
#[inline]
pub fn vscroll(mut self, vscroll: bool) -> Self {
self.scroll = self.scroll.vscroll(vscroll);
self
}
#[inline]
pub fn drag_to_scroll(mut self, drag_to_scroll: DragScroll) -> Self {
self.scroll = self.scroll.scroll_source(ScrollSource {
drag: drag_to_scroll,
..Default::default()
});
self
}
#[inline]
pub fn scroll_bar_visibility(mut self, visibility: ScrollBarVisibility) -> Self {
self.scroll = self.scroll.scroll_bar_visibility(visibility);
self
}
}
impl Window<'_> {
#[inline]
pub fn show<R>(
self,
ctx: &Context,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> Option<InnerResponse<Option<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>,
) -> Option<InnerResponse<Option<R>>> {
let Window {
title,
mut open,
area,
frame,
resize,
scroll,
collapsible,
default_open,
with_title_bar,
fade_out,
auto_sized,
drag_area: drag_area_setting,
} = self;
let effective_drag = if !area.is_movable() || drag_area_setting == WindowDrag::Off {
WindowDrag::Off
} else if !with_title_bar {
WindowDrag::Anywhere
} else {
drag_area_setting.resolve(ctx)
};
let area = if effective_drag == WindowDrag::Off {
area.movable(false)
} else {
area
};
let title_drag_mode = effective_drag == WindowDrag::TitleBar;
let pivot_pos_before_begin = if title_drag_mode {
if let Some(resp) = ctx.read_response(area.id.with("__title_click"))
&& resp.dragged()
{
let delta = ctx.input(|i| i.pointer.delta());
if delta != Vec2::ZERO {
ctx.memory_mut(|mem| {
if let Some(state) = mem.areas_mut().get_mut(area.id)
&& let Some(pivot_pos) = state.pivot_pos.as_mut()
{
*pivot_pos += delta;
}
});
}
}
area::AreaState::load(ctx, area.id).and_then(|s| s.pivot_pos)
} else {
None
};
let style = ctx.global_style();
let window_frame = frame.unwrap_or_else(|| Frame::window(&style));
let window_margin = window_frame.inner_margin;
let window_frame = window_frame.inner_margin(0.0);
let is_explicitly_closed = matches!(open, Some(false));
let is_open = !is_explicitly_closed || ctx.memory(|mem| mem.everything_is_visible());
let opacity = ctx.animate_bool_with_easing(
area.id.with("fade-out"),
is_open,
emath::easing::cubic_out,
);
if opacity <= 0.0 {
return None;
}
let area_id = area.id;
let area_layer_id = area.layer();
let resize_id = area_id.with("resize");
let mut collapsing =
CollapsingState::load_with_default_open(ctx, area_id.with("collapsing"), default_open);
let is_collapsed = with_title_bar && !collapsing.is_open();
let possible = PossibleInteractions::new(&area, &resize, is_collapsed);
let resize = resize.resizable(false); let mut resize = resize.id(resize_id);
let on_top = Some(area_layer_id) == ctx.top_layer_id();
let mut area = area.begin(ctx);
if let Some(pre_begin_pivot) = pivot_pos_before_begin {
let constrain = area.constrain();
let constrain_rect = area.constrain_rect();
let state = area.state_mut();
state.pivot_pos = Some(pre_begin_pivot);
if constrain {
state.set_left_top_pos(
Context::constrain_window_rect_to_area(state.rect(), constrain_rect).min,
);
}
state.set_left_top_pos(area::round_area_position(ctx, state.left_top_pos()));
}
area.with_widget_info(|| {
WidgetInfo::labeled(
WidgetType::Window,
true,
title.text().as_deref().unwrap_or(""),
)
});
{
let constrain_rect = area.constrain_rect();
let max_width = constrain_rect.width();
let max_height = constrain_rect.height();
resize.max_size.x = resize.max_size.x.min(max_width);
resize.max_size.y = resize.max_size.y.min(max_height);
}
{
let frame_margin = window_frame.total_margin().sum();
resize.min_size = (resize.min_size - frame_margin).at_least(Vec2::ZERO);
resize.max_size = (resize.max_size - frame_margin).at_least(Vec2::ZERO);
resize.default_size = (resize.default_size - frame_margin).at_least(Vec2::ZERO);
}
let mut area_content_ui = area.content_ui(ctx);
if is_open {
} else if fade_out {
area_content_ui.multiply_opacity(opacity);
}
let content_inner = {
let outer_response = window_frame.show(&mut area_content_ui, |ui| {
resize.show(ui, |ui| {
if with_title_bar {
title_ui(
ui,
title,
window_frame.inner_margin(window_margin),
&mut collapsing,
collapsible,
on_top,
open.as_deref_mut(),
auto_sized,
effective_drag == WindowDrag::TitleBar,
area_id,
);
}
collapsing
.show_body_unindented(ui, |ui| {
if scroll.is_any_scroll_enabled() {
scroll
.content_margin(window_margin)
.show(ui, add_contents)
.inner
} else {
crate::Frame::NONE
.inner_margin(window_margin)
.show(ui, add_contents)
.inner
}
})
.map(|inner| inner.inner)
})
});
let outer_rect = outer_response.response.rect;
let resize_interaction = do_resize_interaction(
ctx,
possible,
area.id(),
area_layer_id,
outer_rect,
window_frame,
);
paint_resize_corner(
&area_content_ui,
&possible,
outer_rect,
&window_frame,
resize_interaction,
);
{
let margins = window_frame.total_margin().sum();
resize_response(
resize_interaction,
ctx,
margins,
area_layer_id,
&mut area,
resize_id,
);
}
collapsing.store(ctx);
paint_frame_interaction(&area_content_ui, outer_rect, resize_interaction);
outer_response.inner
};
let full_response = area.end(ctx, area_content_ui);
if full_response.should_close()
&& let Some(open) = open
{
*open = false;
}
let inner_response = InnerResponse {
inner: content_inner,
response: full_response,
};
Some(inner_response)
}
}
fn paint_resize_corner(
ui: &Ui,
possible: &PossibleInteractions,
outer_rect: Rect,
window_frame: &Frame,
i: ResizeInteraction,
) {
let cr = window_frame.corner_radius;
let (corner, radius, corner_response) = if possible.resize_right && possible.resize_bottom {
(Align2::RIGHT_BOTTOM, cr.se, i.right & i.bottom)
} else if possible.resize_left && possible.resize_bottom {
(Align2::LEFT_BOTTOM, cr.sw, i.left & i.bottom)
} else if possible.resize_left && possible.resize_top {
(Align2::LEFT_TOP, cr.nw, i.left & i.top)
} else if possible.resize_right && possible.resize_top {
(Align2::RIGHT_TOP, cr.ne, i.right & i.top)
} else {
if possible.resize_right || possible.resize_bottom {
(Align2::RIGHT_BOTTOM, cr.se, i.right & i.bottom)
} else if possible.resize_left || possible.resize_bottom {
(Align2::LEFT_BOTTOM, cr.sw, i.left & i.bottom)
} else if possible.resize_left || possible.resize_top {
(Align2::LEFT_TOP, cr.nw, i.left & i.top)
} else if possible.resize_right || possible.resize_top {
(Align2::RIGHT_TOP, cr.ne, i.right & i.top)
} else {
return;
}
};
let radius = radius as f32;
let offset =
((2.0_f32.sqrt() * (1.0 + radius) - radius) * 45.0_f32.to_radians().cos()).max(2.0);
let stroke = if corner_response.drag {
ui.visuals().widgets.active.fg_stroke
} else if corner_response.hover {
ui.visuals().widgets.hovered.fg_stroke
} else {
window_frame.stroke
};
let fill_rect = outer_rect.shrink(window_frame.stroke.width);
let corner_size = Vec2::splat(ui.visuals().resize_corner_size);
let corner_rect = corner.align_size_within_rect(corner_size, fill_rect);
let corner_rect = corner_rect.translate(-offset * corner.to_sign()); crate::resize::paint_resize_corner_with_style(ui, &corner_rect, stroke.color, corner);
}
#[derive(Clone, Copy, Debug)]
struct PossibleInteractions {
resize_left: bool,
resize_right: bool,
resize_top: bool,
resize_bottom: bool,
}
impl PossibleInteractions {
fn new(area: &Area, resize: &Resize, is_collapsed: bool) -> Self {
let movable = area.is_enabled() && area.is_movable();
let resizable = resize
.is_resizable()
.and(area.is_enabled() && !is_collapsed);
let pivot = area.get_pivot();
Self {
resize_left: resizable.x && (movable || pivot.x() != Align::LEFT),
resize_right: resizable.x && (movable || pivot.x() != Align::RIGHT),
resize_top: resizable.y && (movable || pivot.y() != Align::TOP),
resize_bottom: resizable.y && (movable || pivot.y() != Align::BOTTOM),
}
}
pub fn resizable(&self) -> bool {
self.resize_left || self.resize_right || self.resize_top || self.resize_bottom
}
}
#[derive(Clone, Copy, Debug)]
struct ResizeInteraction {
outer_rect: Rect,
window_frame: Frame,
left: SideResponse,
right: SideResponse,
top: SideResponse,
bottom: SideResponse,
}
#[derive(Clone, Copy, Debug, Default)]
struct SideResponse {
hover: bool,
drag: bool,
}
impl SideResponse {
pub fn any(&self) -> bool {
self.hover || self.drag
}
}
impl std::ops::BitAnd for SideResponse {
type Output = Self;
fn bitand(self, rhs: Self) -> Self::Output {
Self {
hover: self.hover && rhs.hover,
drag: self.drag && rhs.drag,
}
}
}
impl std::ops::BitOrAssign for SideResponse {
fn bitor_assign(&mut self, rhs: Self) {
*self = Self {
hover: self.hover || rhs.hover,
drag: self.drag || rhs.drag,
};
}
}
impl ResizeInteraction {
pub fn set_cursor(&self, ctx: &Context) {
let left = self.left.any();
let right = self.right.any();
let top = self.top.any();
let bottom = self.bottom.any();
if (left && top) || (right && bottom) {
ctx.set_cursor_icon(CursorIcon::ResizeNwSe);
} else if (right && top) || (left && bottom) {
ctx.set_cursor_icon(CursorIcon::ResizeNeSw);
} else if left || right {
ctx.set_cursor_icon(CursorIcon::ResizeHorizontal);
} else if bottom || top {
ctx.set_cursor_icon(CursorIcon::ResizeVertical);
}
}
pub fn any_hovered(&self) -> bool {
self.left.hover || self.right.hover || self.top.hover || self.bottom.hover
}
pub fn any_dragged(&self) -> bool {
self.left.drag || self.right.drag || self.top.drag || self.bottom.drag
}
}
fn resize_response(
resize_interaction: ResizeInteraction,
ctx: &Context,
margins: Vec2,
area_layer_id: LayerId,
area: &mut area::Prepared,
resize_id: Id,
) {
let Some(mut new_rect) = move_and_resize_window(ctx, resize_id, &resize_interaction) else {
return;
};
if area.constrain() {
new_rect = Context::constrain_window_rect_to_area(new_rect, area.constrain_rect());
}
area.state_mut().set_left_top_pos(new_rect.left_top());
if resize_interaction.any_dragged()
&& let Some(mut state) = resize::State::load(ctx, resize_id)
{
state.requested_size = Some(new_rect.size() - margins);
state.store(ctx, resize_id);
}
ctx.memory_mut(|mem| mem.areas_mut().move_to_top(area_layer_id));
}
fn move_and_resize_window(ctx: &Context, id: Id, interaction: &ResizeInteraction) -> Option<Rect> {
let rect_at_start_of_drag_id = id.with("window_rect_at_drag_start");
if !interaction.any_dragged() {
ctx.data_mut(|data| {
data.remove::<Rect>(rect_at_start_of_drag_id);
});
return None;
}
let total_drag_delta = ctx.input(|i| i.pointer.total_drag_delta())?;
let rect_at_start_of_drag = ctx.data_mut(|data| {
*data.get_temp_mut_or::<Rect>(rect_at_start_of_drag_id, interaction.outer_rect)
});
let mut rect = rect_at_start_of_drag;
rect = rect.shrink(interaction.window_frame.stroke.width / 2.0);
if interaction.left.drag {
rect.min.x += total_drag_delta.x;
} else if interaction.right.drag {
rect.max.x += total_drag_delta.x;
}
if interaction.top.drag {
rect.min.y += total_drag_delta.y;
} else if interaction.bottom.drag {
rect.max.y += total_drag_delta.y;
}
rect = rect.expand(interaction.window_frame.stroke.width / 2.0);
Some(rect.round_ui())
}
fn do_resize_interaction(
ctx: &Context,
possible: PossibleInteractions,
accessibility_parent: Id,
layer_id: LayerId,
outer_rect: Rect,
window_frame: Frame,
) -> ResizeInteraction {
if !possible.resizable() {
return ResizeInteraction {
outer_rect,
window_frame,
left: Default::default(),
right: Default::default(),
top: Default::default(),
bottom: Default::default(),
};
}
let rect = outer_rect.shrink(window_frame.stroke.width / 2.0);
let side_response = |rect, id| {
ctx.register_accesskit_parent(id, accessibility_parent);
let response = ctx.create_widget(
WidgetRect {
layer_id,
id,
parent_id: layer_id.id,
rect,
interact_rect: rect,
sense: Sense::DRAG, enabled: true,
},
true,
InteractOptions {
move_to_top: true,
},
);
response.widget_info(|| WidgetInfo::new(crate::WidgetType::ResizeHandle));
SideResponse {
hover: response.hovered(),
drag: response.dragged(),
}
};
let id = Id::new(layer_id).with("edge_drag");
let style = ctx.global_style();
let side_grab_radius = style.interaction.resize_grab_radius_side;
let corner_grab_radius = style.interaction.resize_grab_radius_corner;
let vertical_rect = |a: Pos2, b: Pos2| {
Rect::from_min_max(a, b).expand2(vec2(side_grab_radius, -corner_grab_radius))
};
let horizontal_rect = |a: Pos2, b: Pos2| {
Rect::from_min_max(a, b).expand2(vec2(-corner_grab_radius, side_grab_radius))
};
let corner_rect =
|center: Pos2| Rect::from_center_size(center, Vec2::splat(2.0 * corner_grab_radius));
let [mut left, mut right, mut top, mut bottom] = [SideResponse::default(); 4];
if possible.resize_right {
let response = side_response(
vertical_rect(rect.right_top(), rect.right_bottom()),
id.with("right"),
);
right |= response;
}
if possible.resize_left {
let response = side_response(
vertical_rect(rect.left_top(), rect.left_bottom()),
id.with("left"),
);
left |= response;
}
if possible.resize_bottom {
let response = side_response(
horizontal_rect(rect.left_bottom(), rect.right_bottom()),
id.with("bottom"),
);
bottom |= response;
}
if possible.resize_top {
let response = side_response(
horizontal_rect(rect.left_top(), rect.right_top()),
id.with("top"),
);
top |= response;
}
if possible.resize_right || possible.resize_bottom {
let response = side_response(corner_rect(rect.right_bottom()), id.with("right_bottom"));
if possible.resize_right {
right |= response;
}
if possible.resize_bottom {
bottom |= response;
}
}
if possible.resize_right || possible.resize_top {
let response = side_response(corner_rect(rect.right_top()), id.with("right_top"));
if possible.resize_right {
right |= response;
}
if possible.resize_top {
top |= response;
}
}
if possible.resize_left || possible.resize_bottom {
let response = side_response(corner_rect(rect.left_bottom()), id.with("left_bottom"));
if possible.resize_left {
left |= response;
}
if possible.resize_bottom {
bottom |= response;
}
}
if possible.resize_left || possible.resize_top {
let response = side_response(corner_rect(rect.left_top()), id.with("left_top"));
if possible.resize_left {
left |= response;
}
if possible.resize_top {
top |= response;
}
}
let interaction = ResizeInteraction {
outer_rect,
window_frame,
left,
right,
top,
bottom,
};
interaction.set_cursor(ctx);
interaction
}
fn paint_frame_interaction(ui: &Ui, rect: Rect, interaction: ResizeInteraction) {
use epaint::tessellator::path::add_circle_quadrant;
let visuals = if interaction.any_dragged() {
ui.style().visuals.widgets.active
} else if interaction.any_hovered() {
ui.style().visuals.widgets.hovered
} else {
return;
};
let [left, right, top, bottom]: [bool; 4];
if interaction.any_dragged() {
left = interaction.left.drag;
right = interaction.right.drag;
top = interaction.top.drag;
bottom = interaction.bottom.drag;
} else {
left = interaction.left.hover;
right = interaction.right.hover;
top = interaction.top.hover;
bottom = interaction.bottom.hover;
}
let cr = CornerRadiusF32::from(ui.visuals().window_corner_radius);
let rect = rect.shrink(interaction.window_frame.stroke.width / 2.0);
let stroke = visuals.bg_stroke;
let half_stroke = stroke.width / 2.0;
let rect = rect
.shrink(half_stroke)
.round_to_pixels(ui.pixels_per_point())
.expand(half_stroke);
let Rect { min, max } = rect;
let mut points = Vec::new();
if right && !bottom && !top {
points.push(pos2(max.x, min.y + cr.ne));
points.push(pos2(max.x, max.y - cr.se));
}
if right && bottom {
points.push(pos2(max.x, min.y + cr.ne));
points.push(pos2(max.x, max.y - cr.se));
add_circle_quadrant(&mut points, pos2(max.x - cr.se, max.y - cr.se), cr.se, 0.0);
}
if bottom {
points.push(pos2(max.x - cr.se, max.y));
points.push(pos2(min.x + cr.sw, max.y));
}
if left && bottom {
add_circle_quadrant(&mut points, pos2(min.x + cr.sw, max.y - cr.sw), cr.sw, 1.0);
}
if left {
points.push(pos2(min.x, max.y - cr.sw));
points.push(pos2(min.x, min.y + cr.nw));
}
if left && top {
add_circle_quadrant(&mut points, pos2(min.x + cr.nw, min.y + cr.nw), cr.nw, 2.0);
}
if top {
points.push(pos2(min.x + cr.nw, min.y));
points.push(pos2(max.x - cr.ne, min.y));
}
if right && top {
add_circle_quadrant(&mut points, pos2(max.x - cr.ne, min.y + cr.ne), cr.ne, 3.0);
points.push(pos2(max.x, min.y + cr.ne));
points.push(pos2(max.x, max.y - cr.se));
}
ui.painter().add(Shape::line(points, stroke));
}
#[expect(clippy::too_many_arguments, clippy::fn_params_excessive_bools)]
fn title_ui(
ui: &mut Ui,
mut title: Atoms<'_>,
frame: Frame,
collapsing: &mut CollapsingState,
collapsible: bool,
active: bool,
open: Option<&mut bool>,
auto_sized: bool,
drag_to_move: bool,
area_id: Id,
) -> Response {
let shape_idx = ui.painter().add(Shape::Noop);
let mut atoms = Atoms::default();
let button_size = Vec2::splat(ui.spacing().icon_width);
let heading_font_height =
ui.fonts_mut(|f| f.row_height(&TextStyle::Heading.resolve(ui.style())));
let button_allocation_size = Vec2::splat(heading_font_height);
let button_shrink = (button_allocation_size - button_size) / 2.0;
let collapse_atom_id = Id::new("__window_collapse_button");
let close_atom_id = Id::new("__window_close_button");
let expanded = collapsing.openness(ui.ctx()) > 0.0;
if collapsible {
atoms.push_right(Atom::custom(collapse_atom_id, button_allocation_size));
}
atoms.push_right(Atom::grow());
if !auto_sized
&& !title.any_shrink()
&& let Some(first_text) = title
.iter_mut()
.find(|a| matches!(a.kind, AtomKind::Text(..)))
{
first_text.shrink = true;
}
atoms.extend_right(title);
atoms.push_right(Atom::grow());
if open.is_some() {
atoms.push_right(Atom::custom(close_atom_id, button_allocation_size));
}
let spacing = ui.spacing().item_spacing.x;
let mut child_ui = ui.new_child(UiBuilder::new());
let mut layout = AtomLayout::new(atoms)
.gap(spacing)
.fallback_font(TextStyle::Heading)
.wrap_mode(TextWrapMode::Truncate)
.frame(Frame::NONE.inner_margin(frame.inner_margin));
let frame = frame.inner_margin(0);
if expanded {
let min_width = if auto_sized {
ui.response().rect.width()
} else {
child_ui.available_width()
};
layout = layout.min_size(Vec2::new(min_width, 0.0));
}
let layout_response = layout.show(&mut child_ui);
let mut title_click_rect = layout_response.response.rect + frame.total_margin();
if collapsible && let Some(rect) = layout_response.rect(collapse_atom_id) {
let rect = rect.shrink2(button_shrink);
title_click_rect = title_click_rect.with_min_x(rect.max.x);
let icon_response = child_ui.interact(
rect,
child_ui.auto_id_with("collapse_button"),
Sense::click(),
);
icon_response.widget_info(|| {
WidgetInfo::labeled(
WidgetType::Button,
child_ui.is_enabled(),
if collapsing.is_open() { "Hide" } else { "Show" },
)
});
if icon_response.clicked() {
collapsing.toggle(&child_ui);
}
let openness = collapsing.openness(child_ui.ctx());
crate::collapsing_header::paint_default_icon(&mut child_ui, openness, &icon_response);
}
if let Some(open) = open
&& let Some(rect) = layout_response.rect(close_atom_id)
{
let rect = rect.shrink2(button_shrink);
title_click_rect = title_click_rect.with_max_x(rect.min.x);
if close_button(&mut child_ui, rect).clicked() {
*open = false;
}
}
if collapsible || drag_to_move {
let sense = if drag_to_move {
Sense::click_and_drag()
} else {
Sense::click()
};
let response = child_ui.interact(title_click_rect, area_id.with("__title_click"), sense);
if collapsible && response.double_clicked() {
collapsing.toggle(&child_ui);
}
}
{
let mut header_frame = frame.shadow(Shadow::NONE);
if active {
header_frame = header_frame.fill(ui.visuals().widgets.open.weak_bg_fill);
}
if expanded {
header_frame.corner_radius.sw = 0;
header_frame.corner_radius.se = 0;
}
ui.painter()
.set(shape_idx, header_frame.paint(layout_response.rect));
}
let mut advance_rect = child_ui.min_rect();
if auto_sized {
advance_rect = advance_rect.with_max_x(advance_rect.min.x);
}
if expanded {
advance_rect.max.y += frame.total_margin().bottom + frame.inner_margin.top as f32
- child_ui.spacing().item_spacing.y;
}
ui.advance_cursor_after_rect(advance_rect);
layout_response.response
}
fn close_button(ui: &mut Ui, rect: Rect) -> Response {
let close_id = ui.auto_id_with("window_close_button");
let response = ui.interact(rect, close_id, Sense::click());
response
.widget_info(|| WidgetInfo::labeled(WidgetType::Button, ui.is_enabled(), "Close window"));
ui.expand_to_include_rect(response.rect);
let visuals = ui.style().interact(&response);
let rect = rect.shrink(2.0).expand(visuals.expansion);
let stroke = visuals.fg_stroke;
ui.painter() .line_segment([rect.left_top(), rect.right_bottom()], stroke);
ui.painter() .line_segment([rect.right_top(), rect.left_bottom()], stroke);
response
}