use emath::GuiRounding as _;
use crate::{
emath, pos2, Align2, Context, Id, InnerResponse, LayerId, NumExt, Order, Pos2, Rect, Response,
Sense, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, WidgetRect, WidgetWithState,
};
#[derive(Clone, Copy, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct AreaState {
pub pivot_pos: Option<Pos2>,
pub pivot: Align2,
#[cfg_attr(feature = "serde", serde(skip))]
pub size: Option<Vec2>,
pub interactable: bool,
#[cfg_attr(feature = "serde", serde(skip))]
pub last_became_visible_at: Option<f64>,
}
impl Default for AreaState {
fn default() -> Self {
Self {
pivot_pos: None,
pivot: Align2::LEFT_TOP,
size: None,
interactable: true,
last_became_visible_at: None,
}
}
}
impl AreaState {
pub fn load(ctx: &Context, id: Id) -> Option<Self> {
ctx.memory(|mem| mem.areas().get(id).copied())
}
pub fn left_top_pos(&self) -> Pos2 {
let pivot_pos = self.pivot_pos.unwrap_or_default();
let size = self.size.unwrap_or_default();
pos2(
pivot_pos.x - self.pivot.x().to_factor() * size.x,
pivot_pos.y - self.pivot.y().to_factor() * size.y,
)
.round_ui()
}
pub fn set_left_top_pos(&mut self, pos: Pos2) {
let size = self.size.unwrap_or_default();
self.pivot_pos = Some(pos2(
pos.x + self.pivot.x().to_factor() * size.x,
pos.y + self.pivot.y().to_factor() * size.y,
));
}
pub fn rect(&self) -> Rect {
let size = self.size.unwrap_or_default();
Rect::from_min_size(self.left_top_pos(), size).round_ui()
}
}
#[must_use = "You should call .show()"]
#[derive(Clone, Copy, Debug)]
pub struct Area {
pub(crate) id: Id,
kind: UiKind,
sense: Option<Sense>,
movable: bool,
interactable: bool,
enabled: bool,
constrain: bool,
constrain_rect: Option<Rect>,
order: Order,
default_pos: Option<Pos2>,
default_size: Vec2,
pivot: Align2,
anchor: Option<(Align2, Vec2)>,
new_pos: Option<Pos2>,
fade_in: bool,
}
impl WidgetWithState for Area {
type State = AreaState;
}
impl Area {
pub fn new(id: Id) -> Self {
Self {
id,
kind: UiKind::GenericArea,
sense: None,
movable: true,
interactable: true,
constrain: true,
constrain_rect: None,
enabled: true,
order: Order::Middle,
default_pos: None,
default_size: Vec2::NAN,
new_pos: None,
pivot: Align2::LEFT_TOP,
anchor: None,
fade_in: true,
}
}
#[inline]
pub fn id(mut self, id: Id) -> Self {
self.id = id;
self
}
#[inline]
pub fn kind(mut self, kind: UiKind) -> Self {
self.kind = kind;
self
}
pub fn layer(&self) -> LayerId {
LayerId::new(self.order, self.id)
}
#[inline]
pub fn enabled(mut self, enabled: bool) -> Self {
self.enabled = enabled;
self
}
#[inline]
pub fn movable(mut self, movable: bool) -> Self {
self.movable = movable;
self.interactable |= movable;
self
}
pub fn is_enabled(&self) -> bool {
self.enabled
}
pub fn is_movable(&self) -> bool {
self.movable && self.enabled
}
#[inline]
pub fn interactable(mut self, interactable: bool) -> Self {
self.interactable = interactable;
self.movable &= interactable;
self
}
#[inline]
pub fn sense(mut self, sense: Sense) -> Self {
self.sense = Some(sense);
self
}
#[inline]
pub fn order(mut self, order: Order) -> Self {
self.order = order;
self
}
#[inline]
pub fn default_pos(mut self, default_pos: impl Into<Pos2>) -> Self {
self.default_pos = Some(default_pos.into());
self
}
#[inline]
pub fn default_size(mut self, default_size: impl Into<Vec2>) -> Self {
self.default_size = default_size.into();
self
}
#[inline]
pub fn default_width(mut self, default_width: f32) -> Self {
self.default_size.x = default_width;
self
}
#[inline]
pub fn default_height(mut self, default_height: f32) -> Self {
self.default_size.y = default_height;
self
}
#[inline]
pub fn fixed_pos(mut self, fixed_pos: impl Into<Pos2>) -> Self {
self.new_pos = Some(fixed_pos.into());
self.movable = false;
self
}
#[inline]
pub fn constrain(mut self, constrain: bool) -> Self {
self.constrain = constrain;
self
}
#[inline]
pub fn constrain_to(mut self, constrain_rect: Rect) -> Self {
self.constrain = true;
self.constrain_rect = Some(constrain_rect);
self
}
#[inline]
pub fn pivot(mut self, pivot: Align2) -> Self {
self.pivot = pivot;
self
}
#[inline]
pub fn current_pos(mut self, current_pos: impl Into<Pos2>) -> Self {
self.new_pos = Some(current_pos.into());
self
}
#[inline]
pub fn anchor(mut self, align: Align2, offset: impl Into<Vec2>) -> Self {
self.anchor = Some((align, offset.into()));
self.movable(false)
}
pub(crate) fn get_pivot(&self) -> Align2 {
if let Some((pivot, _)) = self.anchor {
pivot
} else {
Align2::LEFT_TOP
}
}
#[inline]
pub fn fade_in(mut self, fade_in: bool) -> Self {
self.fade_in = fade_in;
self
}
}
pub(crate) struct Prepared {
kind: UiKind,
layer_id: LayerId,
state: AreaState,
move_response: Response,
enabled: bool,
constrain: bool,
constrain_rect: Rect,
sizing_pass: bool,
fade_in: bool,
}
impl Area {
pub fn show<R>(
self,
ctx: &Context,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> InnerResponse<R> {
let prepared = self.begin(ctx);
let mut content_ui = prepared.content_ui(ctx);
let inner = add_contents(&mut content_ui);
let response = prepared.end(ctx, content_ui);
InnerResponse { inner, response }
}
pub(crate) fn begin(self, ctx: &Context) -> Prepared {
let Self {
id,
kind,
sense,
movable,
order,
interactable,
enabled,
default_pos,
default_size,
new_pos,
pivot,
anchor,
constrain,
constrain_rect,
fade_in,
} = self;
let constrain_rect = constrain_rect.unwrap_or_else(|| ctx.screen_rect());
let layer_id = LayerId::new(order, id);
let state = AreaState::load(ctx, id);
let mut sizing_pass = state.is_none();
let mut state = state.unwrap_or(AreaState {
pivot_pos: None,
pivot,
size: None,
interactable,
last_became_visible_at: None,
});
state.pivot = pivot;
state.interactable = interactable;
if let Some(new_pos) = new_pos {
state.pivot_pos = Some(new_pos);
}
state.pivot_pos.get_or_insert_with(|| {
default_pos.unwrap_or_else(|| automatic_area_position(ctx, layer_id))
});
state.interactable = interactable;
let size = *state.size.get_or_insert_with(|| {
sizing_pass = true;
let mut size = default_size;
let default_area_size = ctx.style().spacing.default_area_size;
if size.x.is_nan() {
size.x = default_area_size.x;
}
if size.y.is_nan() {
size.y = default_area_size.y;
}
if constrain {
size = size.at_most(constrain_rect.size());
}
size
});
let visible_last_frame = ctx.memory(|mem| mem.areas().visible_last_frame(&layer_id));
if !visible_last_frame || state.last_became_visible_at.is_none() {
state.last_became_visible_at = Some(ctx.input(|i| i.time));
}
if let Some((anchor, offset)) = anchor {
state.set_left_top_pos(
anchor
.align_size_within_rect(size, constrain_rect)
.left_top()
+ offset,
);
}
let mut move_response = {
let interact_id = layer_id.id.with("move");
let sense = sense.unwrap_or_else(|| {
if movable {
Sense::drag()
} else if interactable {
Sense::click() } else {
Sense::hover()
}
});
let move_response = ctx.create_widget(
WidgetRect {
id: interact_id,
layer_id,
rect: state.rect(),
interact_rect: state.rect().intersect(constrain_rect),
sense,
enabled,
},
true,
);
if movable && move_response.dragged() {
if let Some(pivot_pos) = &mut state.pivot_pos {
*pivot_pos += move_response.drag_delta();
}
}
if (move_response.dragged() || move_response.clicked())
|| pointer_pressed_on_area(ctx, layer_id)
|| !ctx.memory(|m| m.areas().visible_last_frame(&layer_id))
{
ctx.memory_mut(|m| m.areas_mut().move_to_top(layer_id));
ctx.request_repaint();
}
move_response
};
if constrain {
state.set_left_top_pos(
Context::constrain_window_rect_to_area(state.rect(), constrain_rect).min,
);
}
state.set_left_top_pos(state.left_top_pos());
move_response.rect = state.rect();
move_response.interact_rect = state.rect();
Prepared {
kind,
layer_id,
state,
move_response,
enabled,
constrain,
constrain_rect,
sizing_pass,
fade_in,
}
}
}
impl Prepared {
pub(crate) fn state(&self) -> &AreaState {
&self.state
}
pub(crate) fn state_mut(&mut self) -> &mut AreaState {
&mut self.state
}
pub(crate) fn constrain(&self) -> bool {
self.constrain
}
pub(crate) fn constrain_rect(&self) -> Rect {
self.constrain_rect
}
pub(crate) fn content_ui(&self, ctx: &Context) -> Ui {
let max_rect = self.state.rect();
let mut ui_builder = UiBuilder::new()
.ui_stack_info(UiStackInfo::new(self.kind))
.layer_id(self.layer_id)
.max_rect(max_rect);
if !self.enabled {
ui_builder = ui_builder.disabled();
}
if self.sizing_pass {
ui_builder = ui_builder.sizing_pass().invisible();
}
let mut ui = Ui::new(ctx.clone(), self.layer_id.id, ui_builder);
ui.set_clip_rect(self.constrain_rect);
if self.fade_in {
if let Some(last_became_visible_at) = self.state.last_became_visible_at {
let age =
ctx.input(|i| (i.time - last_became_visible_at) as f32 + i.predicted_dt / 2.0);
let opacity = crate::remap_clamp(age, 0.0..=ctx.style().animation_time, 0.0..=1.0);
let opacity = emath::easing::quadratic_out(opacity); ui.multiply_opacity(opacity);
if opacity < 1.0 {
ctx.request_repaint();
}
}
}
ui
}
pub(crate) fn with_widget_info(&self, make_info: impl Fn() -> crate::WidgetInfo) {
self.move_response.widget_info(make_info);
}
pub(crate) fn id(&self) -> Id {
self.move_response.id
}
#[allow(clippy::needless_pass_by_value)] pub(crate) fn end(self, ctx: &Context, content_ui: Ui) -> Response {
let Self {
kind: _,
layer_id,
mut state,
move_response: mut response,
sizing_pass,
..
} = self;
state.size = Some(content_ui.min_size());
let final_rect = state.rect();
response.rect = final_rect;
response.interact_rect = final_rect;
ctx.memory_mut(|m| m.areas_mut().set_state(layer_id, state));
if sizing_pass {
ctx.request_repaint();
}
response
}
}
fn pointer_pressed_on_area(ctx: &Context, layer_id: LayerId) -> bool {
if let Some(pointer_pos) = ctx.pointer_interact_pos() {
let any_pressed = ctx.input(|i| i.pointer.any_pressed());
any_pressed && ctx.layer_id_at(pointer_pos) == Some(layer_id)
} else {
false
}
}
fn automatic_area_position(ctx: &Context, layer_id: LayerId) -> Pos2 {
let mut existing: Vec<Rect> = ctx.memory(|mem| {
mem.areas()
.visible_windows()
.filter(|(id, _)| id != &layer_id) .filter(|(_, state)| state.pivot_pos.is_some() && state.size.is_some())
.map(|(_, state)| state.rect())
.collect()
});
existing.sort_by_key(|r| r.left().round() as i32);
let available_rect = ctx.available_rect();
let spacing = 16.0;
let left = available_rect.left() + spacing;
let top = available_rect.top() + spacing;
if existing.is_empty() {
return pos2(left, top);
}
let mut column_bbs = vec![existing[0]];
for &rect in &existing {
let current_column_bb = column_bbs.last_mut().unwrap();
if rect.left() < current_column_bb.right() {
*current_column_bb = current_column_bb.union(rect);
} else {
column_bbs.push(rect);
}
}
{
let mut x = left;
for col_bb in &column_bbs {
let available = col_bb.left() - x;
if available >= 300.0 {
return pos2(x, top);
}
x = col_bb.right() + spacing;
}
}
for col_bb in &column_bbs {
if col_bb.bottom() < available_rect.center().y {
return pos2(col_bb.left(), col_bb.bottom() + spacing);
}
}
let rightmost = column_bbs.last().unwrap().right();
if rightmost + 200.0 < available_rect.right() {
return pos2(rightmost + spacing, top);
}
let mut best_pos = pos2(left, column_bbs[0].bottom() + spacing);
for col_bb in &column_bbs {
let col_pos = pos2(col_bb.left(), col_bb.bottom() + spacing);
if col_pos.y < best_pos.y {
best_pos = col_pos;
}
}
best_pos
}