// Copyright 2026 Ling71671
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
///|
/// Standalone floating window container widget supporting fluent configuration.
pub struct Window {
title : String
pos : @math.Vec2
size : @math.Vec2
id_salt : String?
constrain_to : @math.Rect?
} derive(Eq, Debug)
///|
/// Create a new window container.
pub fn Window::new(
title : String,
pos : @math.Vec2,
size : @math.Vec2,
) -> Window {
{ title, pos, size, id_salt: None, constrain_to: None, }
}
///|
/// Explicitly configure a unique identification salt for distinguishing windows with matching titles.
pub fn Window::id_salt(self : Window, id_salt : String) -> Window {
{ ..self, id_salt: Some(id_salt), }
}
///|
/// Constrain window dragging within specified boundary rectangle.
pub fn Window::constrain_to(self : Window, bounds : @math.Rect) -> Window {
{ ..self, constrain_to: Some(bounds), }
}
///|
/// Show the floating window and execute content closure.
pub fn Window::show(
self : Window,
ctx : UIContext,
content : (UIContext) -> Unit,
) -> Response {
match (self.id_salt, self.constrain_to) {
(Some(salt), Some(bounds)) =>
window(
ctx,
self.title,
self.pos,
self.size,
content,
id_salt=salt,
constrain_to=bounds,
)
(Some(salt), None) =>
window(ctx, self.title, self.pos, self.size, content, id_salt=salt)
(None, Some(bounds)) =>
window(ctx, self.title, self.pos, self.size, content, constrain_to=bounds)
(None, None) => window(ctx, self.title, self.pos, self.size, content)
}
}
///|
/// Render a floating window container with a draggable title bar, border, and
/// clipped content region. The layout cursor inside the content closure is
/// local to the window interior.
pub fn window(
ctx : UIContext,
title : String,
pos : @math.Vec2,
size : @math.Vec2,
content : (UIContext) -> Unit,
id_salt? : String,
constrain_to? : @math.Rect,
) -> Response {
let scale = ctx.style.scale
let title_bar_h = (ctx.style.control_h_sm + ctx.style.spacing_xs) * scale
let id = ctx.id_stack.derive_id("window::" + id_salt.unwrap_or(title))
ctx.register_focusable(id)
let saved_draw_list = ctx.layers.draw_list()
let saved_clip : Array[@math.Rect] = []
for clip in ctx.layers.clip_stack() {
saved_clip.push(clip)
}
ctx.layers.clip_stack().clear()
let mut win_pos = ctx.get_window_pos(id, pos)
let mut window_rect = @math.Rect::new(win_pos.x, win_pos.y, size.x, size.y)
let mut title_bar_rect = @math.Rect::new(
win_pos.x,
win_pos.y,
size.x,
title_bar_h,
)
ctx.windows.set_active_z(ctx.window_z(id))
let window_hovered = ctx.is_hovered(window_rect)
let bar_hovered = ctx.is_hovered(title_bar_rect)
if window_hovered && ctx.input.mouse_pressed {
ctx.raise_window(id)
ctx.windows.set_active_z(ctx.window_z(id))
}
if bar_hovered && ctx.input.mouse_pressed {
ctx.set_active_id(id)
ctx.request_focus(id)
}
let is_active = ctx.active_id() == id
let is_dragging = is_active &&
ctx.input.mouse_down &&
!ctx.input.mouse_pressed
let clicked = is_active && ctx.input.mouse_released && bar_hovered
// The title bar is the drag handle: advertise it, and keep the cursor while
// the drag continues even after the pointer has left the bar.
if bar_hovered || is_active {
ctx.set_cursor_icon("move")
}
if is_dragging {
win_pos = win_pos.add(ctx.input.mouse_delta)
if constrain_to is Some(bounds) {
win_pos = clamp_window_pos(win_pos, size, bounds)
}
ctx.set_window_pos(id, win_pos)
window_rect = @math.Rect::new(win_pos.x, win_pos.y, size.x, size.y)
title_bar_rect = @math.Rect::new(win_pos.x, win_pos.y, size.x, title_bar_h)
}
// Keyboard move: with the window focused, the arrows nudge it inside the
// same constraint bounds the mouse drag uses.
if ctx.has_focus(id) {
let kb_step = 8.0 * scale
let mut dx = 0.0
let mut dy = 0.0
if ctx.input.key_pressed(Key::ArrowRight) {
dx = kb_step
ctx.input.consume_key(Key::ArrowRight)
}
if ctx.input.key_pressed(Key::ArrowLeft) {
dx = -kb_step
ctx.input.consume_key(Key::ArrowLeft)
}
if ctx.input.key_pressed(Key::ArrowDown) {
dy = kb_step
ctx.input.consume_key(Key::ArrowDown)
}
if ctx.input.key_pressed(Key::ArrowUp) {
dy = -kb_step
ctx.input.consume_key(Key::ArrowUp)
}
if dx != 0.0 || dy != 0.0 {
win_pos = win_pos.add(@math.Vec2::new(dx, dy))
if constrain_to is Some(bounds) {
win_pos = clamp_window_pos(win_pos, size, bounds)
}
ctx.set_window_pos(id, win_pos)
window_rect = @math.Rect::new(win_pos.x, win_pos.y, size.x, size.y)
title_bar_rect = @math.Rect::new(
win_pos.x,
win_pos.y,
size.x,
title_bar_h,
)
}
}
if is_active && ctx.input.mouse_released {
ctx.set_active_id(@core.Id::zero())
}
if window_hovered {
ctx.set_wants_capture_mouse(true)
}
ctx.windows.record_rect(window_rect, ctx.windows.active_z())
let batch = ctx.window_batch(id)
batch.clear()
ctx.windows.mark_drawn(id)
ctx.layers.set_draw_list(batch)
let x = win_pos.x
let y = win_pos.y
let theme = ctx.theme
let bg_color = theme.bg_window
let border_color = theme.border_muted
let header_color = theme.bg_surface
let win_radius = ctx.style.radius_md * scale
let font_sz = ctx.style.font_normal * scale
let painter = ctx.painter()
painter.add_rect(window_rect, bg_color, win_radius)
painter.add_rect_stroke(window_rect, border_color, 1.0 * scale, win_radius)
if ctx.has_focus(id) {
painter.add_rect_stroke(
window_rect.expand(2.0 * scale),
theme.border_focus,
1.5 * scale,
win_radius + 2.0 * scale,
)
}
painter.add_rect(title_bar_rect, header_color, win_radius)
let filler_rect = @math.Rect::new(
x,
y + title_bar_h - 4.0 * scale,
size.x,
4.0 * scale,
)
painter.add_rect(filler_rect, header_color, 0.0)
painter.add_line(
@math.Vec2::new(x, y + title_bar_h),
@math.Vec2::new(x + size.x, y + title_bar_h),
border_color,
1.0 * scale,
)
let text_y = y + (title_bar_h - font_sz) * 0.5
painter.add_text(
@math.Vec2::new(x + 12.0 * scale, text_y),
title,
font_sz,
theme.text_primary,
)
let pad = 12.0 * scale
let content_x = x + pad
let content_y = y + title_bar_h + pad
let content_w = size.x - pad * 2.0
let content_h = size.y - title_bar_h - pad * 2.0
let clip_rect = @math.Rect::new(content_x, content_y, content_w, content_h)
ctx.push_clip(clip_rect)
let prev_cursor = ctx.cursor()
let prev_avail_w = ctx.available_width()
ctx.set_cursor(@math.Vec2::new(content_x, content_y))
ctx.set_available_width(content_w)
content(ctx)
ctx.set_cursor(prev_cursor)
ctx.set_available_width(prev_avail_w)
ctx.pop_clip()
ctx.layers.set_draw_list(saved_draw_list)
ctx.layers.clip_stack().clear()
for clip in saved_clip {
ctx.layers.clip_stack().push(clip)
}
ctx.windows.set_active_z(-1)
Response::with_focus(
id, window_rect, window_hovered, clicked, is_dragging, is_dragging, false, false,
false,
)
}
///|
fn clamp_window_pos(
pos : @math.Vec2,
size : @math.Vec2,
bounds : @math.Rect,
) -> @math.Vec2 {
let max_x = bounds.x + bounds.w - size.x
let max_y = bounds.y + bounds.h - size.y
let x = if pos.x < bounds.x {
bounds.x
} else if pos.x > max_x {
max_x
} else {
pos.x
}
let y = if pos.y < bounds.y {
bounds.y
} else if pos.y > max_y {
max_y
} else {
pos.y
}
@math.Vec2::new(x, y)
}