1 | // LAF Library |
2 | // Copyright (c) 2021 Igara Studio S.A. |
3 | // |
4 | // This file is released under the terms of the MIT license. |
5 | // Read LICENSE.txt for more information. |
6 | |
7 | #include "gfx/path.h" |
8 | #include "os/os.h" |
9 | |
10 | using Hit = os::Hit; |
11 | |
12 | const int kTitleBarSize = 32; |
13 | const int kButtonSize = 32; |
14 | const int kResizeBorder = 8; |
15 | |
16 | const gfx::Color kTitleBarBase = gfx::rgba(100, 100, 200); |
17 | const gfx::Color kTitleBarHigh = gfx::rgba(200, 200, 220); |
18 | const gfx::Color kTitleBarText = gfx::rgba(25, 25, 50); |
19 | const gfx::Color kContentBase = gfx::rgba(40, 40, 35); |
20 | const gfx::Color kContentHigh = gfx::rgba(50, 55, 45); |
21 | const gfx::Color kContentText = gfx::rgba(105, 115, 85); |
22 | const gfx::Color kContentEdge = gfx::rgba(200, 200, 100); |
23 | |
24 | Hit hit_test(os::Window* window, |
25 | const gfx::Point& pos) |
26 | { |
27 | // For a full screen window, we are always in the content area |
28 | if (window->isFullscreen()) |
29 | return Hit::Content; |
30 | |
31 | gfx::Rect rc = window->bounds(); |
32 | gfx::Rect rc2 = rc; |
33 | rc2.shrink(kResizeBorder); |
34 | |
35 | // Mouse in client area |
36 | if (!rc.contains(pos)) { |
37 | return Hit::Content; |
38 | } |
39 | // Resize edges |
40 | else if (!rc2.contains(pos) && |
41 | // macOS cannot start the resizing actions (just the window movement) |
42 | os::instance()->hasCapability(os::Capabilities::CanStartWindowResize)) { |
43 | if (pos.y < kResizeBorder) { |
44 | if (pos.x < kResizeBorder) return Hit::TopLeft; |
45 | else if (pos.x > rc.x2()-kResizeBorder) return Hit::TopRight; |
46 | else return Hit::Top; |
47 | } |
48 | else if (pos.y > rc.y2()-kResizeBorder) { |
49 | if (pos.x < kResizeBorder) return Hit::BottomLeft; |
50 | else if (pos.x > rc.x2()-kResizeBorder) return Hit::BottomRight; |
51 | else return Hit::Bottom; |
52 | } |
53 | else { |
54 | if (pos.x < rc.w/2) return Hit::Left; |
55 | return Hit::Right; |
56 | } |
57 | } |
58 | else if (pos.y <= kTitleBarSize) { |
59 | if (pos.x > rc.x2()-kButtonSize) return Hit::CloseButton; |
60 | else if (pos.x > rc.x2()-kButtonSize*2) return Hit::MaximizeButton; |
61 | else if (pos.x > rc.x2()-kButtonSize*3) return Hit::MinimizeButton; |
62 | else return Hit::TitleBar; |
63 | } |
64 | else { |
65 | return Hit::Content; |
66 | } |
67 | } |
68 | |
69 | void draw_button(os::Surface* surface, int x, Hit button, const Hit hit) |
70 | { |
71 | os::Paint p; |
72 | gfx::Rect box(x, 0, kButtonSize, kButtonSize); |
73 | |
74 | p.color(hit == button ? kTitleBarHigh: kTitleBarBase); |
75 | p.style(os::Paint::Fill); |
76 | surface->drawRect(box, p); |
77 | |
78 | p.color(gfx::rgba(25, 25, 50)); |
79 | p.style(os::Paint::Stroke); |
80 | surface->drawRect(gfx::Rect(x, 0, 2, kButtonSize), p); |
81 | |
82 | // Draw icon |
83 | box.shrink(11); |
84 | box.inflate(1, 1); |
85 | p.strokeWidth(1.5f); |
86 | p.antialias(true); |
87 | switch (button) { |
88 | case Hit::MinimizeButton: |
89 | surface->drawRect(gfx::Rect(box.x, box.y2()-2, box.w, 1), p); |
90 | break; |
91 | case Hit::MaximizeButton: |
92 | surface->drawRect(gfx::Rect(box), p); |
93 | break; |
94 | case Hit::CloseButton: { |
95 | gfx::Path path; |
96 | path.moveTo(box.x, box.y); |
97 | path.lineTo(box.x2(), box.y2()); |
98 | path.moveTo(box.x2(), box.y); |
99 | path.lineTo(box.x, box.y2()); |
100 | surface->drawPath(path, p); |
101 | break; |
102 | } |
103 | } |
104 | } |
105 | |
106 | void draw_window(os::Window* window, const Hit hit) |
107 | { |
108 | os::Surface* surface = window->surface(); |
109 | os::SurfaceLock lock(surface); |
110 | gfx::Rect rc = surface->bounds(); |
111 | gfx::Rect rc2 = rc; |
112 | os::Paint p; |
113 | p.style(os::Paint::Fill); |
114 | |
115 | // Draw custom title bar area |
116 | if (!window->isFullscreen()) { |
117 | rc2.h = kTitleBarSize; |
118 | |
119 | p.color(hit == Hit::TitleBar ? kTitleBarHigh: kTitleBarBase); |
120 | surface->drawRect(gfx::Rect(rc2).inflate(-kButtonSize*3, 0), p); |
121 | |
122 | rc2.y += kTitleBarSize/2 - 10; |
123 | |
124 | p.color(kTitleBarText); |
125 | os::draw_text(surface, nullptr, "Custom Window" , |
126 | rc2.center(), &p, os::TextAlign::Center); |
127 | |
128 | // Draw buttons |
129 | draw_button(surface, rc.x2()-kButtonSize, Hit::CloseButton, hit); |
130 | draw_button(surface, rc.x2()-kButtonSize*2, Hit::MaximizeButton, hit); |
131 | draw_button(surface, rc.x2()-kButtonSize*3, Hit::MinimizeButton, hit); |
132 | |
133 | // Client area |
134 | rc2 = rc; |
135 | rc2.y += kTitleBarSize; |
136 | rc2.h -= kTitleBarSize; |
137 | } |
138 | |
139 | // Draw client area |
140 | p.color(hit == Hit::Content ? kContentHigh: kContentBase); |
141 | surface->drawRect(rc2, p); |
142 | |
143 | p.style(os::Paint::Style::Stroke); |
144 | p.color(kContentEdge); |
145 | surface->drawRect(rc2, p); |
146 | |
147 | p.style(os::Paint::Style::Fill); |
148 | p.color(kContentText); |
149 | os::draw_text(surface, nullptr, "Content Rect" , |
150 | rc2.center(), &p, os::TextAlign::Center); |
151 | |
152 | if (window->isFullscreen()) { |
153 | auto pos = rc2.center(); |
154 | pos.y += 24; |
155 | os::draw_text(surface, nullptr, "(F key or F11 to exit full screen)" , |
156 | pos, &p, os::TextAlign::Center); |
157 | } |
158 | |
159 | if (window->isVisible()) |
160 | window->invalidateRegion(gfx::Region(rc)); |
161 | else |
162 | window->setVisible(true); |
163 | } |
164 | |
165 | bool update_hit(os::Window* window, |
166 | const os::Event& ev, |
167 | Hit& hit) |
168 | { |
169 | Hit newHit = hit_test(window, ev.position()); |
170 | if (newHit != hit) { |
171 | hit = newHit; |
172 | return true; |
173 | } |
174 | else |
175 | return false; |
176 | } |
177 | |
178 | os::WindowRef create_window() |
179 | { |
180 | os::WindowSpec spec; |
181 | spec.contentRect(gfx::Rect(32, 32, 400, 300)); |
182 | spec.titled(false); |
183 | spec.borderless(true); |
184 | |
185 | os::WindowRef window = os::instance()->makeWindow(spec); |
186 | window->setTitle("Custom Window" ); |
187 | window->handleHitTest = hit_test; |
188 | |
189 | return window; |
190 | } |
191 | |
192 | void handle_mouse_move(os::Window* window, |
193 | const Hit hit) |
194 | { |
195 | os::NativeCursor cursor = os::NativeCursor::Arrow; |
196 | switch (hit) { |
197 | case Hit::Content: cursor = os::NativeCursor::Arrow; break; |
198 | case Hit::TitleBar: cursor = os::NativeCursor::Move; break; |
199 | case Hit::TopLeft: cursor = os::NativeCursor::SizeNW; break; |
200 | case Hit::Top: cursor = os::NativeCursor::SizeN; break; |
201 | case Hit::TopRight: cursor = os::NativeCursor::SizeNE; break; |
202 | case Hit::Left: cursor = os::NativeCursor::SizeW; break; |
203 | case Hit::Right: cursor = os::NativeCursor::SizeE; break; |
204 | case Hit::BottomLeft: cursor = os::NativeCursor::SizeSW; break; |
205 | case Hit::Bottom: cursor = os::NativeCursor::SizeS; break; |
206 | case Hit::BottomRight: cursor = os::NativeCursor::SizeSE; break; |
207 | default: break; |
208 | } |
209 | window->setCursor(cursor); |
210 | } |
211 | |
212 | bool handle_mouse_down(os::Window* window, |
213 | const os::Event& ev, |
214 | const Hit hit) |
215 | { |
216 | os::NativeCursor cursor = os::NativeCursor::Arrow; |
217 | os::WindowAction action = os::WindowAction::Move; |
218 | switch (hit) { |
219 | case Hit::Content: return true; |
220 | case Hit::TitleBar: action = os::WindowAction::Move; break; |
221 | case Hit::TopLeft: action = os::WindowAction::ResizeFromTopLeft; break; |
222 | case Hit::Top: action = os::WindowAction::ResizeFromTop; break; |
223 | case Hit::TopRight: action = os::WindowAction::ResizeFromTopRight; break; |
224 | case Hit::Left: action = os::WindowAction::ResizeFromLeft; break; |
225 | case Hit::Right: action = os::WindowAction::ResizeFromRight; break; |
226 | case Hit::BottomLeft: action = os::WindowAction::ResizeFromBottomLeft; break; |
227 | case Hit::Bottom: action = os::WindowAction::ResizeFromBottom; break; |
228 | case Hit::BottomRight: action = os::WindowAction::ResizeFromBottomRight; break; |
229 | case Hit::MinimizeButton: window->minimize(); return true; |
230 | case Hit::MaximizeButton: window->maximize(); return true; |
231 | case Hit::CloseButton: return false; |
232 | } |
233 | window->performWindowAction(action, &ev); |
234 | return true; |
235 | } |
236 | |
237 | int app_main(int argc, char* argv[]) |
238 | { |
239 | os::SystemRef system = os::make_system(); |
240 | system->setAppMode(os::AppMode::GUI); |
241 | |
242 | os::WindowRef window = create_window(); |
243 | Hit hit = Hit::None; // Current area which the mouse cursor hits |
244 | window->activate(); |
245 | |
246 | system->handleWindowResize = [&](os::Window* w) { draw_window(w, hit); }; |
247 | system->finishLaunching(); |
248 | system->activateApp(); |
249 | |
250 | os::EventQueue* queue = system->eventQueue(); |
251 | bool running = true; |
252 | bool redraw = true; |
253 | while (running) { |
254 | if (redraw) { |
255 | redraw = false; |
256 | draw_window(window.get(), hit); |
257 | } |
258 | |
259 | os::Event ev; |
260 | queue->getEvent(ev); |
261 | |
262 | switch (ev.type()) { |
263 | |
264 | case os::Event::CloseApp: |
265 | case os::Event::CloseWindow: |
266 | running = false; |
267 | break; |
268 | |
269 | case os::Event::KeyDown: |
270 | switch (ev.scancode()) { |
271 | case os::kKeyEsc: |
272 | running = false; |
273 | break; |
274 | case os::kKeyF: |
275 | case os::kKeyF11: |
276 | window->setFullscreen(!window->isFullscreen()); |
277 | break; |
278 | } |
279 | break; |
280 | |
281 | case os::Event::MouseEnter: |
282 | case os::Event::MouseMove: |
283 | redraw = update_hit(window.get(), ev, hit); |
284 | handle_mouse_move(window.get(), hit); |
285 | break; |
286 | |
287 | case os::Event::MouseDown: |
288 | redraw = update_hit(window.get(), ev, hit); |
289 | if (!handle_mouse_down(window.get(), ev, hit)) |
290 | running = false; |
291 | break; |
292 | |
293 | case os::Event::MouseLeave: |
294 | redraw = update_hit(window.get(), ev, hit); |
295 | break; |
296 | |
297 | default: |
298 | break; |
299 | } |
300 | } |
301 | |
302 | return 0; |
303 | } |
304 | |