use std::cell::{Cell, RefCell};
use std::rc::Rc;
use std::sync::Arc;
use web_time::Instant;
use crate::color::Color;
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::{paint_subtree, 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 SHADOW_STEPS: usize = 10;
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)]
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>,
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>,
title: String,
on_raised: Option<Box<dyn FnMut(&str)>>,
}
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),
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,
}
}
pub fn title(&self) -> &str {
&self.title
}
pub fn on_raised(mut self, cb: impl FnMut(&str) + 'static) -> Self {
self.on_raised = Some(Box::new(cb));
self
}
pub fn with_bounds(mut self, b: Rect) -> Self {
self.pre_collapse_h = b.height;
self.bounds = b;
if self.maximized {
self.pre_maximize_bounds = b;
}
self
}
pub fn with_font_size(mut self, size: f64) -> Self {
self.font_size = size;
self
}
pub fn with_visible_cell(mut self, cell: Rc<Cell<bool>>) -> Self {
let visible = cell.get();
self.last_visible.set(visible);
self.fade_out_active.set(false);
self.visibility_anim =
crate::animation::Tween::new(if visible { 1.0 } else { 0.0 }, VISIBILITY_FADE_SECS);
self.visible_cell = Some(cell);
self
}
pub fn with_reset_cell(mut self, cell: Rc<Cell<Option<Rect>>>) -> Self {
self.reset_to = Some(cell);
self
}
pub fn with_position_cell(mut self, cell: Rc<Cell<Rect>>) -> Self {
self.position_cell = Some(cell);
self
}
pub fn with_maximized_cell(mut self, cell: Rc<Cell<bool>>) -> Self {
self.maximized = cell.get();
if self.maximized {
self.pre_maximize_bounds = self.bounds;
}
self.maximized_cell = Some(cell);
self
}
pub fn with_margin(mut self, m: Insets) -> Self {
self.base.margin = m;
self
}
pub fn with_h_anchor(mut self, h: HAnchor) -> Self {
self.base.h_anchor = h;
self
}
pub fn with_v_anchor(mut self, v: VAnchor) -> Self {
self.base.v_anchor = v;
self
}
pub fn with_min_size(mut self, s: Size) -> Self {
self.base.min_size = s;
self
}
pub fn with_max_size(mut self, s: Size) -> Self {
self.base.max_size = s;
self
}
pub fn with_constrain(mut self, constrain: bool) -> Self {
self.constrain = constrain;
self
}
pub fn with_gl_backbuffer(mut self, enabled: bool) -> Self {
self.use_gl_backbuffer = enabled;
self.backbuffer.invalidate();
self
}
pub fn with_auto_size(mut self, auto: bool) -> Self {
self.auto_size = auto;
self
}
pub fn with_resizable(mut self, on: bool) -> Self {
self.resizable = on;
self
}
pub fn with_resizable_axes(mut self, h: bool, v: bool) -> Self {
self.resizable = h || v;
self.resizable_h = h;
self.resizable_v = v;
self
}
pub fn with_tight_content_fit(mut self, on: bool) -> Self {
self.tight_content_fit = on;
self
}
pub fn with_height_floor_to_content(mut self, on: bool) -> Self {
self.floor_content_height = on;
self
}
pub fn with_vscroll(mut self, vscroll: bool) -> Self {
if vscroll {
if let Some(content) = self.children.pop() {
let scroll = crate::widgets::ScrollView::new(content)
.vertical(true)
.horizontal(false);
self.children.push(Box::new(scroll));
}
}
self
}
pub fn on_close(mut self, cb: impl FnMut() + 'static) -> Self {
self.on_close = Some(Box::new(cb));
self
}
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 in_chevron_button(&self, local: Point) -> bool {
let cx = 12.0;
let cy = self.bounds.height - TITLE_H * 0.5;
let half = 8.0;
local.x >= cx - half && local.x <= cx + half && local.y >= cy - half && local.y <= cy + half
}
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();
}
}
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 widget_impl;