Skip to main content

fluent_app/
chrome.rs

1use gpui::{
2    canvas, div, point, prelude::*, px, App, Bounds, Context, CursorStyle, Decorations, Entity,
3    HitboxBehavior, IntoElement, MouseButton, Pixels, Point, Render, ResizeEdge, Size, Window,
4};
5
6/// Width of the invisible edge zone that captures resize gestures when the
7/// window is rendered with client-side decorations.
8const RESIZE_INSET: Pixels = px(4.0);
9
10/// Wraps a user's window content with client-decoration helpers:
11///
12/// - 8 resize edges/corners around the perimeter that change the cursor and
13///   trigger `start_window_resize` on press.
14/// - The wrapped content is rendered untouched inside — interactive children
15///   still receive their own mouse events as normal.
16///
17/// `FluentApp::run` installs this automatically when
18/// `WindowDecorations::Client` is in use, so consumers don't need to opt in.
19pub struct WindowChrome<V: Render + 'static> {
20    content: Entity<V>,
21}
22
23impl<V: Render + 'static> WindowChrome<V> {
24    pub fn new(content: Entity<V>) -> Self {
25        Self { content }
26    }
27}
28
29impl<V: Render + 'static> Render for WindowChrome<V> {
30    fn render(&mut self, window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
31        let decorations = window.window_decorations();
32        let content = self.content.clone();
33
34        div().size_full().child(content).map(|d| match decorations {
35            Decorations::Server => d,
36            Decorations::Client { .. } => d
37                // Edge hitbox: only sets the cursor style when hovering an edge.
38                // The on_mouse_down handler below is what actually starts the resize.
39                .child(
40                    canvas(
41                        |_bounds, window, _cx| {
42                            window.insert_hitbox(
43                                Bounds::new(
44                                    point(px(0.0), px(0.0)),
45                                    window.window_bounds().get_bounds().size,
46                                ),
47                                HitboxBehavior::Normal,
48                            )
49                        },
50                        move |_bounds, hitbox, window, _cx| {
51                            let mouse = window.mouse_position();
52                            let size = window.window_bounds().get_bounds().size;
53                            let Some(edge) = resize_edge_at(mouse, RESIZE_INSET, size) else {
54                                return;
55                            };
56                            window.set_cursor_style(cursor_for_edge(edge), &hitbox);
57                        },
58                    )
59                    .size_full()
60                    .absolute(),
61                )
62                .on_mouse_move(|_, window, _| window.refresh())
63                .on_mouse_down(MouseButton::Left, move |e, window, _| {
64                    let size = window.window_bounds().get_bounds().size;
65                    if let Some(edge) = resize_edge_at(e.position, RESIZE_INSET, size) {
66                        window.start_window_resize(edge);
67                    }
68                }),
69        })
70    }
71}
72
73/// Construct a chrome wrapper entity for `content`. Used by `FluentApp::run`.
74pub fn wrap_with_chrome<V: Render + 'static>(
75    content: Entity<V>,
76    cx: &mut App,
77) -> Entity<WindowChrome<V>> {
78    cx.new(|_| WindowChrome::new(content))
79}
80
81fn resize_edge_at(pos: Point<Pixels>, inset: Pixels, size: Size<Pixels>) -> Option<ResizeEdge> {
82    let top = pos.y < inset;
83    let bottom = pos.y > size.height - inset;
84    let left = pos.x < inset;
85    let right = pos.x > size.width - inset;
86    Some(match (top, bottom, left, right) {
87        (true, _, true, _) => ResizeEdge::TopLeft,
88        (true, _, _, true) => ResizeEdge::TopRight,
89        (_, true, true, _) => ResizeEdge::BottomLeft,
90        (_, true, _, true) => ResizeEdge::BottomRight,
91        (true, _, _, _) => ResizeEdge::Top,
92        (_, true, _, _) => ResizeEdge::Bottom,
93        (_, _, true, _) => ResizeEdge::Left,
94        (_, _, _, true) => ResizeEdge::Right,
95        _ => return None,
96    })
97}
98
99fn cursor_for_edge(edge: ResizeEdge) -> CursorStyle {
100    match edge {
101        ResizeEdge::Top | ResizeEdge::Bottom => CursorStyle::ResizeUpDown,
102        ResizeEdge::Left | ResizeEdge::Right => CursorStyle::ResizeLeftRight,
103        ResizeEdge::TopLeft | ResizeEdge::BottomRight => CursorStyle::ResizeUpLeftDownRight,
104        ResizeEdge::TopRight | ResizeEdge::BottomLeft => CursorStyle::ResizeUpRightDownLeft,
105    }
106}