ladybird/Userland/Services/WindowServer/WindowFrame.cpp
2021-11-11 01:27:46 +01:00

892 lines
35 KiB
C++

/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include "ClientConnection.h"
#include <AK/Badge.h>
#include <LibGfx/Font.h>
#include <LibGfx/Painter.h>
#include <LibGfx/StylePainter.h>
#include <LibGfx/WindowTheme.h>
#include <WindowServer/Button.h>
#include <WindowServer/Compositor.h>
#include <WindowServer/Event.h>
#include <WindowServer/MultiScaleBitmaps.h>
#include <WindowServer/Screen.h>
#include <WindowServer/Window.h>
#include <WindowServer/WindowFrame.h>
#include <WindowServer/WindowManager.h>
namespace WindowServer {
static Gfx::WindowTheme::WindowType to_theme_window_type(WindowType type)
{
switch (type) {
case WindowType::Normal:
return Gfx::WindowTheme::WindowType::Normal;
case WindowType::ToolWindow:
return Gfx::WindowTheme::WindowType::ToolWindow;
case WindowType::Notification:
return Gfx::WindowTheme::WindowType::Notification;
default:
return Gfx::WindowTheme::WindowType::Other;
}
}
static RefPtr<MultiScaleBitmaps> s_minimize_icon;
static RefPtr<MultiScaleBitmaps> s_maximize_icon;
static RefPtr<MultiScaleBitmaps> s_restore_icon;
static RefPtr<MultiScaleBitmaps> s_close_icon;
static RefPtr<MultiScaleBitmaps> s_close_modified_icon;
static RefPtr<MultiScaleBitmaps> s_active_window_shadow;
static RefPtr<MultiScaleBitmaps> s_inactive_window_shadow;
static RefPtr<MultiScaleBitmaps> s_menu_shadow;
static RefPtr<MultiScaleBitmaps> s_taskbar_shadow;
static RefPtr<MultiScaleBitmaps> s_tooltip_shadow;
static String s_last_active_window_shadow_path;
static String s_last_inactive_window_shadow_path;
static String s_last_menu_shadow_path;
static String s_last_taskbar_shadow_path;
static String s_last_tooltip_shadow_path;
static Gfx::IntRect frame_rect_for_window(Window& window, const Gfx::IntRect& rect)
{
if (window.is_frameless())
return rect;
int menu_row_count = (window.menubar().has_menus() && window.should_show_menubar()) ? 1 : 0;
return Gfx::WindowTheme::current().frame_rect_for_window(to_theme_window_type(window.type()), rect, WindowManager::the().palette(), menu_row_count);
}
WindowFrame::WindowFrame(Window& window)
: m_window(window)
{
// Because Window constructs a WindowFrame during its construction, we need
// to be careful and defer doing initialization that assumes a fully
// constructed Window. It is fully constructed when Window notifies us with
// a call to WindowFrame::window_was_constructed.
}
void WindowFrame::window_was_constructed(Badge<Window>)
{
if (m_window.is_closeable()) {
auto button = make<Button>(*this, [this](auto&) {
m_window.handle_window_menu_action(WindowMenuAction::Close);
});
m_close_button = button.ptr();
m_buttons.append(move(button));
}
if (m_window.is_resizable()) {
auto button = make<Button>(*this, [this](auto&) {
m_window.handle_window_menu_action(WindowMenuAction::MaximizeOrRestore);
});
button->on_middle_click = [&](auto&) {
m_window.set_vertically_maximized();
};
m_maximize_button = button.ptr();
m_buttons.append(move(button));
}
if (m_window.is_minimizable()) {
auto button = make<Button>(*this, [this](auto&) {
m_window.handle_window_menu_action(WindowMenuAction::MinimizeOrUnminimize);
});
m_minimize_button = button.ptr();
m_buttons.append(move(button));
}
set_button_icons();
m_has_alpha_channel = Gfx::WindowTheme::current().frame_uses_alpha(window_state_for_theme(), WindowManager::the().palette());
}
WindowFrame::~WindowFrame()
{
}
void WindowFrame::set_button_icons()
{
set_dirty();
if (m_window.is_frameless())
return;
if (m_window.is_closeable())
m_close_button->set_icon(m_window.is_modified() ? *s_close_modified_icon : *s_close_icon);
if (m_window.is_minimizable())
m_minimize_button->set_icon(s_minimize_icon);
if (m_window.is_resizable())
m_maximize_button->set_icon(m_window.is_maximized() ? *s_restore_icon : *s_maximize_icon);
}
void WindowFrame::reload_config()
{
String icons_path = WindowManager::the().palette().title_button_icons_path();
auto reload_icon = [&](RefPtr<MultiScaleBitmaps>& icon, StringView path, StringView default_path) {
StringBuilder full_path;
full_path.append(icons_path);
full_path.append(path);
if (icon)
icon->load(full_path.to_string(), default_path);
else
icon = MultiScaleBitmaps::create(full_path.to_string(), default_path);
};
reload_icon(s_minimize_icon, "window-minimize.png", "/res/icons/16x16/downward-triangle.png");
reload_icon(s_maximize_icon, "window-maximize.png", "/res/icons/16x16/upward-triangle.png");
reload_icon(s_restore_icon, "window-restore.png", "/res/icons/16x16/window-restore.png");
reload_icon(s_close_icon, "window-close.png", "/res/icons/16x16/window-close.png");
reload_icon(s_close_modified_icon, "window-close-modified.png", "/res/icons/16x16/window-close-modified.png");
auto load_shadow = [](const String& path, String& last_path, RefPtr<MultiScaleBitmaps>& shadow_bitmap) {
if (path.is_empty()) {
last_path = String::empty();
shadow_bitmap = nullptr;
} else if (!shadow_bitmap || last_path != path) {
if (shadow_bitmap)
shadow_bitmap->load(path);
else
shadow_bitmap = MultiScaleBitmaps::create(path);
if (shadow_bitmap)
last_path = path;
else
last_path = String::empty();
}
};
load_shadow(WindowManager::the().palette().active_window_shadow_path(), s_last_active_window_shadow_path, s_active_window_shadow);
load_shadow(WindowManager::the().palette().inactive_window_shadow_path(), s_last_inactive_window_shadow_path, s_inactive_window_shadow);
load_shadow(WindowManager::the().palette().menu_shadow_path(), s_last_menu_shadow_path, s_menu_shadow);
load_shadow(WindowManager::the().palette().taskbar_shadow_path(), s_last_taskbar_shadow_path, s_taskbar_shadow);
load_shadow(WindowManager::the().palette().tooltip_shadow_path(), s_last_tooltip_shadow_path, s_tooltip_shadow);
}
MultiScaleBitmaps* WindowFrame::shadow_bitmap() const
{
if (m_window.is_frameless() && !m_window.has_forced_shadow())
return nullptr;
switch (m_window.type()) {
case WindowType::Desktop:
return nullptr;
case WindowType::Menu:
return s_menu_shadow;
case WindowType::Tooltip:
return s_tooltip_shadow;
case WindowType::Taskbar:
return s_taskbar_shadow;
case WindowType::AppletArea:
return nullptr;
default:
if (auto* highlight_window = WindowManager::the().highlight_window())
return highlight_window == &m_window ? s_active_window_shadow : s_inactive_window_shadow;
return m_window.is_active() ? s_active_window_shadow : s_inactive_window_shadow;
}
}
bool WindowFrame::has_shadow() const
{
if (auto* shadow_bitmap = this->shadow_bitmap(); shadow_bitmap && shadow_bitmap->format() == Gfx::BitmapFormat::BGRA8888)
return true;
return false;
}
void WindowFrame::did_set_maximized(Badge<Window>, bool maximized)
{
VERIFY(m_maximize_button);
m_maximize_button->set_icon(maximized ? *s_restore_icon : *s_maximize_icon);
}
Gfx::IntRect WindowFrame::menubar_rect() const
{
if (!m_window.menubar().has_menus() || !m_window.should_show_menubar())
return {};
return Gfx::WindowTheme::current().menubar_rect(to_theme_window_type(m_window.type()), m_window.rect(), WindowManager::the().palette(), menu_row_count());
}
Gfx::IntRect WindowFrame::titlebar_rect() const
{
return Gfx::WindowTheme::current().titlebar_rect(to_theme_window_type(m_window.type()), m_window.rect(), WindowManager::the().palette());
}
Gfx::IntRect WindowFrame::titlebar_icon_rect() const
{
return Gfx::WindowTheme::current().titlebar_icon_rect(to_theme_window_type(m_window.type()), m_window.rect(), WindowManager::the().palette());
}
Gfx::IntRect WindowFrame::titlebar_text_rect() const
{
return Gfx::WindowTheme::current().titlebar_text_rect(to_theme_window_type(m_window.type()), m_window.rect(), WindowManager::the().palette());
}
Gfx::WindowTheme::WindowState WindowFrame::window_state_for_theme() const
{
auto& wm = WindowManager::the();
if (m_flash_timer && m_flash_timer->is_active())
return m_flash_counter & 1 ? Gfx::WindowTheme::WindowState::Active : Gfx::WindowTheme::WindowState::Inactive;
if (&m_window == wm.highlight_window())
return Gfx::WindowTheme::WindowState::Highlighted;
if (&m_window == wm.m_move_window)
return Gfx::WindowTheme::WindowState::Moving;
if (wm.is_active_window_or_accessory(m_window))
return Gfx::WindowTheme::WindowState::Active;
return Gfx::WindowTheme::WindowState::Inactive;
}
void WindowFrame::paint_notification_frame(Gfx::Painter& painter)
{
auto palette = WindowManager::the().palette();
Gfx::WindowTheme::current().paint_notification_frame(painter, m_window.rect(), palette, m_buttons.last().relative_rect());
}
void WindowFrame::paint_tool_window_frame(Gfx::Painter& painter)
{
auto palette = WindowManager::the().palette();
Gfx::WindowTheme::current().paint_tool_window_frame(painter, window_state_for_theme(), m_window.rect(), m_window.computed_title(), palette, leftmost_titlebar_button_rect());
}
void WindowFrame::paint_menubar(Gfx::Painter& painter)
{
auto& wm = WindowManager::the();
auto& font = wm.font();
auto palette = wm.palette();
auto menubar_rect = this->menubar_rect();
painter.fill_rect(menubar_rect, palette.window());
Gfx::PainterStateSaver saver(painter);
painter.add_clip_rect(menubar_rect);
painter.translate(menubar_rect.location());
m_window.menubar().for_each_menu([&](Menu& menu) {
auto text_rect = menu.rect_in_window_menubar();
Color text_color = palette.window_text();
auto is_open = menu.is_open();
if (is_open)
text_rect.translate_by(1, 1);
bool paint_as_pressed = is_open;
bool paint_as_hovered = !paint_as_pressed && &menu == MenuManager::the().hovered_menu();
if (paint_as_pressed || paint_as_hovered) {
Gfx::StylePainter::paint_button(painter, menu.rect_in_window_menubar(), palette, Gfx::ButtonStyle::Coolbar, paint_as_pressed, paint_as_hovered);
}
painter.draw_ui_text(text_rect, menu.name(), font, Gfx::TextAlignment::Center, text_color);
return IterationDecision::Continue;
});
}
void WindowFrame::paint_normal_frame(Gfx::Painter& painter)
{
auto palette = WindowManager::the().palette();
Gfx::WindowTheme::current().paint_normal_frame(painter, window_state_for_theme(), m_window.rect(), m_window.computed_title(), m_window.icon(), palette, leftmost_titlebar_button_rect(), menu_row_count(), m_window.is_modified());
if (m_window.menubar().has_menus() && m_window.should_show_menubar())
paint_menubar(painter);
}
void WindowFrame::paint(Screen& screen, Gfx::Painter& painter, const Gfx::IntRect& rect)
{
if (auto* cached = render_to_cache(screen))
cached->paint(*this, painter, rect);
}
void WindowFrame::PerScaleRenderedCache::paint(WindowFrame& frame, Gfx::Painter& painter, const Gfx::IntRect& rect)
{
auto frame_rect = frame.unconstrained_render_rect();
auto window_rect = frame.window().rect();
if (m_top_bottom) {
auto top_bottom_height = frame_rect.height() - window_rect.height();
if (m_bottom_y > 0) {
// We have a top piece
auto src_rect = rect.intersected({ frame_rect.location(), { frame_rect.width(), m_bottom_y } });
if (!src_rect.is_empty())
painter.blit(src_rect.location(), *m_top_bottom, src_rect.translated(-frame_rect.location()), frame.opacity());
}
if (m_bottom_y < top_bottom_height) {
// We have a bottom piece
Gfx::IntRect rect_in_frame { frame_rect.x(), window_rect.bottom() + 1, frame_rect.width(), top_bottom_height - m_bottom_y };
auto src_rect = rect.intersected(rect_in_frame);
if (!src_rect.is_empty())
painter.blit(src_rect.location(), *m_top_bottom, src_rect.translated(-rect_in_frame.x(), -rect_in_frame.y() + m_bottom_y), frame.opacity());
}
}
if (m_left_right) {
auto left_right_width = frame_rect.width() - window_rect.width();
if (m_right_x > 0) {
// We have a left piece
Gfx::IntRect rect_in_frame { frame_rect.x(), window_rect.y(), m_right_x, window_rect.height() };
auto src_rect = rect.intersected(rect_in_frame);
if (!src_rect.is_empty())
painter.blit(src_rect.location(), *m_left_right, src_rect.translated(-rect_in_frame.location()), frame.opacity());
}
if (m_right_x < left_right_width) {
// We have a right piece
Gfx::IntRect rect_in_frame { window_rect.right() + 1, window_rect.y(), left_right_width - m_right_x, window_rect.height() };
auto src_rect = rect.intersected(rect_in_frame);
if (!src_rect.is_empty())
painter.blit(src_rect.location(), *m_left_right, src_rect.translated(-rect_in_frame.x() + m_right_x, -rect_in_frame.y()), frame.opacity());
}
}
}
void WindowFrame::render(Screen& screen, Gfx::Painter& painter)
{
if (m_window.is_frameless())
return;
if (m_window.type() == WindowType::Notification)
paint_notification_frame(painter);
else if (m_window.type() == WindowType::Normal)
paint_normal_frame(painter);
else if (m_window.type() == WindowType::ToolWindow)
paint_tool_window_frame(painter);
else
return;
for (auto& button : m_buttons)
button.paint(screen, painter);
}
void WindowFrame::theme_changed()
{
m_rendered_cache = {};
layout_buttons();
set_button_icons();
m_has_alpha_channel = Gfx::WindowTheme::current().frame_uses_alpha(window_state_for_theme(), WindowManager::the().palette());
}
auto WindowFrame::render_to_cache(Screen& screen) -> PerScaleRenderedCache*
{
auto scale = screen.scale_factor();
PerScaleRenderedCache* rendered_cache;
auto cached_it = m_rendered_cache.find(scale);
if (cached_it == m_rendered_cache.end()) {
auto new_rendered_cache = make<PerScaleRenderedCache>();
rendered_cache = new_rendered_cache.ptr();
m_rendered_cache.set(scale, move(new_rendered_cache));
} else {
rendered_cache = cached_it->value.ptr();
}
rendered_cache->render(*this, screen);
return rendered_cache;
}
void WindowFrame::PerScaleRenderedCache::render(WindowFrame& frame, Screen& screen)
{
if (!m_dirty)
return;
m_dirty = false;
auto scale = screen.scale_factor();
auto frame_rect = frame.rect();
auto frame_rect_including_shadow = frame_rect;
auto* shadow_bitmap = frame.shadow_bitmap();
Gfx::IntPoint shadow_offset;
if (shadow_bitmap) {
auto total_shadow_size = shadow_bitmap->bitmap(screen.scale_factor()).height();
frame_rect_including_shadow.inflate(total_shadow_size, total_shadow_size);
auto offset = total_shadow_size / 2;
shadow_offset = { offset, offset };
}
auto window_rect = frame.window().rect();
// TODO: if we stop using a scaling factor we should clear cached bitmaps from this map
static HashMap<int, RefPtr<Gfx::Bitmap>> s_tmp_bitmap_cache;
Gfx::Bitmap* tmp_bitmap;
{
auto tmp_it = s_tmp_bitmap_cache.find(scale);
if (tmp_it == s_tmp_bitmap_cache.end() || !tmp_it->value->size().contains(frame_rect_including_shadow.size())) {
// Explicitly clear the old bitmap first so this works on machines with very little memory
if (tmp_it != s_tmp_bitmap_cache.end())
tmp_it->value = nullptr;
auto bitmap_or_error = Gfx::Bitmap::try_create(Gfx::BitmapFormat::BGRA8888, frame_rect_including_shadow.size(), scale);
if (bitmap_or_error.is_error()) {
s_tmp_bitmap_cache.remove(scale);
dbgln("Could not create bitmap of size {}: {}", frame_rect_including_shadow.size(), bitmap_or_error.error());
return;
}
auto bitmap = bitmap_or_error.release_value();
tmp_bitmap = bitmap.ptr();
if (tmp_it != s_tmp_bitmap_cache.end())
tmp_it->value = move(bitmap);
else
s_tmp_bitmap_cache.set(scale, move(bitmap));
} else {
tmp_bitmap = tmp_it->value.ptr();
}
}
VERIFY(tmp_bitmap);
auto top_bottom_height = frame_rect_including_shadow.height() - window_rect.height();
auto left_right_width = frame_rect_including_shadow.width() - window_rect.width();
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) {
if (top_bottom_height > 0)
m_top_bottom = Gfx::Bitmap::try_create(Gfx::BitmapFormat::BGRA8888, { frame_rect_including_shadow.width(), top_bottom_height }, scale).release_value_but_fixme_should_propagate_errors();
else
m_top_bottom = nullptr;
m_shadow_dirty = true;
}
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) {
if (left_right_width > 0)
m_left_right = Gfx::Bitmap::try_create(Gfx::BitmapFormat::BGRA8888, { left_right_width, frame_rect_including_shadow.height() }, scale).release_value_but_fixme_should_propagate_errors();
else
m_left_right = nullptr;
m_shadow_dirty = true;
}
auto& frame_rect_to_update = m_shadow_dirty ? frame_rect_including_shadow : frame_rect;
Gfx::IntPoint update_location(m_shadow_dirty ? Gfx::IntPoint { 0, 0 } : shadow_offset);
Gfx::Painter painter(*tmp_bitmap);
// Clear the frame area, not including the window content area, which we don't care about
for (auto& rect : frame_rect_to_update.shatter(window_rect))
painter.clear_rect({ rect.location() - frame_rect_to_update.location(), rect.size() }, { 255, 255, 255, 0 });
if (m_shadow_dirty && shadow_bitmap)
Gfx::StylePainter::paint_simple_rect_shadow(painter, { { 0, 0 }, frame_rect_including_shadow.size() }, shadow_bitmap->bitmap(screen.scale_factor()));
{
Gfx::PainterStateSaver save(painter);
painter.translate(shadow_offset);
frame.render(screen, painter);
}
if (m_top_bottom && top_bottom_height > 0) {
m_bottom_y = window_rect.y() - frame_rect_including_shadow.y();
VERIFY(m_bottom_y >= 0);
Gfx::Painter top_bottom_painter(*m_top_bottom);
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()) } });
if (m_bottom_y > 0)
top_bottom_painter.blit({ 0, 0 }, *tmp_bitmap, { 0, 0, frame_rect_including_shadow.width(), m_bottom_y }, 1.0, false);
if (m_bottom_y < top_bottom_height)
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);
} else {
m_bottom_y = 0;
}
if (left_right_width > 0) {
m_right_x = window_rect.x() - frame_rect_including_shadow.x();
VERIFY(m_right_x >= 0);
Gfx::Painter left_right_painter(*m_left_right);
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() } });
if (m_right_x > 0)
left_right_painter.blit({ 0, 0 }, *tmp_bitmap, { 0, m_bottom_y, m_right_x, window_rect.height() }, 1.0, false);
if (m_right_x < left_right_width)
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);
} else {
m_right_x = 0;
}
m_shadow_dirty = false;
}
void WindowFrame::set_opacity(float opacity)
{
if (m_opacity == opacity)
return;
bool was_opaque = is_opaque();
m_opacity = opacity;
if (was_opaque != is_opaque())
Compositor::the().invalidate_occlusions();
Compositor::the().invalidate_screen(render_rect());
WindowManager::the().notify_opacity_changed(m_window);
}
Gfx::IntRect WindowFrame::inflated_for_shadow(const Gfx::IntRect& frame_rect) const
{
if (auto* shadow = shadow_bitmap()) {
auto total_shadow_size = shadow->default_bitmap().height();
return frame_rect.inflated(total_shadow_size, total_shadow_size);
}
return frame_rect;
}
Gfx::IntRect WindowFrame::rect() const
{
return frame_rect_for_window(m_window, m_window.rect());
}
Gfx::IntRect WindowFrame::constrained_render_rect_to_screen(const Gfx::IntRect& render_rect) const
{
if (m_window.is_maximized() || m_window.tiled() != WindowTileType::None)
return render_rect.intersected(Screen::closest_to_rect(rect()).rect());
return render_rect;
}
Gfx::IntRect WindowFrame::leftmost_titlebar_button_rect() const
{
if (!m_buttons.is_empty())
return m_buttons.last().relative_rect();
auto rect = titlebar_rect();
rect.translate_by(rect.width(), 0);
return rect;
}
Gfx::IntRect WindowFrame::render_rect() const
{
return constrained_render_rect_to_screen(inflated_for_shadow(rect()));
}
Gfx::IntRect WindowFrame::unconstrained_render_rect() const
{
return inflated_for_shadow(rect());
}
Gfx::DisjointRectSet WindowFrame::opaque_render_rects() const
{
if (has_alpha_channel()) {
if (m_window.is_opaque())
return constrained_render_rect_to_screen(m_window.rect());
return {};
}
if (m_window.is_opaque())
return constrained_render_rect_to_screen(rect());
Gfx::DisjointRectSet opaque_rects;
opaque_rects.add_many(constrained_render_rect_to_screen(rect()).shatter(m_window.rect()));
return opaque_rects;
}
Gfx::DisjointRectSet WindowFrame::transparent_render_rects() const
{
if (has_alpha_channel()) {
if (m_window.is_opaque()) {
Gfx::DisjointRectSet transparent_rects;
transparent_rects.add_many(render_rect().shatter(m_window.rect()));
return transparent_rects;
}
return render_rect();
}
auto total_render_rect = render_rect();
Gfx::DisjointRectSet transparent_rects;
if (has_shadow())
transparent_rects.add_many(total_render_rect.shatter(rect()));
if (!m_window.is_opaque())
transparent_rects.add(m_window.rect().intersected(total_render_rect));
return transparent_rects;
}
void WindowFrame::invalidate_titlebar()
{
set_dirty();
invalidate(titlebar_rect());
}
void WindowFrame::invalidate()
{
auto frame_rect = render_rect();
invalidate(Gfx::IntRect { frame_rect.location() - m_window.position(), frame_rect.size() });
m_window.invalidate(true, true);
}
void WindowFrame::invalidate_menubar()
{
invalidate(menubar_rect());
}
void WindowFrame::invalidate(Gfx::IntRect relative_rect)
{
auto frame_rect = rect();
auto window_rect = m_window.rect();
relative_rect.translate_by(frame_rect.x() - window_rect.x(), frame_rect.y() - window_rect.y());
set_dirty();
m_window.invalidate(relative_rect);
}
void WindowFrame::window_rect_changed(const Gfx::IntRect& old_rect, const Gfx::IntRect& new_rect)
{
layout_buttons();
set_dirty(true);
WindowManager::the().notify_rect_changed(m_window, old_rect, new_rect);
}
void WindowFrame::layout_buttons()
{
auto button_rects = Gfx::WindowTheme::current().layout_buttons(to_theme_window_type(m_window.type()), m_window.rect(), WindowManager::the().palette(), m_buttons.size());
for (size_t i = 0; i < m_buttons.size(); i++)
m_buttons[i].set_relative_rect(button_rects[i]);
}
Optional<HitTestResult> WindowFrame::hit_test(Gfx::IntPoint const& position)
{
if (m_window.is_frameless() || m_window.is_fullscreen())
return {};
if (!constrained_render_rect_to_screen(rect()).contains(position)) {
// Checking just frame_rect is not enough. If we constrain rendering
// a window to one screen (e.g. when it's maximized or tiled) so that
// the frame doesn't bleed into the adjacent screen(s), then we need
// to also check that we're within these bounds.
return {};
}
auto window_rect = m_window.rect();
if (window_rect.contains(position))
return {};
auto* screen = Screen::find_by_location(position);
if (!screen)
return {};
auto* cached = render_to_cache(*screen);
if (!cached)
return {};
auto window_relative_position = position.translated(-unconstrained_render_rect().location());
return cached->hit_test(*this, position, window_relative_position);
}
Optional<HitTestResult> WindowFrame::PerScaleRenderedCache::hit_test(WindowFrame& frame, Gfx::IntPoint const& position, Gfx::IntPoint const& window_relative_position)
{
HitTestResult result {
.window = frame.window(),
.screen_position = position,
.window_relative_position = window_relative_position,
.is_frame_hit = true,
};
u8 alpha_threshold = Gfx::WindowTheme::current().frame_alpha_hit_threshold(frame.window_state_for_theme()) * 255;
if (alpha_threshold == 0)
return result;
u8 alpha = 0xff;
auto window_rect = frame.window().rect();
if (position.y() < window_rect.y()) {
if (m_top_bottom) {
auto scaled_relative_point = window_relative_position * m_top_bottom->scale();
if (m_top_bottom->rect().contains(scaled_relative_point))
alpha = m_top_bottom->get_pixel(scaled_relative_point).alpha();
}
} else if (position.y() > window_rect.bottom()) {
if (m_top_bottom) {
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 };
if (m_top_bottom->rect().contains(scaled_relative_point))
alpha = m_top_bottom->get_pixel(scaled_relative_point).alpha();
}
} else if (position.x() < window_rect.x()) {
if (m_left_right) {
Gfx::IntPoint scaled_relative_point { window_relative_position.x() * m_left_right->scale(), (window_relative_position.y() - m_bottom_y) * m_left_right->scale() };
if (m_left_right->rect().contains(scaled_relative_point))
alpha = m_left_right->get_pixel(scaled_relative_point).alpha();
}
} else if (position.x() > window_rect.right()) {
if (m_left_right) {
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() };
if (m_left_right->rect().contains(scaled_relative_point))
alpha = m_left_right->get_pixel(scaled_relative_point).alpha();
}
} else {
return {};
}
if (alpha >= alpha_threshold)
return result;
return {};
}
bool WindowFrame::handle_titlebar_icon_mouse_event(MouseEvent const& event)
{
auto& wm = WindowManager::the();
if (event.type() == Event::MouseDown && (event.button() == MouseButton::Primary || event.button() == MouseButton::Secondary)) {
// Manually start a potential double click. Since we're opening
// a menu, we will only receive the MouseDown event, so we
// need to record that fact. If the user subsequently clicks
// on the same area, the menu will get closed, and we will
// receive a MouseUp event, but because windows have changed
// we don't get a MouseDoubleClick event. We can however record
// this click, and when we receive the MouseUp event check if
// it would have been considered a double click, if it weren't
// for the fact that we opened and closed a window in the meanwhile
wm.start_menu_doubleclick(m_window, event);
m_window.popup_window_menu(titlebar_rect().bottom_left().translated(rect().location()), WindowMenuDefaultAction::Close);
return true;
} else if (event.type() == Event::MouseUp && event.button() == MouseButton::Primary) {
// Since the MouseDown event opened a menu, another MouseUp
// from the second click outside the menu wouldn't be considered
// a double click, so let's manually check if it would otherwise
// have been be considered to be one
if (wm.is_menu_doubleclick(m_window, event)) {
// It is a double click, so perform activate the default item
m_window.window_menu_activate_default();
}
return true;
}
return false;
}
void WindowFrame::handle_titlebar_mouse_event(MouseEvent const& event)
{
auto& wm = WindowManager::the();
if (titlebar_icon_rect().contains(event.position())) {
if (handle_titlebar_icon_mouse_event(event))
return;
}
for (auto& button : m_buttons) {
if (button.relative_rect().contains(event.position()))
return button.on_mouse_event(event.translated(-button.relative_rect().location()));
}
if (event.type() == Event::MouseDown) {
if ((m_window.type() == WindowType::Normal || m_window.type() == WindowType::ToolWindow) && event.button() == MouseButton::Secondary) {
auto default_action = m_window.is_maximized() ? WindowMenuDefaultAction::Restore : WindowMenuDefaultAction::Maximize;
m_window.popup_window_menu(event.position().translated(rect().location()), default_action);
return;
}
if (m_window.is_movable() && event.button() == MouseButton::Primary)
wm.start_window_move(m_window, event.translated(rect().location()));
}
}
void WindowFrame::handle_mouse_event(MouseEvent const& event)
{
VERIFY(!m_window.is_fullscreen());
if (m_window.type() != WindowType::Normal && m_window.type() != WindowType::ToolWindow && m_window.type() != WindowType::Notification)
return;
auto& wm = WindowManager::the();
if (m_window.type() == WindowType::Normal || m_window.type() == WindowType::ToolWindow) {
if (event.type() == Event::MouseDown)
wm.move_to_front_and_make_active(m_window);
}
if (m_window.blocking_modal_window())
return;
// This is slightly hackish, but expand the title bar rect by two pixels downwards,
// so that mouse events between the title bar and window contents don't act like
// mouse events on the border.
auto adjusted_titlebar_rect = titlebar_rect();
adjusted_titlebar_rect.set_height(adjusted_titlebar_rect.height() + 2);
if (adjusted_titlebar_rect.contains(event.position())) {
handle_titlebar_mouse_event(event);
return;
}
if (menubar_rect().contains(event.position())) {
handle_menubar_mouse_event(event);
return;
}
handle_border_mouse_event(event);
}
void WindowFrame::handle_border_mouse_event(const MouseEvent& event)
{
if (!m_window.is_resizable())
return;
auto& wm = WindowManager::the();
if (event.type() == Event::MouseMove && event.buttons() == 0) {
constexpr ResizeDirection direction_for_hot_area[3][3] = {
{ ResizeDirection::UpLeft, ResizeDirection::Up, ResizeDirection::UpRight },
{ ResizeDirection::Left, ResizeDirection::None, ResizeDirection::Right },
{ ResizeDirection::DownLeft, ResizeDirection::Down, ResizeDirection::DownRight },
};
Gfx::IntRect outer_rect = { {}, rect().size() };
VERIFY(outer_rect.contains(event.position()));
int window_relative_x = event.x() - outer_rect.x();
int window_relative_y = event.y() - outer_rect.y();
int hot_area_row = min(2, window_relative_y / (outer_rect.height() / 3));
int hot_area_column = min(2, window_relative_x / (outer_rect.width() / 3));
wm.set_resize_candidate(m_window, direction_for_hot_area[hot_area_row][hot_area_column]);
Compositor::the().invalidate_cursor();
return;
}
if (event.type() == Event::MouseDown && event.button() == MouseButton::Primary)
wm.start_window_resize(m_window, event.translated(rect().location()));
}
void WindowFrame::handle_menubar_mouse_event(const MouseEvent& event)
{
Menu* hovered_menu = nullptr;
auto menubar_rect = this->menubar_rect();
auto adjusted_position = event.position().translated(-menubar_rect.location());
m_window.menubar().for_each_menu([&](Menu& menu) {
if (menu.rect_in_window_menubar().contains(adjusted_position)) {
hovered_menu = &menu;
handle_menu_mouse_event(menu, event);
return IterationDecision::Break;
}
return IterationDecision::Continue;
});
if (!hovered_menu && event.type() == Event::Type::MouseDown)
MenuManager::the().close_everyone();
if (hovered_menu != MenuManager::the().hovered_menu()) {
MenuManager::the().set_hovered_menu(hovered_menu);
invalidate(menubar_rect);
}
}
void WindowFrame::open_menubar_menu(Menu& menu)
{
auto menubar_rect = this->menubar_rect();
MenuManager::the().close_everyone();
menu.ensure_menu_window(menu.rect_in_window_menubar().bottom_left().translated(rect().location()).translated(menubar_rect.location()));
MenuManager::the().open_menu(menu);
WindowManager::the().set_window_with_active_menu(&m_window);
invalidate(menubar_rect);
}
void WindowFrame::handle_menu_mouse_event(Menu& menu, const MouseEvent& event)
{
auto menubar_rect = this->menubar_rect();
bool is_hover_with_any_menu_open = event.type() == MouseEvent::MouseMove && &m_window == WindowManager::the().window_with_active_menu();
bool is_mousedown_with_left_button = event.type() == MouseEvent::MouseDown && event.button() == MouseButton::Primary;
bool should_open_menu = &menu != MenuManager::the().current_menu() && (is_hover_with_any_menu_open || is_mousedown_with_left_button);
bool should_close_menu = &menu == MenuManager::the().current_menu() && is_mousedown_with_left_button;
if (should_open_menu) {
open_menubar_menu(menu);
return;
}
if (should_close_menu) {
invalidate(menubar_rect);
MenuManager::the().close_everyone();
}
}
void WindowFrame::start_flash_animation()
{
if (!m_flash_timer) {
m_flash_timer = Core::Timer::construct(100, [this] {
VERIFY(m_flash_counter);
invalidate_titlebar();
if (!--m_flash_counter)
m_flash_timer->stop();
});
}
m_flash_counter = 8;
m_flash_timer->start();
}
int WindowFrame::menu_row_count() const
{
if (!m_window.should_show_menubar())
return 0;
return m_window.menubar().has_menus() ? 1 : 0;
}
}