Serenity Operating System
1/*
2 * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
3 *
4 * SPDX-License-Identifier: BSD-2-Clause
5 */
6
7#include "ConnectionFromClient.h"
8#include <AK/Badge.h>
9#include <LibGfx/Font/Font.h>
10#include <LibGfx/Painter.h>
11#include <LibGfx/StylePainter.h>
12#include <LibGfx/WindowTheme.h>
13#include <WindowServer/Button.h>
14#include <WindowServer/Compositor.h>
15#include <WindowServer/Event.h>
16#include <WindowServer/MultiScaleBitmaps.h>
17#include <WindowServer/Screen.h>
18#include <WindowServer/Window.h>
19#include <WindowServer/WindowFrame.h>
20#include <WindowServer/WindowManager.h>
21
22namespace WindowServer {
23
24static Gfx::WindowTheme::WindowType to_theme_window_type(WindowType type)
25{
26 switch (type) {
27 case WindowType::Normal:
28 return Gfx::WindowTheme::WindowType::Normal;
29 case WindowType::Notification:
30 return Gfx::WindowTheme::WindowType::Notification;
31 default:
32 return Gfx::WindowTheme::WindowType::Other;
33 }
34}
35
36static Gfx::WindowTheme::WindowMode to_theme_window_mode(WindowMode mode)
37{
38 switch (mode) {
39 case WindowMode::RenderAbove:
40 return Gfx::WindowTheme::WindowMode::RenderAbove;
41 default:
42 return Gfx::WindowTheme::WindowMode::Other;
43 }
44}
45
46static Button::Icon s_minimize_icon;
47static Button::Icon s_maximize_icon;
48static Button::Icon s_restore_icon;
49static Button::Icon s_close_icon;
50static Button::Icon s_close_modified_icon;
51
52static RefPtr<MultiScaleBitmaps> s_active_window_shadow;
53static RefPtr<MultiScaleBitmaps> s_inactive_window_shadow;
54static RefPtr<MultiScaleBitmaps> s_menu_shadow;
55static RefPtr<MultiScaleBitmaps> s_taskbar_shadow;
56static RefPtr<MultiScaleBitmaps> s_tooltip_shadow;
57static DeprecatedString s_last_active_window_shadow_path;
58static DeprecatedString s_last_inactive_window_shadow_path;
59static DeprecatedString s_last_menu_shadow_path;
60static DeprecatedString s_last_taskbar_shadow_path;
61static DeprecatedString s_last_tooltip_shadow_path;
62
63static Gfx::IntRect frame_rect_for_window(Window& window, Gfx::IntRect const& rect)
64{
65 if (window.is_frameless())
66 return rect;
67 int menu_row_count = (window.menubar().has_menus() && window.should_show_menubar()) ? 1 : 0;
68 return Gfx::WindowTheme::current().frame_rect_for_window(to_theme_window_type(window.type()), to_theme_window_mode(window.mode()), rect, WindowManager::the().palette(), menu_row_count);
69}
70
71WindowFrame::WindowFrame(Window& window)
72 : m_window(window)
73{
74 // Because Window constructs a WindowFrame during its construction, we need
75 // to be careful and defer doing initialization that assumes a fully
76 // constructed Window. It is fully constructed when Window notifies us with
77 // a call to WindowFrame::window_was_constructed.
78}
79
80void WindowFrame::window_was_constructed(Badge<Window>)
81{
82 if (m_window.is_closeable()) {
83 auto button = make<Button>(*this, [this](auto&) {
84 m_window.handle_window_menu_action(WindowMenuAction::Close);
85 });
86 m_close_button = button.ptr();
87 m_buttons.append(move(button));
88 }
89
90 if (m_window.is_resizable()) {
91 auto button = make<Button>(*this, [this](auto&) {
92 m_window.handle_window_menu_action(WindowMenuAction::MaximizeOrRestore);
93 });
94 button->on_middle_click = [&](auto&) {
95 if (m_window.tile_type() == WindowTileType::VerticallyMaximized)
96 m_window.set_untiled();
97 else
98 m_window.set_tiled(WindowTileType::VerticallyMaximized);
99 };
100 button->on_secondary_click = [&](auto&) {
101 if (m_window.tile_type() == WindowTileType::HorizontallyMaximized)
102 m_window.set_untiled();
103 else
104 m_window.set_tiled(WindowTileType::HorizontallyMaximized);
105 };
106 m_maximize_button = button.ptr();
107 m_buttons.append(move(button));
108 }
109
110 if (m_window.is_minimizable() && !m_window.is_modal()) {
111 auto button = make<Button>(*this, [this](auto&) {
112 m_window.handle_window_menu_action(WindowMenuAction::MinimizeOrUnminimize);
113 });
114 m_minimize_button = button.ptr();
115 m_buttons.append(move(button));
116 }
117
118 set_button_icons();
119
120 m_has_alpha_channel = Gfx::WindowTheme::current().frame_uses_alpha(window_state_for_theme(), WindowManager::the().palette());
121}
122
123WindowFrame::~WindowFrame() = default;
124
125void WindowFrame::set_button_icons()
126{
127 set_dirty();
128 if (m_window.is_frameless())
129 return;
130
131 auto button_style = WindowManager::the().palette().title_buttons_icon_only()
132 ? Button::Style::IconOnly
133 : Button::Style::Normal;
134
135 if (m_window.is_closeable()) {
136 m_close_button->set_icon(m_window.is_modified() ? s_close_modified_icon : s_close_icon);
137 m_close_button->set_style(button_style);
138 }
139 if (m_window.is_minimizable() && !m_window.is_modal()) {
140 m_minimize_button->set_icon(s_minimize_icon);
141 m_minimize_button->set_style(button_style);
142 }
143 if (m_window.is_resizable()) {
144 m_maximize_button->set_icon(m_window.is_maximized() ? s_restore_icon : s_maximize_icon);
145 m_maximize_button->set_style(button_style);
146 }
147}
148
149void WindowFrame::reload_config()
150{
151 DeprecatedString icons_path = WindowManager::the().palette().title_button_icons_path();
152
153 auto reload_bitmap = [&](RefPtr<MultiScaleBitmaps>& multiscale_bitmap, StringView path, StringView default_path = ""sv) {
154 StringBuilder full_path;
155 full_path.append(icons_path);
156 full_path.append(path);
157 if (multiscale_bitmap)
158 multiscale_bitmap->load(full_path.string_view(), default_path);
159 else
160 multiscale_bitmap = MultiScaleBitmaps::create(full_path.string_view(), default_path);
161 };
162
163 auto reload_icon = [&](Button::Icon& icon, StringView name, StringView default_path) {
164 StringBuilder full_name;
165 full_name.append(name);
166 full_name.append(".png"sv);
167 reload_bitmap(icon.bitmap, full_name.string_view(), default_path);
168 // Note: No default for hover bitmaps
169 full_name.clear();
170 full_name.append(name);
171 full_name.append("-hover.png"sv);
172 reload_bitmap(icon.hover_bitmap, full_name.string_view());
173 };
174 reload_icon(s_minimize_icon, "window-minimize"sv, "/res/icons/16x16/downward-triangle.png"sv);
175 reload_icon(s_maximize_icon, "window-maximize"sv, "/res/icons/16x16/upward-triangle.png"sv);
176 reload_icon(s_restore_icon, "window-restore"sv, "/res/icons/16x16/window-restore.png"sv);
177 reload_icon(s_close_icon, "window-close"sv, "/res/icons/16x16/window-close.png"sv);
178 reload_icon(s_close_modified_icon, "window-close-modified"sv, "/res/icons/16x16/window-close-modified.png"sv);
179
180 auto load_shadow = [](DeprecatedString const& path, DeprecatedString& last_path, RefPtr<MultiScaleBitmaps>& shadow_bitmap) {
181 if (path.is_empty()) {
182 last_path = DeprecatedString::empty();
183 shadow_bitmap = nullptr;
184 } else if (!shadow_bitmap || last_path != path) {
185 if (shadow_bitmap)
186 shadow_bitmap->load(path);
187 else
188 shadow_bitmap = MultiScaleBitmaps::create(path);
189 if (shadow_bitmap)
190 last_path = path;
191 else
192 last_path = DeprecatedString::empty();
193 }
194 };
195 load_shadow(WindowManager::the().palette().active_window_shadow_path(), s_last_active_window_shadow_path, s_active_window_shadow);
196 load_shadow(WindowManager::the().palette().inactive_window_shadow_path(), s_last_inactive_window_shadow_path, s_inactive_window_shadow);
197 load_shadow(WindowManager::the().palette().menu_shadow_path(), s_last_menu_shadow_path, s_menu_shadow);
198 load_shadow(WindowManager::the().palette().taskbar_shadow_path(), s_last_taskbar_shadow_path, s_taskbar_shadow);
199 load_shadow(WindowManager::the().palette().tooltip_shadow_path(), s_last_tooltip_shadow_path, s_tooltip_shadow);
200}
201
202MultiScaleBitmaps const* WindowFrame::shadow_bitmap() const
203{
204 if (m_window.is_frameless() && !m_window.has_forced_shadow())
205 return nullptr;
206 switch (m_window.type()) {
207 case WindowType::Desktop:
208 return nullptr;
209 case WindowType::Menu:
210 if (!WindowManager::the().system_effects().menu_shadow())
211 return nullptr;
212 return s_menu_shadow;
213 case WindowType::Autocomplete:
214 case WindowType::Tooltip:
215 if (!WindowManager::the().system_effects().tooltip_shadow())
216 return nullptr;
217 return s_tooltip_shadow;
218 case WindowType::Taskbar:
219 return s_taskbar_shadow;
220 case WindowType::AppletArea:
221 return nullptr;
222 case WindowType::WindowSwitcher:
223 return nullptr;
224 case WindowType::Popup:
225 if (!WindowManager::the().system_effects().window_shadow())
226 return nullptr;
227 if (!m_window.has_forced_shadow())
228 return nullptr;
229 return s_active_window_shadow;
230 default:
231 if (!WindowManager::the().system_effects().window_shadow())
232 return nullptr;
233 // FIXME: Support shadow for themes with border radius
234 if (WindowManager::the().palette().window_border_radius() > 0)
235 return nullptr;
236 if (auto* highlight_window = WindowManager::the().highlight_window())
237 return highlight_window == &m_window ? s_active_window_shadow : s_inactive_window_shadow;
238 return m_window.is_active() ? s_active_window_shadow : s_inactive_window_shadow;
239 }
240}
241
242bool WindowFrame::has_shadow() const
243{
244 if (auto* shadow_bitmap = this->shadow_bitmap(); shadow_bitmap && shadow_bitmap->format() == Gfx::BitmapFormat::BGRA8888)
245 return true;
246 return false;
247}
248
249void WindowFrame::did_set_maximized(Badge<Window>, bool maximized)
250{
251 VERIFY(m_maximize_button);
252 set_dirty();
253 m_maximize_button->set_icon(maximized ? s_restore_icon : s_maximize_icon);
254}
255
256Gfx::IntRect WindowFrame::menubar_rect() const
257{
258 if (!m_window.menubar().has_menus() || !m_window.should_show_menubar())
259 return {};
260 return Gfx::WindowTheme::current().menubar_rect(to_theme_window_type(m_window.type()), to_theme_window_mode(m_window.mode()), m_window.rect(), WindowManager::the().palette(), menu_row_count());
261}
262
263Gfx::IntRect WindowFrame::titlebar_rect() const
264{
265 return Gfx::WindowTheme::current().titlebar_rect(to_theme_window_type(m_window.type()), to_theme_window_mode(m_window.mode()), m_window.rect(), WindowManager::the().palette());
266}
267
268Gfx::IntRect WindowFrame::titlebar_icon_rect() const
269{
270 return Gfx::WindowTheme::current().titlebar_icon_rect(to_theme_window_type(m_window.type()), to_theme_window_mode(m_window.mode()), m_window.rect(), WindowManager::the().palette());
271}
272
273Gfx::IntRect WindowFrame::titlebar_text_rect() const
274{
275 return Gfx::WindowTheme::current().titlebar_text_rect(to_theme_window_type(m_window.type()), to_theme_window_mode(m_window.mode()), m_window.rect(), WindowManager::the().palette());
276}
277
278Gfx::WindowTheme::WindowState WindowFrame::window_state_for_theme() const
279{
280 auto& wm = WindowManager::the();
281
282 if (m_flash_timer && m_flash_timer->is_active())
283 return m_flash_counter & 1 ? Gfx::WindowTheme::WindowState::Highlighted : Gfx::WindowTheme::WindowState::Inactive;
284
285 if (&m_window == wm.highlight_window())
286 return Gfx::WindowTheme::WindowState::Highlighted;
287 if (&m_window == wm.m_move_window)
288 return Gfx::WindowTheme::WindowState::Moving;
289 if (m_window.is_active())
290 return Gfx::WindowTheme::WindowState::Active;
291 return Gfx::WindowTheme::WindowState::Inactive;
292}
293
294void WindowFrame::paint_notification_frame(Gfx::Painter& painter)
295{
296 auto palette = WindowManager::the().palette();
297 Gfx::WindowTheme::current().paint_notification_frame(painter, to_theme_window_mode(m_window.mode()), m_window.rect(), palette, m_buttons.last()->relative_rect());
298}
299
300void WindowFrame::paint_menubar(Gfx::Painter& painter)
301{
302 auto& wm = WindowManager::the();
303 auto& font = wm.font();
304 auto palette = wm.palette();
305 auto menubar_rect = this->menubar_rect();
306
307 painter.fill_rect(menubar_rect, palette.window());
308
309 Gfx::PainterStateSaver saver(painter);
310 painter.add_clip_rect(menubar_rect);
311 painter.translate(menubar_rect.location());
312
313 m_window.menubar().for_each_menu([&](Menu& menu) {
314 bool paint_as_flashed = ((&menu) == m_window.menubar().flashed_menu());
315 if (paint_as_flashed) {
316 auto flashed_rect = menu.rect_in_window_menubar();
317 flashed_rect.shrink(2, 2);
318 painter.fill_rect(flashed_rect, palette.selection());
319 }
320
321 auto text_rect = menu.rect_in_window_menubar();
322 Color text_color = (paint_as_flashed ? palette.selection_text() : palette.window_text());
323 auto is_open = menu.is_open();
324 if (is_open)
325 text_rect.translate_by(1, 1);
326 bool paint_as_pressed = is_open;
327 bool paint_as_hovered = !paint_as_pressed && &menu == MenuManager::the().hovered_menu();
328 if (paint_as_pressed || paint_as_hovered) {
329 Gfx::StylePainter::paint_button(painter, menu.rect_in_window_menubar(), palette, Gfx::ButtonStyle::Coolbar, paint_as_pressed, paint_as_hovered);
330 }
331 painter.draw_ui_text(text_rect, menu.name(), font, Gfx::TextAlignment::Center, text_color);
332 return IterationDecision::Continue;
333 });
334}
335
336void WindowFrame::paint_normal_frame(Gfx::Painter& painter)
337{
338 auto palette = WindowManager::the().palette();
339 Gfx::WindowTheme::current().paint_normal_frame(painter, window_state_for_theme(), to_theme_window_mode(m_window.mode()), m_window.rect(), m_window.computed_title(), m_window.icon(), palette, leftmost_titlebar_button_rect(), menu_row_count(), m_window.is_modified());
340
341 if (m_window.menubar().has_menus() && m_window.should_show_menubar())
342 paint_menubar(painter);
343}
344
345void WindowFrame::paint(Screen& screen, Gfx::Painter& painter, Gfx::IntRect const& rect)
346{
347 if (auto* cached = render_to_cache(screen))
348 cached->paint(*this, painter, rect);
349}
350
351void WindowFrame::PerScaleRenderedCache::paint(WindowFrame& frame, Gfx::Painter& painter, Gfx::IntRect const& rect)
352{
353 auto frame_rect = frame.unconstrained_render_rect();
354 auto window_rect = frame.window().rect();
355 if (m_top_bottom) {
356 auto top_bottom_height = frame_rect.height() - window_rect.height();
357 if (m_bottom_y > 0) {
358 // We have a top piece
359 auto src_rect = rect.intersected(Gfx::Rect { frame_rect.location(), { frame_rect.width(), m_bottom_y } });
360 if (!src_rect.is_empty())
361 painter.blit(src_rect.location(), *m_top_bottom, src_rect.translated(-frame_rect.location()), frame.opacity());
362 }
363 if (m_bottom_y < top_bottom_height) {
364 // We have a bottom piece
365 Gfx::IntRect rect_in_frame { frame_rect.x(), window_rect.bottom() + 1, frame_rect.width(), top_bottom_height - m_bottom_y };
366 auto src_rect = rect.intersected(rect_in_frame);
367 if (!src_rect.is_empty())
368 painter.blit(src_rect.location(), *m_top_bottom, src_rect.translated(-rect_in_frame.x(), -rect_in_frame.y() + m_bottom_y), frame.opacity());
369 }
370 }
371
372 if (m_left_right) {
373 auto left_right_width = frame_rect.width() - window_rect.width();
374 if (m_right_x > 0) {
375 // We have a left piece
376 Gfx::IntRect rect_in_frame { frame_rect.x(), window_rect.y(), m_right_x, window_rect.height() };
377 auto src_rect = rect.intersected(rect_in_frame);
378 if (!src_rect.is_empty())
379 painter.blit(src_rect.location(), *m_left_right, src_rect.translated(-rect_in_frame.location()), frame.opacity());
380 }
381 if (m_right_x < left_right_width) {
382 // We have a right piece
383 Gfx::IntRect rect_in_frame { window_rect.right() + 1, window_rect.y(), left_right_width - m_right_x, window_rect.height() };
384 auto src_rect = rect.intersected(rect_in_frame);
385 if (!src_rect.is_empty())
386 painter.blit(src_rect.location(), *m_left_right, src_rect.translated(-rect_in_frame.x() + m_right_x, -rect_in_frame.y()), frame.opacity());
387 }
388 }
389}
390
391void WindowFrame::render(Screen& screen, Gfx::Painter& painter)
392{
393 if (m_window.is_frameless())
394 return;
395
396 if (m_window.type() == WindowType::Notification)
397 paint_notification_frame(painter);
398 else if (m_window.type() == WindowType::Normal)
399 paint_normal_frame(painter);
400 else
401 return;
402
403 for (auto& button : m_buttons)
404 button->paint(screen, painter);
405}
406
407void WindowFrame::theme_changed()
408{
409 m_rendered_cache = {};
410
411 layout_buttons();
412 set_button_icons();
413
414 m_has_alpha_channel = Gfx::WindowTheme::current().frame_uses_alpha(window_state_for_theme(), WindowManager::the().palette());
415}
416
417auto WindowFrame::render_to_cache(Screen& screen) -> PerScaleRenderedCache*
418{
419 auto scale = screen.scale_factor();
420 PerScaleRenderedCache* rendered_cache;
421 auto cached_it = m_rendered_cache.find(scale);
422 if (cached_it == m_rendered_cache.end()) {
423 auto new_rendered_cache = make<PerScaleRenderedCache>();
424 rendered_cache = new_rendered_cache.ptr();
425 m_rendered_cache.set(scale, move(new_rendered_cache));
426 } else {
427 rendered_cache = cached_it->value.ptr();
428 }
429 rendered_cache->render(*this, screen);
430 return rendered_cache;
431}
432
433void WindowFrame::PerScaleRenderedCache::render(WindowFrame& frame, Screen& screen)
434{
435 if (!m_dirty)
436 return;
437 m_dirty = false;
438
439 auto scale = screen.scale_factor();
440
441 auto frame_rect = frame.rect();
442
443 auto frame_rect_including_shadow = frame_rect;
444 auto* shadow_bitmap = frame.shadow_bitmap();
445 Gfx::IntPoint shadow_offset;
446
447 if (shadow_bitmap) {
448 auto total_shadow_size = shadow_bitmap->bitmap(screen.scale_factor()).height();
449 frame_rect_including_shadow.inflate(total_shadow_size, total_shadow_size);
450 auto offset = total_shadow_size / 2;
451 shadow_offset = { offset, offset };
452 }
453
454 auto window_rect = frame.window().rect();
455
456 // TODO: if we stop using a scaling factor we should clear cached bitmaps from this map
457 static HashMap<int, RefPtr<Gfx::Bitmap>> s_tmp_bitmap_cache;
458 Gfx::Bitmap* tmp_bitmap;
459 {
460 auto tmp_it = s_tmp_bitmap_cache.find(scale);
461 if (tmp_it == s_tmp_bitmap_cache.end() || !tmp_it->value->size().contains(frame_rect_including_shadow.size())) {
462 // Explicitly clear the old bitmap first so this works on machines with very little memory
463 if (tmp_it != s_tmp_bitmap_cache.end())
464 tmp_it->value = nullptr;
465
466 auto bitmap_or_error = Gfx::Bitmap::create(Gfx::BitmapFormat::BGRA8888, frame_rect_including_shadow.size(), scale);
467 if (bitmap_or_error.is_error()) {
468 s_tmp_bitmap_cache.remove(scale);
469 dbgln("Could not create bitmap of size {}: {}", frame_rect_including_shadow.size(), bitmap_or_error.error());
470 return;
471 }
472 auto bitmap = bitmap_or_error.release_value();
473 tmp_bitmap = bitmap.ptr();
474 if (tmp_it != s_tmp_bitmap_cache.end())
475 tmp_it->value = move(bitmap);
476 else
477 s_tmp_bitmap_cache.set(scale, move(bitmap));
478 } else {
479 tmp_bitmap = tmp_it->value.ptr();
480 }
481 }
482
483 VERIFY(tmp_bitmap);
484
485 auto top_bottom_height = frame_rect_including_shadow.height() - window_rect.height();
486 auto left_right_width = frame_rect_including_shadow.width() - window_rect.width();
487
488 if (!m_top_bottom || m_top_bottom->width() != frame_rect_including_shadow.width() || m_top_bottom->height() != top_bottom_height || m_top_bottom->scale() != scale) {
489 if (top_bottom_height > 0)
490 m_top_bottom = Gfx::Bitmap::create(Gfx::BitmapFormat::BGRA8888, { frame_rect_including_shadow.width(), top_bottom_height }, scale).release_value_but_fixme_should_propagate_errors();
491 else
492 m_top_bottom = nullptr;
493 m_shadow_dirty = true;
494 }
495 if (!m_left_right || m_left_right->height() != frame_rect_including_shadow.height() || m_left_right->width() != left_right_width || m_left_right->scale() != scale) {
496 if (left_right_width > 0)
497 m_left_right = Gfx::Bitmap::create(Gfx::BitmapFormat::BGRA8888, { left_right_width, frame_rect_including_shadow.height() }, scale).release_value_but_fixme_should_propagate_errors();
498 else
499 m_left_right = nullptr;
500 m_shadow_dirty = true;
501 }
502
503 auto& frame_rect_to_update = m_shadow_dirty ? frame_rect_including_shadow : frame_rect;
504 Gfx::IntPoint update_location(m_shadow_dirty ? Gfx::IntPoint { 0, 0 } : shadow_offset);
505
506 Gfx::Painter painter(*tmp_bitmap);
507
508 // Clear the frame area, not including the window content area, which we don't care about
509 for (auto& rect : frame_rect_to_update.shatter(window_rect))
510 painter.clear_rect({ rect.location() - frame_rect_to_update.location(), rect.size() }, { 255, 255, 255, 0 });
511
512 if (m_shadow_dirty && shadow_bitmap)
513 Gfx::StylePainter::paint_simple_rect_shadow(painter, { { 0, 0 }, frame_rect_including_shadow.size() }, shadow_bitmap->bitmap(screen.scale_factor()));
514
515 {
516 Gfx::PainterStateSaver save(painter);
517 painter.translate(shadow_offset);
518 frame.render(screen, painter);
519 }
520
521 if (m_top_bottom && top_bottom_height > 0) {
522 m_bottom_y = window_rect.y() - frame_rect_including_shadow.y();
523 VERIFY(m_bottom_y >= 0);
524
525 Gfx::Painter top_bottom_painter(*m_top_bottom);
526 top_bottom_painter.add_clip_rect({ update_location, { frame_rect_to_update.width(), top_bottom_height - update_location.y() - (frame_rect_including_shadow.bottom() - frame_rect_to_update.bottom()) } });
527 if (m_bottom_y > 0)
528 top_bottom_painter.blit({ 0, 0 }, *tmp_bitmap, { 0, 0, frame_rect_including_shadow.width(), m_bottom_y }, 1.0, false);
529 if (m_bottom_y < top_bottom_height)
530 top_bottom_painter.blit({ 0, m_bottom_y }, *tmp_bitmap, { 0, frame_rect_including_shadow.height() - (frame_rect_including_shadow.bottom() - window_rect.bottom()), frame_rect_including_shadow.width(), top_bottom_height - m_bottom_y }, 1.0, false);
531 } else {
532 m_bottom_y = 0;
533 }
534
535 if (left_right_width > 0) {
536 m_right_x = window_rect.x() - frame_rect_including_shadow.x();
537 VERIFY(m_right_x >= 0);
538
539 Gfx::Painter left_right_painter(*m_left_right);
540 left_right_painter.add_clip_rect({ update_location, { left_right_width - update_location.x() - (frame_rect_including_shadow.right() - frame_rect_to_update.right()), window_rect.height() } });
541 if (m_right_x > 0)
542 left_right_painter.blit({ 0, 0 }, *tmp_bitmap, { 0, m_bottom_y, m_right_x, window_rect.height() }, 1.0, false);
543 if (m_right_x < left_right_width)
544 left_right_painter.blit({ m_right_x, 0 }, *tmp_bitmap, { (window_rect.right() - frame_rect_including_shadow.x()) + 1, m_bottom_y, frame_rect_including_shadow.width() - (frame_rect_including_shadow.right() - window_rect.right()), window_rect.height() }, 1.0, false);
545 } else {
546 m_right_x = 0;
547 }
548
549 m_shadow_dirty = false;
550}
551
552void WindowFrame::set_opacity(float opacity)
553{
554 if (m_opacity == opacity)
555 return;
556 bool was_opaque = is_opaque();
557 m_opacity = opacity;
558 if (was_opaque != is_opaque())
559 Compositor::the().invalidate_occlusions();
560 Compositor::the().invalidate_screen(render_rect());
561 WindowManager::the().notify_opacity_changed(m_window);
562}
563
564Gfx::IntRect WindowFrame::inflated_for_shadow(Gfx::IntRect const& frame_rect) const
565{
566 if (auto* shadow = shadow_bitmap()) {
567 auto total_shadow_size = shadow->default_bitmap().height();
568 return frame_rect.inflated(total_shadow_size, total_shadow_size);
569 }
570 return frame_rect;
571}
572
573Gfx::IntRect WindowFrame::rect() const
574{
575 return frame_rect_for_window(m_window, m_window.rect());
576}
577
578Gfx::IntRect WindowFrame::constrained_render_rect_to_screen(Gfx::IntRect const& render_rect) const
579{
580 if (m_window.is_tiled())
581 return render_rect.intersected(Screen::closest_to_rect(rect()).rect());
582 return render_rect;
583}
584
585Gfx::IntRect WindowFrame::leftmost_titlebar_button_rect() const
586{
587 if (!m_buttons.is_empty())
588 return m_buttons.last()->relative_rect();
589
590 auto rect = titlebar_rect();
591 rect.translate_by(rect.width(), 0);
592 return rect;
593}
594
595Gfx::IntRect WindowFrame::render_rect() const
596{
597 return constrained_render_rect_to_screen(inflated_for_shadow(rect()));
598}
599
600Gfx::IntRect WindowFrame::unconstrained_render_rect() const
601{
602 return inflated_for_shadow(rect());
603}
604
605Gfx::DisjointIntRectSet WindowFrame::opaque_render_rects() const
606{
607 auto border_radius = WindowManager::the().palette().window_border_radius();
608 if (has_alpha_channel() || border_radius > 0) {
609 if (m_window.is_opaque())
610 return constrained_render_rect_to_screen(m_window.rect());
611 return {};
612 }
613 if (m_window.is_opaque())
614 return constrained_render_rect_to_screen(rect());
615 Gfx::DisjointIntRectSet opaque_rects;
616 opaque_rects.add_many(constrained_render_rect_to_screen(rect()).shatter(m_window.rect()));
617 return opaque_rects;
618}
619
620Gfx::DisjointIntRectSet WindowFrame::transparent_render_rects() const
621{
622 auto border_radius = WindowManager::the().palette().window_border_radius();
623 if (has_alpha_channel() || border_radius > 0) {
624 if (m_window.is_opaque()) {
625 Gfx::DisjointIntRectSet transparent_rects;
626 transparent_rects.add_many(render_rect().shatter(m_window.rect()));
627 return transparent_rects;
628 }
629 return render_rect();
630 }
631
632 auto total_render_rect = render_rect();
633 Gfx::DisjointIntRectSet transparent_rects;
634 if (has_shadow())
635 transparent_rects.add_many(total_render_rect.shatter(rect()));
636 if (!m_window.is_opaque())
637 transparent_rects.add(m_window.rect().intersected(total_render_rect));
638 return transparent_rects;
639}
640
641void WindowFrame::invalidate_titlebar()
642{
643 set_dirty();
644 invalidate(titlebar_rect());
645}
646
647void WindowFrame::invalidate()
648{
649 auto frame_rect = render_rect();
650 invalidate(Gfx::IntRect { frame_rect.location() - m_window.position(), frame_rect.size() });
651 m_window.invalidate(true, true);
652}
653
654void WindowFrame::invalidate_menubar()
655{
656 invalidate(menubar_rect());
657}
658
659void WindowFrame::invalidate(Gfx::IntRect relative_rect)
660{
661 auto frame_rect = rect();
662 auto window_rect = m_window.rect();
663 relative_rect.translate_by(frame_rect.x() - window_rect.x(), frame_rect.y() - window_rect.y());
664 set_dirty();
665 m_window.invalidate(relative_rect, true);
666}
667
668void WindowFrame::window_rect_changed(Gfx::IntRect const& old_rect, Gfx::IntRect const& new_rect)
669{
670 layout_buttons();
671
672 set_dirty(true);
673 WindowManager::the().notify_rect_changed(m_window, old_rect, new_rect);
674}
675
676void WindowFrame::layout_buttons()
677{
678 auto button_rects = Gfx::WindowTheme::current().layout_buttons(to_theme_window_type(m_window.type()), to_theme_window_mode(m_window.mode()), m_window.rect(), WindowManager::the().palette(), m_buttons.size());
679 for (size_t i = 0; i < m_buttons.size(); i++)
680 m_buttons[i]->set_relative_rect(button_rects[i]);
681}
682
683Optional<HitTestResult> WindowFrame::hit_test(Gfx::IntPoint position)
684{
685 if (m_window.is_frameless() || m_window.is_fullscreen())
686 return {};
687 if (!constrained_render_rect_to_screen(rect()).contains(position)) {
688 // Checking just frame_rect is not enough. If we constrain rendering
689 // a window to one screen (e.g. when it's maximized or tiled) so that
690 // the frame doesn't bleed into the adjacent screen(s), then we need
691 // to also check that we're within these bounds.
692 return {};
693 }
694 auto window_rect = m_window.rect();
695 if (window_rect.contains(position))
696 return {};
697
698 auto* screen = Screen::find_by_location(position);
699 if (!screen)
700 return {};
701 auto* cached = render_to_cache(*screen);
702 if (!cached)
703 return {};
704
705 auto window_relative_position = position.translated(-unconstrained_render_rect().location());
706 return cached->hit_test(*this, position, window_relative_position);
707}
708
709Optional<HitTestResult> WindowFrame::PerScaleRenderedCache::hit_test(WindowFrame& frame, Gfx::IntPoint position, Gfx::IntPoint window_relative_position)
710{
711 HitTestResult result {
712 .window = frame.window(),
713 .screen_position = position,
714 .window_relative_position = window_relative_position,
715 .is_frame_hit = true,
716 };
717
718 u8 alpha_threshold = Gfx::WindowTheme::current().frame_alpha_hit_threshold(frame.window_state_for_theme()) * 255;
719 if (alpha_threshold == 0)
720 return result;
721 u8 alpha = 0xff;
722
723 auto window_rect = frame.window().rect();
724 if (position.y() < window_rect.y()) {
725 if (m_top_bottom) {
726 auto scaled_relative_point = window_relative_position * m_top_bottom->scale();
727 if (m_top_bottom->rect().contains(scaled_relative_point))
728 alpha = m_top_bottom->get_pixel(scaled_relative_point).alpha();
729 }
730 } else if (position.y() > window_rect.bottom()) {
731 if (m_top_bottom) {
732 Gfx::IntPoint scaled_relative_point { window_relative_position.x() * m_top_bottom->scale(), m_bottom_y * m_top_bottom->scale() + position.y() - window_rect.bottom() - 1 };
733 if (m_top_bottom->rect().contains(scaled_relative_point))
734 alpha = m_top_bottom->get_pixel(scaled_relative_point).alpha();
735 }
736 } else if (position.x() < window_rect.x()) {
737 if (m_left_right) {
738 Gfx::IntPoint scaled_relative_point { window_relative_position.x() * m_left_right->scale(), (window_relative_position.y() - m_bottom_y) * m_left_right->scale() };
739 if (m_left_right->rect().contains(scaled_relative_point))
740 alpha = m_left_right->get_pixel(scaled_relative_point).alpha();
741 }
742 } else if (position.x() > window_rect.right()) {
743 if (m_left_right) {
744 Gfx::IntPoint scaled_relative_point { m_right_x * m_left_right->scale() + position.x() - window_rect.right() - 1, (window_relative_position.y() - m_bottom_y) * m_left_right->scale() };
745 if (m_left_right->rect().contains(scaled_relative_point))
746 alpha = m_left_right->get_pixel(scaled_relative_point).alpha();
747 }
748 } else {
749 return {};
750 }
751 if (alpha >= alpha_threshold)
752 return result;
753 return {};
754}
755
756bool WindowFrame::handle_titlebar_icon_mouse_event(MouseEvent const& event)
757{
758 auto& wm = WindowManager::the();
759
760 if (event.type() == Event::MouseDown && (event.button() == MouseButton::Primary || event.button() == MouseButton::Secondary)) {
761 // Manually start a potential double click. Since we're opening
762 // a menu, we will only receive the MouseDown event, so we
763 // need to record that fact. If the user subsequently clicks
764 // on the same area, the menu will get closed, and we will
765 // receive a MouseUp event, but because windows have changed
766 // we don't get a MouseDoubleClick event. We can however record
767 // this click, and when we receive the MouseUp event check if
768 // it would have been considered a double click, if it weren't
769 // for the fact that we opened and closed a window in the meanwhile
770 wm.system_menu_doubleclick(m_window, event);
771
772 m_window.popup_window_menu(titlebar_rect().bottom_left().translated(rect().location()), WindowMenuDefaultAction::Close);
773 return true;
774 } else if (event.type() == Event::MouseUp && event.button() == MouseButton::Primary) {
775 // Since the MouseDown event opened a menu, another MouseUp
776 // from the second click outside the menu wouldn't be considered
777 // a double click, so let's manually check if it would otherwise
778 // have been be considered to be one
779 if (wm.is_menu_doubleclick(m_window, event)) {
780 // It is a double click, so perform activate the default item
781 m_window.window_menu_activate_default();
782 }
783 return true;
784 }
785 return false;
786}
787
788void WindowFrame::handle_titlebar_mouse_event(MouseEvent const& event)
789{
790 auto& wm = WindowManager::the();
791
792 if (titlebar_icon_rect().contains(event.position())) {
793 if (handle_titlebar_icon_mouse_event(event))
794 return;
795 }
796
797 for (auto& button : m_buttons) {
798 if (button->relative_rect().contains(event.position()))
799 return button->on_mouse_event(event.translated(-button->relative_rect().location()));
800 }
801
802 if (event.type() == Event::MouseDown) {
803 if (m_window.type() == WindowType::Normal && event.button() == MouseButton::Secondary) {
804 auto default_action = m_window.is_maximized() ? WindowMenuDefaultAction::Restore : WindowMenuDefaultAction::Maximize;
805 m_window.popup_window_menu(event.position().translated(rect().location()), default_action);
806 return;
807 }
808 if (m_window.is_movable() && event.button() == MouseButton::Primary)
809 wm.start_window_move(m_window, event.translated(rect().location()));
810 }
811}
812
813void WindowFrame::handle_mouse_event(MouseEvent const& event)
814{
815 VERIFY(!m_window.is_fullscreen());
816
817 if (m_window.type() != WindowType::Normal && m_window.type() != WindowType::Notification)
818 return;
819
820 auto& wm = WindowManager::the();
821 if (m_window.type() == WindowType::Normal) {
822 if (event.type() == Event::MouseDown)
823 wm.move_to_front_and_make_active(m_window);
824 }
825
826 if (m_window.blocking_modal_window())
827 return;
828
829 // This is slightly hackish, but expand the title bar rect by two pixels downwards,
830 // so that mouse events between the title bar and window contents don't act like
831 // mouse events on the border.
832 auto adjusted_titlebar_rect = titlebar_rect();
833 adjusted_titlebar_rect.set_height(adjusted_titlebar_rect.height() + 2);
834
835 if (adjusted_titlebar_rect.contains(event.position())) {
836 handle_titlebar_mouse_event(event);
837 return;
838 }
839
840 if (menubar_rect().contains(event.position())) {
841 handle_menubar_mouse_event(event);
842 return;
843 }
844
845 handle_border_mouse_event(event);
846}
847
848void WindowFrame::handle_border_mouse_event(MouseEvent const& event)
849{
850 if (!m_window.is_resizable())
851 return;
852
853 auto& wm = WindowManager::the();
854
855 constexpr ResizeDirection direction_for_hot_area[3][3] = {
856 { ResizeDirection::UpLeft, ResizeDirection::Up, ResizeDirection::UpRight },
857 { ResizeDirection::Left, ResizeDirection::None, ResizeDirection::Right },
858 { ResizeDirection::DownLeft, ResizeDirection::Down, ResizeDirection::DownRight },
859 };
860 Gfx::IntRect outer_rect = { {}, rect().size() };
861 VERIFY(outer_rect.contains(event.position()));
862 int window_relative_x = event.x() - outer_rect.x();
863 int window_relative_y = event.y() - outer_rect.y();
864 int corner_size = titlebar_rect().height();
865 int hot_area_row = (window_relative_y < corner_size) ? 0 : (window_relative_y > outer_rect.height() - corner_size) ? 2
866 : 1;
867 int hot_area_column = (window_relative_x < corner_size) ? 0 : (window_relative_x > outer_rect.width() - corner_size) ? 2
868 : 1;
869 ResizeDirection resize_direction = direction_for_hot_area[hot_area_row][hot_area_column];
870
871 // Double click latches a window's edge to the screen's edge
872 if (event.type() == Event::MouseDoubleClick) {
873 latch_window_to_screen_edge(resize_direction);
874 return;
875 }
876
877 if (event.type() == Event::MouseMove && event.buttons() == 0) {
878 wm.set_resize_candidate(m_window, resize_direction);
879 Compositor::the().invalidate_cursor();
880 return;
881 }
882
883 if (event.type() == Event::MouseDown && event.button() == MouseButton::Primary)
884 wm.start_window_resize(m_window, event.translated(rect().location()), resize_direction);
885}
886
887void WindowFrame::handle_menubar_mouse_event(MouseEvent const& event)
888{
889 Menu* hovered_menu = nullptr;
890 auto menubar_rect = this->menubar_rect();
891 auto adjusted_position = event.position().translated(-menubar_rect.location());
892 m_window.menubar().for_each_menu([&](Menu& menu) {
893 if (menu.rect_in_window_menubar().contains(adjusted_position)) {
894 hovered_menu = &menu;
895 handle_menu_mouse_event(menu, event);
896 return IterationDecision::Break;
897 }
898 return IterationDecision::Continue;
899 });
900 if (!hovered_menu && event.type() == Event::Type::MouseDown)
901 MenuManager::the().close_everyone();
902 if (hovered_menu != MenuManager::the().hovered_menu()) {
903 MenuManager::the().set_hovered_menu(hovered_menu);
904 invalidate(menubar_rect);
905 }
906}
907
908void WindowFrame::open_menubar_menu(Menu& menu)
909{
910 auto menubar_rect = this->menubar_rect();
911 MenuManager::the().close_everyone();
912 auto position = menu.rect_in_window_menubar().bottom_left().translated(rect().location()).translated(menubar_rect.location());
913 menu.set_unadjusted_position(position);
914 auto& window = menu.ensure_menu_window(position);
915 auto window_rect = window.rect();
916 auto& screen = Screen::closest_to_rect(window_rect);
917 auto window_border_thickness = 1;
918
919 // If the menu is off the right edge of the screen align its right edge with the edge of the screen.
920 if (window_rect.right() > screen.width()) {
921 position = position.translated(((window_rect.right() - screen.width()) * -1) - window_border_thickness, 0);
922 }
923 // If the menu is below the bottom of the screen move it to appear above the menubar.
924 if (window_rect.bottom() > screen.height()) {
925 position = position.translated(0, (window_rect.height() * -1) - menubar_rect.height());
926 }
927
928 window.set_rect(position.x(), position.y(), window_rect.width(), window_rect.height());
929
930 MenuManager::the().open_menu(menu);
931 WindowManager::the().set_window_with_active_menu(&m_window);
932 invalidate(menubar_rect);
933}
934
935void WindowFrame::handle_menu_mouse_event(Menu& menu, MouseEvent const& event)
936{
937 auto menubar_rect = this->menubar_rect();
938 bool is_hover_with_any_menu_open = event.type() == MouseEvent::MouseMove && &m_window == WindowManager::the().window_with_active_menu();
939 bool is_mousedown_with_left_button = event.type() == MouseEvent::MouseDown && event.button() == MouseButton::Primary;
940 bool should_open_menu = &menu != MenuManager::the().current_menu() && (is_hover_with_any_menu_open || is_mousedown_with_left_button);
941 bool should_close_menu = &menu == MenuManager::the().current_menu() && is_mousedown_with_left_button;
942
943 if (should_open_menu) {
944 open_menubar_menu(menu);
945 return;
946 }
947
948 if (should_close_menu) {
949 invalidate(menubar_rect);
950 MenuManager::the().close_everyone();
951 }
952}
953
954void WindowFrame::start_flash_animation()
955{
956 if (!m_flash_timer) {
957 m_flash_timer = Core::Timer::create_repeating(100, [this] {
958 VERIFY(m_flash_counter);
959 invalidate_titlebar();
960 if (!--m_flash_counter)
961 m_flash_timer->stop();
962 }).release_value_but_fixme_should_propagate_errors();
963 }
964 m_flash_counter = 8;
965 m_flash_timer->start();
966}
967
968int WindowFrame::menu_row_count() const
969{
970 if (!m_window.should_show_menubar())
971 return 0;
972 return m_window.menubar().has_menus() ? 1 : 0;
973}
974
975void WindowFrame::latch_window_to_screen_edge(ResizeDirection resize_direction)
976{
977 auto window_rect = m_window.rect();
978 auto frame_rect = rect();
979 auto& screen = Screen::closest_to_rect(window_rect);
980 auto screen_rect = WindowManager::the().desktop_rect(screen);
981
982 if (resize_direction == ResizeDirection::UpLeft
983 || resize_direction == ResizeDirection::Up
984 || resize_direction == ResizeDirection::UpRight)
985 window_rect.inflate(frame_rect.top() - screen_rect.top(), 0, 0, 0);
986
987 if (resize_direction == ResizeDirection::UpRight
988 || resize_direction == ResizeDirection::Right
989 || resize_direction == ResizeDirection::DownRight)
990 window_rect.inflate(0, screen_rect.right() - frame_rect.right(), 0, 0);
991
992 if (resize_direction == ResizeDirection::DownLeft
993 || resize_direction == ResizeDirection::Down
994 || resize_direction == ResizeDirection::DownRight)
995 window_rect.inflate(0, 0, screen_rect.bottom() - frame_rect.bottom(), 0);
996
997 if (resize_direction == ResizeDirection::UpLeft
998 || resize_direction == ResizeDirection::Left
999 || resize_direction == ResizeDirection::DownLeft)
1000 window_rect.inflate(0, 0, 0, frame_rect.left() - screen_rect.left());
1001
1002 m_window.set_rect(window_rect);
1003}
1004
1005}