/* * Copyright (c) 2018-2023, Andreas Kling * * SPDX-License-Identifier: BSD-2-Clause */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace GUI { static i32 s_next_backing_store_serial; static IDAllocator s_window_id_allocator; class WindowBackingStore { public: explicit WindowBackingStore(NonnullRefPtr bitmap) : m_bitmap(move(bitmap)) , m_serial(++s_next_backing_store_serial) , m_visible_size(m_bitmap->size()) { } Gfx::Bitmap& bitmap() { return *m_bitmap; } Gfx::Bitmap const& bitmap() const { return *m_bitmap; } Gfx::IntSize size() const { return m_bitmap->size(); } i32 serial() const { return m_serial; } Gfx::IntSize visible_size() const { return m_visible_size; } void set_visible_size(Gfx::IntSize visible_size) { m_visible_size = visible_size; } private: NonnullRefPtr m_bitmap; const i32 m_serial; Gfx::IntSize m_visible_size; }; static NeverDestroyed> all_windows; static NeverDestroyed> reified_windows; Window* Window::from_window_id(int window_id) { auto it = reified_windows->find(window_id); if (it != reified_windows->end()) return (*it).value; return nullptr; } Window::Window(Core::Object* parent) : Core::Object(parent) , m_menubar(Menubar::construct()) { if (parent) set_window_mode(WindowMode::Passive); all_windows->set(this); m_rect_when_windowless = { -5000, -5000, 0, 0 }; m_title_when_windowless = "GUI::Window"; register_property( "title", [this] { return title(); }, [this](auto& value) { set_title(value.to_deprecated_string()); return true; }); register_property("visible", [this] { return is_visible(); }); register_property("active", [this] { return is_active(); }); REGISTER_BOOL_PROPERTY("minimizable", is_minimizable, set_minimizable); REGISTER_BOOL_PROPERTY("resizable", is_resizable, set_resizable); REGISTER_BOOL_PROPERTY("fullscreen", is_fullscreen, set_fullscreen); REGISTER_RECT_PROPERTY("rect", rect, set_rect); REGISTER_SIZE_PROPERTY("base_size", base_size, set_base_size); REGISTER_SIZE_PROPERTY("size_increment", size_increment, set_size_increment); REGISTER_BOOL_PROPERTY("obey_widget_min_size", is_obeying_widget_min_size, set_obey_widget_min_size); } Window::~Window() { all_windows->remove(this); hide(); } void Window::close() { hide(); if (on_close) on_close(); } void Window::move_to_front() { if (!is_visible()) return; ConnectionToWindowServer::the().async_move_window_to_front(m_window_id); } void Window::show() { if (is_visible()) return; auto* parent_window = find_parent_window(); m_window_id = s_window_id_allocator.allocate(); Gfx::IntRect launch_origin_rect; if (auto* launch_origin_rect_string = getenv("__libgui_launch_origin_rect")) { auto parts = StringView { launch_origin_rect_string, strlen(launch_origin_rect_string) }.split_view(','); if (parts.size() == 4) { launch_origin_rect = Gfx::IntRect { parts[0].to_int().value_or(0), parts[1].to_int().value_or(0), parts[2].to_int().value_or(0), parts[3].to_int().value_or(0), }; } unsetenv("__libgui_launch_origin_rect"); } update_min_size(); ConnectionToWindowServer::the().async_create_window( m_window_id, m_rect_when_windowless, !m_moved_by_client, m_has_alpha_channel, m_minimizable, m_closeable, m_resizable, m_fullscreen, m_frameless, m_forced_shadow, m_opacity_when_windowless, m_alpha_hit_threshold, m_base_size, m_size_increment, m_minimum_size_when_windowless, m_resize_aspect_ratio, (i32)m_window_type, (i32)m_window_mode, m_title_when_windowless, parent_window ? parent_window->window_id() : 0, launch_origin_rect); m_visible = true; m_visible_for_timer_purposes = true; apply_icon(); m_menubar->for_each_menu([&](Menu& menu) { menu.realize_menu_if_needed(); ConnectionToWindowServer::the().async_add_menu(m_window_id, menu.menu_id()); return IterationDecision::Continue; }); set_maximized(m_maximized); reified_windows->set(m_window_id, this); Application::the()->did_create_window({}); update(); } Window* Window::find_parent_window() { for (auto* ancestor = parent(); ancestor; ancestor = ancestor->parent()) { if (is(ancestor)) return static_cast(ancestor); } return nullptr; } void Window::server_did_destroy() { reified_windows->remove(m_window_id); m_window_id = 0; m_visible = false; m_pending_paint_event_rects.clear(); m_back_store = nullptr; m_front_store = nullptr; m_cursor = Gfx::StandardCursor::None; } void Window::hide() { if (!is_visible()) return; // NOTE: Don't bother asking WindowServer to destroy windows during application teardown. // All our windows will be automatically garbage-collected by WindowServer anyway. if (GUI::Application::in_teardown()) return; m_rect_when_windowless = rect(); auto destroyed_window_ids = ConnectionToWindowServer::the().destroy_window(m_window_id); server_did_destroy(); for (auto child_window_id : destroyed_window_ids) { if (auto* window = Window::from_window_id(child_window_id)) { window->server_did_destroy(); } } if (auto* app = Application::the()) { bool app_has_visible_windows = false; for (auto& window : *all_windows) { if (window->is_visible()) { app_has_visible_windows = true; break; } } if (!app_has_visible_windows) app->did_delete_last_window({}); } } void Window::set_title(DeprecatedString title) { m_title_when_windowless = move(title); if (!is_visible()) return; ConnectionToWindowServer::the().async_set_window_title(m_window_id, m_title_when_windowless); } DeprecatedString Window::title() const { if (!is_visible()) return m_title_when_windowless; return ConnectionToWindowServer::the().get_window_title(m_window_id); } Gfx::IntRect Window::applet_rect_on_screen() const { VERIFY(m_window_type == WindowType::Applet); return ConnectionToWindowServer::the().get_applet_rect_on_screen(m_window_id); } Gfx::IntRect Window::rect() const { if (!is_visible()) return m_rect_when_windowless; return ConnectionToWindowServer::the().get_window_rect(m_window_id); } void Window::set_rect(Gfx::IntRect const& a_rect) { if (a_rect.location() != m_rect_when_windowless.location()) { m_moved_by_client = true; } m_rect_when_windowless = a_rect; if (!is_visible()) { if (m_main_widget) m_main_widget->resize(m_rect_when_windowless.size()); return; } auto window_rect = ConnectionToWindowServer::the().set_window_rect(m_window_id, a_rect); if (m_back_store && m_back_store->size() != window_rect.size()) m_back_store = nullptr; if (m_front_store && m_front_store->size() != window_rect.size()) m_front_store = nullptr; if (m_main_widget) m_main_widget->resize(window_rect.size()); } Gfx::IntSize Window::minimum_size() const { if (!is_visible()) return m_minimum_size_when_windowless; return ConnectionToWindowServer::the().get_window_minimum_size(m_window_id); } void Window::set_minimum_size(Gfx::IntSize size) { VERIFY(size.width() >= 0 && size.height() >= 0); VERIFY(!is_obeying_widget_min_size()); m_minimum_size_when_windowless = size; if (is_visible()) ConnectionToWindowServer::the().async_set_window_minimum_size(m_window_id, size); } void Window::center_on_screen() { set_rect(rect().centered_within(Desktop::the().rect())); } void Window::center_within(Window const& other) { if (this == &other) return; set_rect(rect().centered_within(other.rect())); } void Window::set_window_type(WindowType window_type) { m_window_type = window_type; } void Window::set_window_mode(WindowMode mode) { VERIFY(!is_visible()); m_window_mode = mode; } void Window::make_window_manager(unsigned event_mask) { GUI::ConnectionToWindowManagerServer::the().async_set_event_mask(event_mask); GUI::ConnectionToWindowManagerServer::the().async_set_manager_window(m_window_id); } bool Window::are_cursors_the_same(AK::Variant> const& left, AK::Variant> const& right) const { if (left.has() != right.has()) return false; if (left.has()) return left.get() == right.get(); return left.get>().ptr() == right.get>().ptr(); } void Window::set_cursor(Gfx::StandardCursor cursor) { if (are_cursors_the_same(m_cursor, cursor)) return; m_cursor = cursor; update_cursor(); } void Window::set_cursor(NonnullRefPtr cursor) { if (are_cursors_the_same(m_cursor, cursor)) return; m_cursor = cursor; update_cursor(); } void Window::handle_drop_event(DropEvent& event) { if (!m_main_widget) return; auto result = m_main_widget->hit_test(event.position()); auto local_event = make(result.local_position, event.text(), event.mime_data()); VERIFY(result.widget); result.widget->dispatch_event(*local_event, this); Application::the()->set_drag_hovered_widget({}, nullptr); } void Window::handle_mouse_event(MouseEvent& event) { if (!m_main_widget) return; auto result = m_main_widget->hit_test(event.position()); VERIFY(result.widget); if (m_automatic_cursor_tracking_widget) { auto window_relative_rect = m_automatic_cursor_tracking_widget->window_relative_rect(); Gfx::IntPoint local_point { event.x() - window_relative_rect.x(), event.y() - window_relative_rect.y() }; auto local_event = MouseEvent((Event::Type)event.type(), local_point, event.buttons(), event.button(), event.modifiers(), event.wheel_delta_x(), event.wheel_delta_y(), event.wheel_raw_delta_x(), event.wheel_raw_delta_y()); m_automatic_cursor_tracking_widget->dispatch_event(local_event, this); if (event.buttons() == 0) { m_automatic_cursor_tracking_widget = nullptr; } else { auto is_hovered = m_automatic_cursor_tracking_widget.ptr() == result.widget.ptr(); set_hovered_widget(is_hovered ? m_automatic_cursor_tracking_widget.ptr() : nullptr); } return; } set_hovered_widget(result.widget); if (event.buttons() != 0 && !m_automatic_cursor_tracking_widget) m_automatic_cursor_tracking_widget = *result.widget; auto local_event = MouseEvent((Event::Type)event.type(), result.local_position, event.buttons(), event.button(), event.modifiers(), event.wheel_delta_x(), event.wheel_delta_y(), event.wheel_raw_delta_x(), event.wheel_raw_delta_y()); result.widget->dispatch_event(local_event, this); } Gfx::IntSize Window::backing_store_size(Gfx::IntSize window_size) const { if (!m_resizing) return window_size; int const backing_margin_during_resize = 64; return { window_size.width() + backing_margin_during_resize, window_size.height() + backing_margin_during_resize }; } void Window::handle_multi_paint_event(MultiPaintEvent& event) { if (!is_visible()) return; if (!m_main_widget) return; auto rects = event.rects(); if (!m_pending_paint_event_rects.is_empty()) { // It's possible that there had been some calls to update() that // haven't been flushed. We can handle these right now, avoiding // another round trip. rects.extend(move(m_pending_paint_event_rects)); } VERIFY(!rects.is_empty()); // Throw away our backing store if its size is different, and we've stopped resizing or double buffering is disabled. // This ensures that we shrink the backing store after a resize, and that we do not get flickering artifacts when // directly painting into a shared active backing store. if (m_back_store && (!m_resizing || !m_double_buffering_enabled) && m_back_store->size() != event.window_size()) m_back_store = nullptr; // Discard our backing store if it's unable to contain the new window size. Smaller is fine though, that prevents // lots of backing store allocations during a resize. if (m_back_store && !m_back_store->size().contains(event.window_size())) m_back_store = nullptr; bool created_new_backing_store = false; if (!m_back_store) { m_back_store = create_backing_store(backing_store_size(event.window_size())).release_value_but_fixme_should_propagate_errors(); created_new_backing_store = true; } else if (m_double_buffering_enabled) { bool was_purged = false; bool bitmap_has_memory = m_back_store->bitmap().set_nonvolatile(was_purged); if (!bitmap_has_memory) { // We didn't have enough memory to make the bitmap non-volatile! // Fall back to single-buffered mode for this window. // FIXME: Once we have a way to listen for system memory pressure notifications, // it would be cool to transition back into double-buffered mode once // the coast is clear. dbgln("Not enough memory to make backing store non-volatile. Falling back to single-buffered mode."); m_double_buffering_enabled = false; m_back_store = move(m_front_store); created_new_backing_store = true; } else if (was_purged) { // The backing store bitmap was cleared, but it does have memory. // Act as if it's a new backing store so the entire window gets repainted. created_new_backing_store = true; } } if (created_new_backing_store) { rects.clear(); rects.append({ {}, event.window_size() }); } for (auto& rect : rects) { PaintEvent paint_event(rect); m_main_widget->dispatch_event(paint_event, this); } m_back_store->set_visible_size(event.window_size()); if (m_double_buffering_enabled) flip(rects); else if (created_new_backing_store) set_current_backing_store(*m_back_store, true); if (is_visible()) ConnectionToWindowServer::the().async_did_finish_painting(m_window_id, rects); } void Window::propagate_shortcuts_up_to_application(KeyEvent& event, Widget* widget) { VERIFY(event.type() == Event::KeyDown); auto shortcut = Shortcut(event.modifiers(), event.key()); Action* action = nullptr; if (widget) { VERIFY(widget->window() == this); do { action = widget->action_for_shortcut(shortcut); if (action) break; widget = widget->parent_widget(); } while (widget); } if (!action) action = action_for_shortcut(shortcut); if (!action) action = Application::the()->action_for_shortcut(shortcut); if (action) { action->process_event(*this, event); return; } event.ignore(); } void Window::handle_key_event(KeyEvent& event) { if (!m_focused_widget && event.type() == Event::KeyDown && event.key() == Key_Tab && !event.ctrl() && !event.alt() && !event.super()) { focus_a_widget_if_possible(FocusSource::Keyboard); } if (m_default_return_key_widget && event.key() == Key_Return) if (!m_focused_widget || !is