From 94c0be051be354b81f018e2dcf3feb07046f0c5b Mon Sep 17 00:00:00 2001 From: hippoz <10706925-hippoz@users.noreply.gitlab.com> Date: Fri, 10 Jun 2022 19:42:03 +0300 Subject: [PATCH] Add "invalidation rectangle"-based repainting Previously, we were repainting each widget as repainting was needed. However, this created issues with clipping, since the widgets were not aware of their parent's clips. It also created many other issues, including performance problems and the lack of support for overlapping widgets. This commit improves repainting behavior by adopting a painting model similar to the one found in SerenityOS's LibGUI. It uses "damage rectangles", which are translated to the widget coordinate space as needed, since the position of widgets is relative to their parent's origin. When a widget needs to be repainted, a repaint event with the damage rectangle equal to the widget's current geometry translated to the window's coordinate space is dispatched to the main widget. It will recursively follow the widget tree. Widgets fully contain their children, thus widgets not contained by the damage rectangle will reject the event, ensuring repainting is only done where needed. Relayouting is done on a per-subtree basis only. This commit should pave the way for things like scrolling and overlapping widgets. --- src/Button.cpp | 1 + src/DocumentLayout.cpp | 4 +- src/Events.hpp | 42 ++++++------ src/Painter.cpp | 4 +- src/PropMacros.hpp | 2 +- src/TopLevelStyles.hpp | 4 +- src/Widget.cpp | 147 +++++++++++++++++++++++------------------ src/Widget.hpp | 14 ++-- src/Window.cpp | 64 +++++++++--------- src/Window.hpp | 9 ++- src/main.cpp | 9 ++- 11 files changed, 164 insertions(+), 136 deletions(-) diff --git a/src/Button.cpp b/src/Button.cpp index ad7c111..8faf7cb 100644 --- a/src/Button.cpp +++ b/src/Button.cpp @@ -17,6 +17,7 @@ void Button::set_text(std::string text) { } void Button::on_init() { + set_background_border_radius(styles()->button_border_radius()); set_background_fill_color(styles()->button_normal_color()); set_do_background_fill(true); fit_text(m_text); diff --git a/src/DocumentLayout.cpp b/src/DocumentLayout.cpp index 9b8d27d..dae8fd3 100644 --- a/src/DocumentLayout.cpp +++ b/src/DocumentLayout.cpp @@ -11,9 +11,9 @@ void DocumentLayout::run() { Point current_position { m_margin, m_margin }; double largest_height_so_far = -1.0; - auto& children = m_target->children(); - for (auto& child : children) { + auto& children = m_target->children(); + for (auto child : children) { if (child->absolute()) continue; diff --git a/src/Events.hpp b/src/Events.hpp index e07eec6..9b2391a 100644 --- a/src/Events.hpp +++ b/src/Events.hpp @@ -11,21 +11,12 @@ enum class EventType { MouseButton, MouseMove, - Reflow, + RelayoutSubtree, + RepaintRect, FocusUpdate, ActivationUpdate, }; -enum class ReflowType { - RepaintRect, - RelayoutRect -}; - -enum class ReflowGrouping { - Yes, - No -}; - class Event { private: bool m_accepted { false }; @@ -72,31 +63,36 @@ public: const char *name() { return "MouseMove"; } Point &point() { return m_point; } + void set_point(Point point) { m_point = point; } }; -class ReflowEvent: public Event { +class RepaintRectEvent : public Event { private: - ReflowType m_reflow_type; - ReflowGrouping m_grouping; + bool m_grouping { true }; Box m_box; public: - ReflowEvent(ReflowType type, ReflowGrouping grouping, Box box) - : m_reflow_type(type) - , m_grouping(grouping) + RepaintRectEvent(bool grouping, Box box) + : m_grouping(grouping) , m_box(box) {} - EventType type() { return EventType::Reflow; } - const char *name() { return "Reflow"; } + EventType type() { return EventType::RepaintRect; } + const char *name() { return "RepaintRect"; } - ReflowType reflow_type() { return m_reflow_type; } - ReflowGrouping grouping() { return m_grouping; } + bool grouping() { return m_grouping; } Box &box() { return m_box; } - void set_grouping(ReflowGrouping grouping) { m_grouping = grouping; } - void set_reflow_type(ReflowType type) { m_reflow_type = type; } + void set_grouping(bool grouping) { m_grouping = grouping; } void set_box(Box box) { m_box = box; } }; +class RelayoutSubtreeEvent : public Event { + public: + RelayoutSubtreeEvent() {} + + EventType type() { return EventType::RelayoutSubtree; } + const char *name() { return "RelayoutSubtree"; } +}; + class FocusUpdateEvent : public Event { private: bool m_focus_status; diff --git a/src/Painter.cpp b/src/Painter.cpp index a5b2042..2caf394 100644 --- a/src/Painter.cpp +++ b/src/Painter.cpp @@ -10,8 +10,8 @@ void Painter::rounded_rectangle(Box &geometry, double border_radius) { double aspect = 1.0; double radius = border_radius / aspect; double degrees = M_PI / 180.0; - double x = 0; - double y = 0; + double x = geometry.x(); + double y = geometry.y(); double w = geometry.width(); double h = geometry.height(); diff --git a/src/PropMacros.hpp b/src/PropMacros.hpp index 2c6dd5b..ed98e92 100644 --- a/src/PropMacros.hpp +++ b/src/PropMacros.hpp @@ -4,6 +4,6 @@ private: \ type m_##name {__VA_ARGS__}; \ public: \ - void set_##name(type new_prop_value) { m_##name = new_prop_value; wants_repaint(); } \ + void set_##name(type new_prop_value) { m_##name = new_prop_value; repaint(); } \ type name() { return m_##name; } \ private: diff --git a/src/TopLevelStyles.hpp b/src/TopLevelStyles.hpp index e9f0a03..386f371 100644 --- a/src/TopLevelStyles.hpp +++ b/src/TopLevelStyles.hpp @@ -23,7 +23,7 @@ DEF_WIDGET_STYLE_PROP(button_text_color, RGB, m_text_color) DEF_WIDGET_STYLE_PROP(button_normal_color, RGB, m_accent_color) DEF_WIDGET_STYLE_PROP(button_focused_color, RGB, m_accent_color_darker) DEF_WIDGET_STYLE_PROP(button_active_color, RGB, m_accent_color_darkest) -DEF_WIDGET_STYLE_PROP(button_border_radius, double, 8.0) +DEF_WIDGET_STYLE_PROP(button_border_radius, double, 6.0) DEF_WIDGET_STYLE_PROP(label_text_color, RGB, m_text_color) @@ -51,7 +51,7 @@ public: Window *window() { return m_window; } void set_window(Window *window) { m_window = window; } - void wants_repaint(); + void repaint(); }; } diff --git a/src/Widget.cpp b/src/Widget.cpp index 82baaa8..5beeb32 100644 --- a/src/Widget.cpp +++ b/src/Widget.cpp @@ -9,6 +9,14 @@ namespace Raven { +Point Widget::window_relative() { + Point point = { 0, 0 }; + for (Widget* parent = m_parent; parent; parent = parent->parent()) { + point.add(parent->current_geometry().x(), parent->current_geometry().y()); + } + return point; +} + void Widget::fit_text(std::string &text) { if (!window()) return; @@ -16,7 +24,7 @@ void Widget::fit_text(std::string &text) { window()->painter().set_pango_font_description(styles()->controls_font_description()); auto size = window()->painter().compute_text_size(current_geometry(), text); if (!resize(size)) { - wants_full_relayout(); + reflow(); } } @@ -29,7 +37,7 @@ bool Widget::resize(double width, double height) { m_current_geometry.set_width(width); m_current_geometry.set_height(height); - wants_full_relayout(); + reflow(); return true; } @@ -39,7 +47,7 @@ void Widget::move_to(double x, double y) { m_current_geometry.set_x(x); m_current_geometry.set_y(y); - wants_full_relayout(); + reflow(); } void Widget::do_layout() { @@ -73,36 +81,26 @@ bool Widget::add_child(std::shared_ptr child) { // TODO?: what happens when the parent changes its window? child->set_window(m_window); - wants_full_relayout(); + reflow(); return true; } void Widget::remove_child(std::shared_ptr child) { m_children.erase(std::remove(m_children.begin(), m_children.end(), child), m_children.end()); - wants_full_relayout(); + reflow(); } -void Widget::wants_repaint() { +void Widget::repaint() { if (m_window) - m_window->reflow(this, ReflowType::RepaintRect); + m_window->repaint(this); } -void Widget::wants_relayout() { +void Widget::reflow() { if (m_window) - m_window->reflow(this, ReflowType::RelayoutRect); + m_window->reflow(); } -void Widget::wants_full_repaint() { - if (m_window) - m_window->reflow(ReflowType::RepaintRect); -} - -void Widget::wants_full_relayout() { - if (m_window) - m_window->reflow(ReflowType::RelayoutRect); -} - -void Widget::handle_reflow(ReflowEvent &event) { +void Widget::handle_repaint_rect(RepaintRectEvent &event) { event.accept(); // immediately accept the event - we will do our own propagation logic auto painter = m_window->painter(); @@ -112,36 +110,40 @@ void Widget::handle_reflow(ReflowEvent &event) { if (!event.box().contains_box(current_geometry())) return; // widgets contain their children, thus we don't need to recurse further + // using a "group" in cairo reduces flickering bool should_end_paint_group = false; - if (event.grouping() == ReflowGrouping::Yes) { + if (event.grouping()) { painter.begin_paint_group(); should_end_paint_group = true; } - if (event.reflow_type() == ReflowType::RelayoutRect) { - do_layout(); - } - auto cr = painter.cairo(); cr->save(); - cr->translate(current_geometry().x(), current_geometry().y()); + // clip this widget. this ensuers that the background fill or children won't overflow the bounds of it painter.rounded_rectangle(m_current_geometry, m_background_border_radius); cr->clip(); + + // paint the background fill + // note that we're using the bounds of the paint event as the rectangle, this ensures that we dont draw over other widgets which won't be repainted. if (m_do_background_fill) { painter.source_rgb(m_background_fill_color); painter.rounded_rectangle(event.box(), m_background_border_radius); cr->fill(); } + // translate to the widget's position + // the origin of the painter is now the widget's origin + // this is because the position of children is relative to the origin of their parent + cr->translate(current_geometry().x(), current_geometry().y()); + on_paint(); - Box box = event.box(); - for (auto& child : m_children) { - // convert the invalidation rectangle to the child's coordinate space - auto local_rect = box.offset(child->current_geometry().x(), child->current_geometry().y()); - auto local_event = ReflowEvent(event.reflow_type(), ReflowGrouping::No, local_rect); + // convert the paint event's origin to our coordinate space because all positions of children are relative to the origin of their parent + auto local_box = event.box().offset(-m_current_geometry.x(), -m_current_geometry.y()); + auto local_event = RepaintRectEvent(false, local_box); + for (auto child : m_children) { child->dispatch_event(local_event); } @@ -152,59 +154,72 @@ void Widget::handle_reflow(ReflowEvent &event) { } } +void Widget::handle_relayout_subtree() { + do_layout(); +} + void Widget::handle_mouse_move_event(MouseMoveEvent &event) { + event.accept(); // we will do our own propagation logic + bool update_focus_to = true; - - std::cout << "point: " << event.point().x() << ", " << event.point().y() << std::endl; - if (!m_current_geometry.contains_point(event.point())) { // we just became unfocused - if (m_is_focused) { - update_focus_to = false; - } else { - event.accept(); - return; - } + update_focus_to = false; } - if (m_is_focused == update_focus_to) - return; - - m_is_focused = update_focus_to; - auto focus_update_event = FocusUpdateEvent(update_focus_to); - on_focus_update(focus_update_event); on_mouse_move(event); - if (m_consumes_hits) { - event.accept(); + if (m_is_focused != update_focus_to) { + m_is_focused = update_focus_to; + if (m_is_focused && m_window) + m_window->set_focused_widget(this); + + auto focus_update_event = FocusUpdateEvent(update_focus_to); + on_focus_update(focus_update_event); } - if (m_is_focused && m_window) - m_window->set_focused_widget(this); + if (!m_consumes_hits) { + // translate the event's point to our coordinate space because the position of all children is relative to the origin of their parent + auto local_event = MouseMoveEvent(Point( + event.point().x() - m_current_geometry.x(), + event.point().y() - m_current_geometry.y() + )); + for (auto child : m_children) { + child->dispatch_event(local_event); + } + } } void Widget::handle_mouse_button_event(MouseButtonEvent &event) { + event.accept(); // we will do our own propagation logic + bool update_activation_to = event.was_left_button_pressed(); if (!m_current_geometry.contains_point(event.point())) { - event.accept(); return; } - if (m_is_active == update_activation_to) - return; - - m_is_active = update_activation_to; - auto activation_update_event = ActivationUpdateEvent(update_activation_to); - on_activation_update(activation_update_event); - on_mouse_button(event); - if (m_is_active && m_window) - m_window->set_active_widget(this); + if (m_is_active != update_activation_to) { + m_is_active = update_activation_to; + if (m_is_active && m_window) + m_window->set_active_widget(this); - if (m_consumes_hits) - event.accept(); + auto activation_update_event = ActivationUpdateEvent(update_activation_to); + on_activation_update(activation_update_event); + } + + if (!m_consumes_hits) { + // translate the event's point to our coordinate space because the position of all children is relative to the origin of their parent + auto local_event = MouseButtonEvent(event.was_left_button_pressed(), event.was_right_button_pressed(), Point( + event.point().x() - m_current_geometry.x(), + event.point().y() - m_current_geometry.y() + )); + for (auto child : m_children) { + child->dispatch_event(local_event); + } + } } void Widget::dispatch_event(Event &event) { @@ -222,8 +237,12 @@ void Widget::dispatch_event(Event &event) { handle_mouse_button_event(reinterpret_cast(event)); break; } - case EventType::Reflow: { - handle_reflow(reinterpret_cast(event)); + case EventType::RepaintRect: { + handle_repaint_rect(reinterpret_cast(event)); + break; + } + case EventType::RelayoutSubtree: { + handle_relayout_subtree(); break; } /* these events aren't handled here, as they won't be dispatched to us from other places */ diff --git a/src/Widget.hpp b/src/Widget.hpp index e51374c..969cacb 100644 --- a/src/Widget.hpp +++ b/src/Widget.hpp @@ -59,13 +59,14 @@ public: void remove_child(std::shared_ptr child); Box ¤t_geometry() { return m_current_geometry; } - void set_current_geometry(Box current_geometry) { m_current_geometry = current_geometry; wants_full_relayout(); } - void set_current_geometry(Box current_geometry, bool is_pure) { m_current_geometry = current_geometry; if (!is_pure) { wants_full_relayout(); } } + void set_current_geometry(Box current_geometry) { m_current_geometry = current_geometry; reflow(); } + void set_current_geometry(Box current_geometry, bool is_pure) { m_current_geometry = current_geometry; if (!is_pure) { reflow(); } } + + Point window_relative(); WidgetType type() { return m_type; } ControlWidgetType control_type() { return m_control_type; } - Widget *parent() { return m_parent; } void set_parent(Widget *parent) { m_parent = parent; } @@ -121,11 +122,12 @@ protected: virtual void on_activation_update(ActivationUpdateEvent &event) {} virtual void on_paint() {} - void wants_full_repaint(); - void wants_full_relayout(); + void repaint(); + void reflow(); private: void do_layout(); - void handle_reflow(ReflowEvent& event); + void handle_repaint_rect(RepaintRectEvent &event); + void handle_relayout_subtree(); void handle_mouse_move_event(MouseMoveEvent &event); void handle_mouse_button_event(MouseButtonEvent &event); diff --git a/src/Window.cpp b/src/Window.cpp index 29de1be..75fb463 100644 --- a/src/Window.cpp +++ b/src/Window.cpp @@ -64,55 +64,57 @@ bool Window::dispatch_to_main_widget(Event &event) { if (!m_main_widget) return false; - std::cout << "Dispatched " << event.name() << " to main widget" << std::endl; + //std::cout << "Dispatched " << event.name() << " to main widget" << std::endl; m_main_widget->dispatch_event(event); return true; } -void Window::reflow(Widget *target, ReflowType type) { - if (type == ReflowType::RelayoutRect) { - m_did_relayout_during_batch = true; - } else if (type == ReflowType::RepaintRect) { - m_did_repaint_during_batch = true; - } +void Window::repaint(Box geometry) { + m_did_repaint_during_batch = true; if (m_is_batching) { return; } - auto event = ReflowEvent(type, ReflowGrouping::Yes, target->current_geometry()); + auto event = RepaintRectEvent(true, geometry); + dispatch_to_main_widget(event); +} + +void Window::repaint(Widget *target) { + std::cout << target->window_relative().x() << ", " << target->window_relative().y() << std::endl; + repaint(target->current_geometry().offset(target->window_relative().x(), target->window_relative().y())); +} + +void Window::repaint() { + repaint(m_current_geometry); +} + +void Window::relayout(Widget *target) { + m_did_relayout_during_batch = true; + + if (m_is_batching) { + return; + } + + auto event = RelayoutSubtreeEvent(); target->dispatch_event(event); } -void Window::reflow(ReflowType type) { - if (type == ReflowType::RelayoutRect) { - m_did_relayout_during_batch = true; - } else if (type == ReflowType::RepaintRect) { - m_did_repaint_during_batch = true; - } +void Window::relayout() { + m_did_relayout_during_batch = true; if (m_is_batching) { return; } - auto event = ReflowEvent(type, ReflowGrouping::Yes, m_current_geometry); + auto event = RelayoutSubtreeEvent(); dispatch_to_main_widget(event); } -void Window::reflow(Box &box, ReflowType type) { - if (type == ReflowType::RelayoutRect) { - m_did_relayout_during_batch = true; - } else if (type == ReflowType::RepaintRect) { - m_did_repaint_during_batch = true; - } - - if (m_is_batching) { - return; - } - - auto event = ReflowEvent(type, ReflowGrouping::Yes, box); - dispatch_to_main_widget(event); +void Window::reflow() { + relayout(); + repaint(); } void Window::start_batch() { @@ -125,9 +127,9 @@ void Window::end_batch() { if (m_is_batching) { m_is_batching = false; if (m_did_relayout_during_batch) { - reflow(ReflowType::RelayoutRect); + reflow(); } else if (m_did_repaint_during_batch) { - reflow(ReflowType::RelayoutRect); + repaint(); } } } @@ -161,7 +163,7 @@ void Window::run(bool block) { } case Expose: { if (e.xexpose.count == 0) { - reflow(ReflowType::RelayoutRect); + reflow(); } break; } diff --git a/src/Window.hpp b/src/Window.hpp index e31f269..08aa7dd 100644 --- a/src/Window.hpp +++ b/src/Window.hpp @@ -36,9 +36,12 @@ public: std::shared_ptr top_level_styles() { return m_top_level_styles; } - void reflow(Widget *target, ReflowType type); - void reflow(Box &box, ReflowType type); - void reflow(ReflowType type); + void repaint(); + void repaint(Box geometry); + void repaint(Widget *target); + void relayout(Widget *target); + void relayout(); + void reflow(); Box ¤t_geometry() { return m_current_geometry; } diff --git a/src/main.cpp b/src/main.cpp index 8cdc1ab..07dcc19 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -5,6 +5,7 @@ #include "Box.hpp" #include "Label.hpp" #include "Layout.hpp" +#include "RGB.hpp" #include "src/DocumentLayout.hpp" #include "src/Events.hpp" #include @@ -17,9 +18,13 @@ int main() { auto main_widget = window.set_main_widget(); - auto inner_widget = main_widget->add(); + auto second_widget = main_widget->add(); + second_widget->set_layout(16.0); + second_widget->resize(800, 800); + + auto inner_widget = second_widget->add(); inner_widget->set_layout(8.0); - inner_widget->resize(400, 400); + inner_widget->resize(600, 600); int number = 0;