use crate::{Space, Text, TitleBar};
use gpui::{
AnyElement, App, Component, IntoElement, ParentElement, RenderOnce, SharedString, Styled,
Window, WindowDecorations, WindowOptions, div, point, px,
};
use liora_core::Config;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum WindowFrameMode {
#[default]
System,
Custom,
}
impl WindowFrameMode {
pub fn is_custom(self) -> bool {
matches!(self, Self::Custom)
}
pub fn from_custom(custom: bool) -> Self {
if custom { Self::Custom } else { Self::System }
}
pub fn label(self) -> &'static str {
match self {
Self::System => "System frame",
Self::Custom => "Custom frame",
}
}
}
pub fn apply_window_frame_mode(mut options: WindowOptions, mode: WindowFrameMode) -> WindowOptions {
match mode {
WindowFrameMode::System => {
if let Some(titlebar) = options.titlebar.as_mut() {
titlebar.appears_transparent = false;
titlebar.traffic_light_position = None;
}
options.window_decorations = Some(WindowDecorations::Server);
}
WindowFrameMode::Custom => {
if let Some(titlebar) = options.titlebar.as_mut() {
titlebar.appears_transparent = true;
titlebar.traffic_light_position = Some(point(px(12.0), px(12.0)));
}
options.window_decorations = Some(WindowDecorations::Client);
}
}
options
}
pub fn request_window_frame_mode(window: &mut Window, mode: WindowFrameMode) {
let decorations = match mode {
WindowFrameMode::System => WindowDecorations::Server,
WindowFrameMode::Custom => WindowDecorations::Client,
};
window.request_decorations(decorations);
}
pub fn frame_mode_switch_row(switch: impl IntoElement, mode: WindowFrameMode) -> impl IntoElement {
Space::new()
.gap_sm()
.child(Text::new("Frame"))
.child(switch)
.child(Text::new(mode.label()).size(px(12.0)))
}
pub struct AppWindowFrame {
titlebar: TitleBar,
mode: WindowFrameMode,
content: AnyElement,
}
impl AppWindowFrame {
pub fn new(title: impl Into<SharedString>, content: impl IntoElement) -> Self {
Self {
titlebar: TitleBar::new().title(title),
mode: WindowFrameMode::System,
content: content.into_any_element(),
}
}
pub fn titlebar(mut self, titlebar: TitleBar) -> Self {
self.titlebar = titlebar;
self
}
pub fn subtitle(mut self, subtitle: impl Into<SharedString>) -> Self {
self.titlebar = self.titlebar.subtitle(subtitle);
self
}
pub fn mode(mut self, mode: WindowFrameMode) -> Self {
self.mode = mode;
self
}
pub fn action(mut self, action: impl IntoElement) -> Self {
self.titlebar = self.titlebar.action(action);
self
}
pub fn actions(mut self, actions: impl IntoIterator<Item = impl IntoElement>) -> Self {
self.titlebar = self.titlebar.actions(actions);
self
}
pub fn on_close(mut self, close: impl Fn(&mut Window, &mut App) + 'static) -> Self {
self.titlebar = self.titlebar.on_close(close);
self
}
}
impl RenderOnce for AppWindowFrame {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
if !self.mode.is_custom() {
return self.content;
}
let theme = cx.global::<Config>().theme.clone();
let titlebar = self.titlebar.render(window, cx);
div()
.size_full()
.flex()
.flex_col()
.bg(theme.neutral.body)
.child(titlebar)
.child(div().flex_1().min_h_0().child(self.content))
.into_any_element()
}
}
impl IntoElement for AppWindowFrame {
type Element = Component<Self>;
fn into_element(self) -> Self::Element {
Component::new(self)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn frame_mode_tracks_custom_state_and_labels() {
assert!(!WindowFrameMode::System.is_custom());
assert!(WindowFrameMode::Custom.is_custom());
assert_eq!(WindowFrameMode::from_custom(false), WindowFrameMode::System);
assert_eq!(WindowFrameMode::from_custom(true), WindowFrameMode::Custom);
assert_eq!(WindowFrameMode::Custom.label(), "Custom frame");
}
#[test]
fn window_frame_options_switch_between_server_and_client_decorations() {
let custom = apply_window_frame_mode(WindowOptions::default(), WindowFrameMode::Custom);
assert!(
custom
.titlebar
.as_ref()
.is_some_and(|titlebar| titlebar.appears_transparent)
);
assert_eq!(custom.window_decorations, Some(WindowDecorations::Client));
let system = apply_window_frame_mode(custom, WindowFrameMode::System);
assert!(
system
.titlebar
.as_ref()
.is_some_and(|titlebar| !titlebar.appears_transparent)
);
assert_eq!(system.window_decorations, Some(WindowDecorations::Server));
let production = include_str!("window_frame.rs")
.split("#[cfg(test)]")
.next()
.unwrap();
assert!(production.contains("pub fn request_window_frame_mode"));
assert!(production.contains("window.request_decorations(decorations)"));
}
#[test]
fn custom_window_frame_renders_native_window_control_areas() {
let frame_source = include_str!("window_frame.rs")
.split("#[cfg(test)]")
.next()
.unwrap();
let titlebar_source = include_str!("titlebar.rs")
.split("#[cfg(test)]")
.next()
.unwrap();
assert!(frame_source.contains("TitleBar::new"));
assert!(frame_source.contains("titlebar.render"));
assert!(titlebar_source.contains("WindowControlArea::Drag"));
assert!(titlebar_source.contains("WindowControlArea::Min"));
assert!(titlebar_source.contains("WindowControlArea::Max"));
assert!(titlebar_source.contains("WindowControlArea::Close"));
assert!(titlebar_source.contains("theme.danger.base"));
assert!(titlebar_source.contains("theme.neutral.inverted"));
assert!(titlebar_source.contains("start_window_move"));
assert!(titlebar_source.contains("titlebar_double_click"));
}
}