use std::cell::{Cell, RefCell};
use std::rc::Rc;
use std::sync::Arc;
use web_time::Instant;
use crate::cursor::{set_cursor_icon, CursorIcon};
use crate::draw_ctx::DrawCtx;
use crate::event::{Event, EventResult, MouseButton};
use crate::geometry::{Point, Rect, Size};
use crate::layout_props::{HAnchor, Insets, VAnchor, WidgetBase};
use crate::text::Font;
use crate::widget::{BackbufferKind, BackbufferSpec, BackbufferState, Widget};
use crate::widgets::window_title_bar::{TitleBarView, WindowTitleBar};
fn snap(r: Rect) -> Rect {
Rect::new(r.x.round(), r.y.round(), r.width.round(), r.height.round())
}
const TITLE_H: f64 = 28.0;
const CORNER_R: f64 = 8.0;
const SHADOW_BLUR: f64 = 14.0;
const SHADOW_DX: f64 = 2.0;
const SHADOW_DY: f64 = 6.0;
const VISIBILITY_FADE_SECS: f64 = 0.18;
const CLOSE_R: f64 = 6.0;
const CLOSE_PAD: f64 = 10.0;
const MAX_PAD: f64 = CLOSE_PAD + CLOSE_R * 2.0 + 4.0; const RESIZE_EDGE: f64 = 6.0; const MIN_W: f64 = 120.0;
const MIN_H: f64 = 80.0;
const DBL_CLICK_MS: u128 = 500;
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) enum ResizeDir {
N,
NE,
E,
SE,
S,
SW,
W,
NW,
}
#[derive(Clone, Copy, Debug, PartialEq)]
enum DragMode {
None,
Move,
Resize(ResizeDir),
}
pub struct Window {
bounds: Rect,
children: Vec<Box<dyn Widget>>, base: WidgetBase,
font_size: f64,
visible: bool,
visible_cell: Option<Rc<Cell<bool>>>,
visibility_anim: crate::animation::Tween,
fade_out_active: Cell<bool>,
backbuffer: BackbufferState,
use_gl_backbuffer: bool,
reset_to: Option<Rc<Cell<Option<Rect>>>>,
position_cell: Option<Rc<Cell<Rect>>>,
maximized_cell: Option<Rc<Cell<bool>>>,
last_visible: Cell<bool>,
needs_initial_fit: Cell<bool>,
raise_request: Cell<bool>,
collapsed: bool,
pre_collapse_h: f64,
drag_mode: DragMode,
drag_start_world: Point,
drag_start_bounds: Rect,
close_hovered: bool,
on_close: Option<Box<dyn FnMut()>>,
maximized: bool,
pre_maximize_bounds: Rect,
maximize_hovered: bool,
hover_dir: Option<ResizeDir>,
last_title_click: Option<Instant>,
title_bar: WindowTitleBar,
title_state: Rc<RefCell<TitleBarView>>,
canvas_size: Size,
constrain: bool,
auto_size: bool,
resizable: bool,
resizable_h: bool,
resizable_v: bool,
tight_content_fit: bool,
floor_content_height: bool,
last_content_natural_h: Cell<f64>,
foreground_layer_active: Cell<bool>,
live_content: bool,
title: String,
on_raised: Option<Box<dyn FnMut(&str)>>,
snap_id: crate::snap::SnapId,
}
impl Window {
pub fn new(title: impl Into<String>, font: Arc<Font>, content: Box<dyn Widget>) -> Self {
let font_size = 13.0;
let title_str: String = title.into();
let title_state = Rc::new(RefCell::new(TitleBarView::default_visuals()));
let title_bar = WindowTitleBar::new(&title_str, Arc::clone(&font), Rc::clone(&title_state));
Self {
bounds: Rect::new(60.0, 60.0, 360.0, 280.0),
children: vec![content],
base: WidgetBase::new(),
font_size,
visible: true,
visible_cell: None,
visibility_anim: crate::animation::Tween::new(1.0, VISIBILITY_FADE_SECS),
fade_out_active: Cell::new(false),
backbuffer: BackbufferState::new(),
use_gl_backbuffer: true,
reset_to: None,
position_cell: None,
maximized_cell: None,
last_visible: Cell::new(true),
needs_initial_fit: Cell::new(true),
raise_request: Cell::new(false),
collapsed: false,
pre_collapse_h: 280.0,
drag_mode: DragMode::None,
drag_start_world: Point::ORIGIN,
drag_start_bounds: Rect::default(),
close_hovered: false,
on_close: None,
maximized: false,
pre_maximize_bounds: Rect::new(60.0, 60.0, 360.0, 280.0),
maximize_hovered: false,
hover_dir: None,
last_title_click: None,
title_bar,
title_state,
canvas_size: Size::new(0.0, 0.0),
constrain: true,
auto_size: false,
resizable: true,
resizable_h: true,
resizable_v: true,
tight_content_fit: false,
floor_content_height: false,
last_content_natural_h: Cell::new(0.0),
foreground_layer_active: Cell::new(false),
title: title_str,
on_raised: None,
live_content: false,
snap_id: crate::snap::next_snap_id(),
}
}
pub fn title(&self) -> &str {
&self.title
}
pub fn invalidate_backbuffer(&mut self) {
self.backbuffer.invalidate();
}
fn requested_visible(&self) -> bool {
if let Some(ref cell) = self.visible_cell {
cell.get()
} else {
self.visible
}
}
fn layer_outsets() -> (f64, f64, f64, f64) {
let left = (SHADOW_BLUR - SHADOW_DX).max(0.0).ceil();
let bottom = (SHADOW_BLUR + SHADOW_DY).ceil();
let right = (SHADOW_BLUR + SHADOW_DX).ceil();
let top = (SHADOW_BLUR - SHADOW_DY).max(0.0).ceil();
(left, bottom, right, top)
}
fn clamp_to_canvas(&mut self) {
if !self.constrain {
return;
}
let cw = self.canvas_size.width;
let ch = self.canvas_size.height;
const MIN_H_VISIBLE: f64 = 40.0;
let min_x = MIN_H_VISIBLE - self.bounds.width;
let max_x = (cw - MIN_H_VISIBLE).max(min_x);
self.bounds.x = self.bounds.x.clamp(min_x, max_x).round();
let min_y = TITLE_H - self.bounds.height;
let max_y = (ch - self.bounds.height).max(min_y);
self.bounds.y = self.bounds.y.clamp(min_y, max_y).round();
}
fn fit_fully_to_canvas(&mut self, available: Size) {
if !self.constrain || available.width <= 1.0 || available.height <= 1.0 {
return;
}
let max_w = available.width.max(MIN_W);
let max_h = available.height.max(TITLE_H);
self.bounds.width = self.bounds.width.clamp(MIN_W.min(max_w), max_w).round();
self.bounds.height = self.bounds.height.clamp(TITLE_H, max_h).round();
self.bounds.x = self
.bounds
.x
.clamp(0.0, (available.width - self.bounds.width).max(0.0))
.round();
self.bounds.y = self
.bounds
.y
.clamp(0.0, (available.height - self.bounds.height).max(0.0))
.round();
self.pre_collapse_h = self.bounds.height;
if self.maximized {
self.pre_maximize_bounds = self.bounds;
}
}
pub fn show(&mut self) {
self.visible = true;
self.fade_out_active.set(false);
self.visibility_anim.set_target(1.0);
crate::animation::request_draw();
}
pub fn hide(&mut self) {
self.visible = false;
self.visibility_anim.set_target(0.0);
crate::animation::request_draw();
}
pub fn toggle(&mut self) {
if self.visible {
self.hide();
} else {
self.show();
}
}
pub fn is_visible(&self) -> bool {
self.requested_visible() || self.fade_out_active.get()
}
fn title_bar_bottom(&self) -> f64 {
self.bounds.height - TITLE_H
}
fn in_title_bar(&self, local: Point) -> bool {
local.y >= self.title_bar_bottom()
&& local.y <= self.bounds.height
&& local.x >= 0.0
&& local.x <= self.bounds.width
}
fn close_center(&self) -> Point {
Point::new(
self.bounds.width - CLOSE_PAD,
self.bounds.height - TITLE_H * 0.5,
)
}
fn in_close_button(&self, local: Point) -> bool {
let c = self.close_center();
let dx = local.x - c.x;
let dy = local.y - c.y;
dx * dx + dy * dy <= (CLOSE_R + 3.0) * (CLOSE_R + 3.0)
}
fn maximize_center(&self) -> Point {
Point::new(
self.bounds.width - MAX_PAD,
self.bounds.height - TITLE_H * 0.5,
)
}
fn in_maximize_button(&self, local: Point) -> bool {
let c = self.maximize_center();
let dx = local.x - c.x;
let dy = local.y - c.y;
dx * dx + dy * dy <= (CLOSE_R + 3.0) * (CLOSE_R + 3.0)
}
fn toggle_collapse(&mut self) {
let top = self.bounds.y + self.bounds.height;
if self.collapsed {
self.bounds.height = self.pre_collapse_h;
self.bounds.y = (top - self.pre_collapse_h).round();
self.collapsed = false;
} else {
self.pre_collapse_h = self.bounds.height;
self.bounds.height = TITLE_H;
self.bounds.y = (top - TITLE_H).round();
self.collapsed = true;
}
self.clamp_to_canvas();
}
fn toggle_maximize(&mut self) {
if self.maximized {
self.bounds = self.pre_maximize_bounds;
self.maximized = false;
} else {
self.pre_maximize_bounds = self.bounds;
self.bounds = snap(Rect::new(
0.0,
0.0,
self.canvas_size.width,
self.canvas_size.height,
));
self.maximized = true;
}
if let Some(ref cell) = self.maximized_cell {
cell.set(self.maximized);
}
}
fn resize_dir(&self, local: Point) -> Option<ResizeDir> {
if self.collapsed || self.auto_size {
return None;
}
if !self.resizable {
return None;
}
let w = self.bounds.width;
let h = self.bounds.height;
let x = local.x;
let y = local.y;
if x < 0.0 || x > w || y < 0.0 || y > h {
return None;
}
let on_n = self.resizable_v && y > h - RESIZE_EDGE;
let on_s = self.resizable_v && y < RESIZE_EDGE;
let on_w = self.resizable_h && x < RESIZE_EDGE;
let on_e = self.resizable_h && x > w - RESIZE_EDGE;
match (on_n, on_e, on_s, on_w) {
(true, true, _, _) => Some(ResizeDir::NE),
(true, _, _, true) => Some(ResizeDir::NW),
(_, _, true, true) => Some(ResizeDir::SW),
(_, true, true, _) => Some(ResizeDir::SE),
(true, _, _, _) => Some(ResizeDir::N),
(_, true, _, _) => Some(ResizeDir::E),
(_, _, true, _) => Some(ResizeDir::S),
(_, _, _, true) => Some(ResizeDir::W),
_ => None,
}
}
fn effective_min_h(&self) -> f64 {
if self.tight_content_fit || self.floor_content_height {
let content_min = self.last_content_natural_h.get() + TITLE_H;
MIN_H.max(content_min)
} else {
MIN_H
}
}
fn apply_resize(&mut self, world_pos: Point) {
let dx = world_pos.x - self.drag_start_world.x;
let dy = world_pos.y - self.drag_start_world.y;
let sb = self.drag_start_bounds;
let min_h = self.effective_min_h();
let (mut x, mut y, mut w, mut h) = (sb.x, sb.y, sb.width, sb.height);
if let DragMode::Resize(dir) = self.drag_mode {
match dir {
ResizeDir::N => {
h = (sb.height + dy).max(min_h);
}
ResizeDir::S => {
y = sb.y + dy;
h = (sb.height - dy).max(min_h);
if h == min_h {
y = sb.y + sb.height - min_h;
}
}
ResizeDir::E => {
w = (sb.width + dx).max(MIN_W);
}
ResizeDir::W => {
x = sb.x + dx;
w = (sb.width - dx).max(MIN_W);
if w == MIN_W {
x = sb.x + sb.width - MIN_W;
}
}
ResizeDir::NE => {
w = (sb.width + dx).max(MIN_W);
h = (sb.height + dy).max(min_h);
}
ResizeDir::NW => {
x = sb.x + dx;
w = (sb.width - dx).max(MIN_W);
if w == MIN_W {
x = sb.x + sb.width - MIN_W;
}
h = (sb.height + dy).max(min_h);
}
ResizeDir::SE => {
w = (sb.width + dx).max(MIN_W);
y = sb.y + dy;
h = (sb.height - dy).max(min_h);
if h == min_h {
y = sb.y + sb.height - min_h;
}
}
ResizeDir::SW => {
x = sb.x + dx;
w = (sb.width - dx).max(MIN_W);
if w == MIN_W {
x = sb.x + sb.width - MIN_W;
}
y = sb.y + dy;
h = (sb.height - dy).max(min_h);
if h == min_h {
y = sb.y + sb.height - min_h;
}
}
}
}
self.bounds = snap(Rect::new(x, y, w, h));
self.clamp_to_canvas();
}
pub(crate) fn apply_move_snap(&mut self) {
if !crate::snap::is_enabled() {
crate::snap::clear_guides();
return;
}
let targets = crate::snap::targets_snapshot();
let result = crate::snap::compute_snap(
self.bounds,
self.snap_id,
&targets,
crate::snap::DEFAULT_THRESHOLD,
crate::snap::SnapMode::Move,
);
self.bounds = snap(result.rect);
crate::snap::set_guides(result.guides);
}
pub(crate) fn apply_resize_snap(&mut self, dir: ResizeDir) {
if !crate::snap::is_enabled() {
crate::snap::clear_guides();
return;
}
let targets = crate::snap::targets_snapshot();
let edge = resize_dir_to_snap_edge(dir);
let result = crate::snap::compute_snap(
self.bounds,
self.snap_id,
&targets,
crate::snap::DEFAULT_THRESHOLD,
crate::snap::SnapMode::Resize(edge),
);
self.bounds = snap(result.rect);
crate::snap::set_guides(result.guides);
}
}
fn resize_dir_to_snap_edge(dir: ResizeDir) -> crate::snap::ResizeEdge {
use crate::snap::ResizeEdge as E;
match dir {
ResizeDir::N => E::North,
ResizeDir::NE => E::NorthEast,
ResizeDir::E => E::East,
ResizeDir::SE => E::SouthEast,
ResizeDir::S => E::South,
ResizeDir::SW => E::SouthWest,
ResizeDir::W => E::West,
ResizeDir::NW => E::NorthWest,
}
}
impl crate::snap::Snappable for Window {
fn snap_id(&self) -> crate::snap::SnapId {
self.snap_id
}
fn snap_rect(&self) -> Rect {
self.bounds
}
fn set_snap_rect(&mut self, r: Rect) {
self.bounds = snap(r);
}
fn is_snap_source(&self) -> bool {
self.requested_visible() && !self.maximized
}
fn is_snap_target(&self) -> bool {
self.requested_visible() && !self.maximized
}
}
fn resize_cursor(dir: ResizeDir) -> CursorIcon {
match dir {
ResizeDir::N => CursorIcon::ResizeNorth,
ResizeDir::S => CursorIcon::ResizeSouth,
ResizeDir::E => CursorIcon::ResizeEast,
ResizeDir::W => CursorIcon::ResizeWest,
ResizeDir::NE => CursorIcon::ResizeNorthEast,
ResizeDir::NW => CursorIcon::ResizeNorthWest,
ResizeDir::SE => CursorIcon::ResizeSouthEast,
ResizeDir::SW => CursorIcon::ResizeSouthWest,
}
}
mod builder;
pub mod chrome;
mod paint;
mod widget_impl;
pub use chrome::{
paint_chevron, paint_chrome_body, paint_chrome_border, paint_chrome_shadow,
paint_chrome_title_bar, ChromeStyle,
};