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.
This commit is contained in:
hippoz 2022-06-10 19:42:03 +03:00
parent f14d88c776
commit 94c0be051b
Signed by: hippoz
GPG key ID: 7C52899193467641
11 changed files with 164 additions and 136 deletions

View file

@ -17,6 +17,7 @@ void Button::set_text(std::string text) {
} }
void Button::on_init() { void Button::on_init() {
set_background_border_radius(styles()->button_border_radius());
set_background_fill_color(styles()->button_normal_color()); set_background_fill_color(styles()->button_normal_color());
set_do_background_fill(true); set_do_background_fill(true);
fit_text(m_text); fit_text(m_text);

View file

@ -11,9 +11,9 @@ void DocumentLayout::run() {
Point current_position { m_margin, m_margin }; Point current_position { m_margin, m_margin };
double largest_height_so_far = -1.0; 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()) if (child->absolute())
continue; continue;

View file

@ -11,21 +11,12 @@ enum class EventType {
MouseButton, MouseButton,
MouseMove, MouseMove,
Reflow, RelayoutSubtree,
RepaintRect,
FocusUpdate, FocusUpdate,
ActivationUpdate, ActivationUpdate,
}; };
enum class ReflowType {
RepaintRect,
RelayoutRect
};
enum class ReflowGrouping {
Yes,
No
};
class Event { class Event {
private: private:
bool m_accepted { false }; bool m_accepted { false };
@ -72,31 +63,36 @@ public:
const char *name() { return "MouseMove"; } const char *name() { return "MouseMove"; }
Point &point() { return m_point; } Point &point() { return m_point; }
void set_point(Point point) { m_point = point; }
}; };
class ReflowEvent: public Event { class RepaintRectEvent : public Event {
private: private:
ReflowType m_reflow_type; bool m_grouping { true };
ReflowGrouping m_grouping;
Box m_box; Box m_box;
public: public:
ReflowEvent(ReflowType type, ReflowGrouping grouping, Box box) RepaintRectEvent(bool grouping, Box box)
: m_reflow_type(type) : m_grouping(grouping)
, m_grouping(grouping)
, m_box(box) {} , m_box(box) {}
EventType type() { return EventType::Reflow; } EventType type() { return EventType::RepaintRect; }
const char *name() { return "Reflow"; } const char *name() { return "RepaintRect"; }
ReflowType reflow_type() { return m_reflow_type; } bool grouping() { return m_grouping; }
ReflowGrouping grouping() { return m_grouping; }
Box &box() { return m_box; } Box &box() { return m_box; }
void set_grouping(ReflowGrouping grouping) { m_grouping = grouping; } void set_grouping(bool grouping) { m_grouping = grouping; }
void set_reflow_type(ReflowType type) { m_reflow_type = type; }
void set_box(Box box) { m_box = box; } 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 { class FocusUpdateEvent : public Event {
private: private:
bool m_focus_status; bool m_focus_status;

View file

@ -10,8 +10,8 @@ void Painter::rounded_rectangle(Box &geometry, double border_radius) {
double aspect = 1.0; double aspect = 1.0;
double radius = border_radius / aspect; double radius = border_radius / aspect;
double degrees = M_PI / 180.0; double degrees = M_PI / 180.0;
double x = 0; double x = geometry.x();
double y = 0; double y = geometry.y();
double w = geometry.width(); double w = geometry.width();
double h = geometry.height(); double h = geometry.height();

View file

@ -4,6 +4,6 @@
private: \ private: \
type m_##name {__VA_ARGS__}; \ type m_##name {__VA_ARGS__}; \
public: \ 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; } \ type name() { return m_##name; } \
private: private:

View file

@ -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_normal_color, RGB, m_accent_color)
DEF_WIDGET_STYLE_PROP(button_focused_color, RGB, m_accent_color_darker) 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_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) DEF_WIDGET_STYLE_PROP(label_text_color, RGB, m_text_color)
@ -51,7 +51,7 @@ public:
Window *window() { return m_window; } Window *window() { return m_window; }
void set_window(Window *window) { m_window = window; } void set_window(Window *window) { m_window = window; }
void wants_repaint(); void repaint();
}; };
} }

View file

@ -9,6 +9,14 @@
namespace Raven { 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) { void Widget::fit_text(std::string &text) {
if (!window()) if (!window())
return; return;
@ -16,7 +24,7 @@ void Widget::fit_text(std::string &text) {
window()->painter().set_pango_font_description(styles()->controls_font_description()); window()->painter().set_pango_font_description(styles()->controls_font_description());
auto size = window()->painter().compute_text_size(current_geometry(), text); auto size = window()->painter().compute_text_size(current_geometry(), text);
if (!resize(size)) { 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_width(width);
m_current_geometry.set_height(height); m_current_geometry.set_height(height);
wants_full_relayout(); reflow();
return true; return true;
} }
@ -39,7 +47,7 @@ void Widget::move_to(double x, double y) {
m_current_geometry.set_x(x); m_current_geometry.set_x(x);
m_current_geometry.set_y(y); m_current_geometry.set_y(y);
wants_full_relayout(); reflow();
} }
void Widget::do_layout() { void Widget::do_layout() {
@ -73,36 +81,26 @@ bool Widget::add_child(std::shared_ptr<Widget> child) {
// TODO?: what happens when the parent changes its window? // TODO?: what happens when the parent changes its window?
child->set_window(m_window); child->set_window(m_window);
wants_full_relayout(); reflow();
return true; return true;
} }
void Widget::remove_child(std::shared_ptr<Widget> child) { void Widget::remove_child(std::shared_ptr<Widget> child) {
m_children.erase(std::remove(m_children.begin(), m_children.end(), child), m_children.end()); 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) if (m_window)
m_window->reflow(this, ReflowType::RepaintRect); m_window->repaint(this);
} }
void Widget::wants_relayout() { void Widget::reflow() {
if (m_window) if (m_window)
m_window->reflow(this, ReflowType::RelayoutRect); m_window->reflow();
} }
void Widget::wants_full_repaint() { void Widget::handle_repaint_rect(RepaintRectEvent &event) {
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) {
event.accept(); // immediately accept the event - we will do our own propagation logic event.accept(); // immediately accept the event - we will do our own propagation logic
auto painter = m_window->painter(); auto painter = m_window->painter();
@ -112,36 +110,40 @@ void Widget::handle_reflow(ReflowEvent &event) {
if (!event.box().contains_box(current_geometry())) if (!event.box().contains_box(current_geometry()))
return; // widgets contain their children, thus we don't need to recurse further 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; bool should_end_paint_group = false;
if (event.grouping() == ReflowGrouping::Yes) { if (event.grouping()) {
painter.begin_paint_group(); painter.begin_paint_group();
should_end_paint_group = true; should_end_paint_group = true;
} }
if (event.reflow_type() == ReflowType::RelayoutRect) {
do_layout();
}
auto cr = painter.cairo(); auto cr = painter.cairo();
cr->save(); 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); painter.rounded_rectangle(m_current_geometry, m_background_border_radius);
cr->clip(); 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) { if (m_do_background_fill) {
painter.source_rgb(m_background_fill_color); painter.source_rgb(m_background_fill_color);
painter.rounded_rectangle(event.box(), m_background_border_radius); painter.rounded_rectangle(event.box(), m_background_border_radius);
cr->fill(); 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(); on_paint();
Box box = event.box(); // convert the paint event's origin to our coordinate space because all positions of children are relative to the origin of their parent
for (auto& child : m_children) { auto local_box = event.box().offset(-m_current_geometry.x(), -m_current_geometry.y());
// convert the invalidation rectangle to the child's coordinate space auto local_event = RepaintRectEvent(false, local_box);
auto local_rect = box.offset(child->current_geometry().x(), child->current_geometry().y()); for (auto child : m_children) {
auto local_event = ReflowEvent(event.reflow_type(), ReflowGrouping::No, local_rect);
child->dispatch_event(local_event); 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) { void Widget::handle_mouse_move_event(MouseMoveEvent &event) {
event.accept(); // we will do our own propagation logic
bool update_focus_to = true; bool update_focus_to = true;
std::cout << "point: " << event.point().x() << ", " << event.point().y() << std::endl;
if (!m_current_geometry.contains_point(event.point())) { if (!m_current_geometry.contains_point(event.point())) {
// we just became unfocused // we just became unfocused
if (m_is_focused) { update_focus_to = false;
update_focus_to = false;
} else {
event.accept();
return;
}
} }
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); on_mouse_move(event);
if (m_consumes_hits) { if (m_is_focused != update_focus_to) {
event.accept(); 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) if (!m_consumes_hits) {
m_window->set_focused_widget(this); // 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) { 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(); bool update_activation_to = event.was_left_button_pressed();
if (!m_current_geometry.contains_point(event.point())) { if (!m_current_geometry.contains_point(event.point())) {
event.accept();
return; 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); on_mouse_button(event);
if (m_is_active && m_window) if (m_is_active != update_activation_to) {
m_window->set_active_widget(this); m_is_active = update_activation_to;
if (m_is_active && m_window)
m_window->set_active_widget(this);
if (m_consumes_hits) auto activation_update_event = ActivationUpdateEvent(update_activation_to);
event.accept(); 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) { void Widget::dispatch_event(Event &event) {
@ -222,8 +237,12 @@ void Widget::dispatch_event(Event &event) {
handle_mouse_button_event(reinterpret_cast<MouseButtonEvent&>(event)); handle_mouse_button_event(reinterpret_cast<MouseButtonEvent&>(event));
break; break;
} }
case EventType::Reflow: { case EventType::RepaintRect: {
handle_reflow(reinterpret_cast<ReflowEvent&>(event)); handle_repaint_rect(reinterpret_cast<RepaintRectEvent&>(event));
break;
}
case EventType::RelayoutSubtree: {
handle_relayout_subtree();
break; break;
} }
/* these events aren't handled here, as they won't be dispatched to us from other places */ /* these events aren't handled here, as they won't be dispatched to us from other places */

View file

@ -59,13 +59,14 @@ public:
void remove_child(std::shared_ptr<Widget> child); void remove_child(std::shared_ptr<Widget> child);
Box &current_geometry() { return m_current_geometry; } Box &current_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) { m_current_geometry = current_geometry; reflow(); }
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, bool is_pure) { m_current_geometry = current_geometry; if (!is_pure) { reflow(); } }
Point window_relative();
WidgetType type() { return m_type; } WidgetType type() { return m_type; }
ControlWidgetType control_type() { return m_control_type; } ControlWidgetType control_type() { return m_control_type; }
Widget *parent() { return m_parent; } Widget *parent() { return m_parent; }
void set_parent(Widget *parent) { m_parent = parent; } void set_parent(Widget *parent) { m_parent = parent; }
@ -121,11 +122,12 @@ protected:
virtual void on_activation_update(ActivationUpdateEvent &event) {} virtual void on_activation_update(ActivationUpdateEvent &event) {}
virtual void on_paint() {} virtual void on_paint() {}
void wants_full_repaint(); void repaint();
void wants_full_relayout(); void reflow();
private: private:
void do_layout(); 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_move_event(MouseMoveEvent &event);
void handle_mouse_button_event(MouseButtonEvent &event); void handle_mouse_button_event(MouseButtonEvent &event);

View file

@ -64,55 +64,57 @@ bool Window::dispatch_to_main_widget(Event &event) {
if (!m_main_widget) if (!m_main_widget)
return false; 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); m_main_widget->dispatch_event(event);
return true; return true;
} }
void Window::reflow(Widget *target, ReflowType type) { void Window::repaint(Box geometry) {
if (type == ReflowType::RelayoutRect) { m_did_repaint_during_batch = true;
m_did_relayout_during_batch = true;
} else if (type == ReflowType::RepaintRect) {
m_did_repaint_during_batch = true;
}
if (m_is_batching) { if (m_is_batching) {
return; 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); target->dispatch_event(event);
} }
void Window::reflow(ReflowType type) { void Window::relayout() {
if (type == ReflowType::RelayoutRect) { m_did_relayout_during_batch = true;
m_did_relayout_during_batch = true;
} else if (type == ReflowType::RepaintRect) {
m_did_repaint_during_batch = true;
}
if (m_is_batching) { if (m_is_batching) {
return; return;
} }
auto event = ReflowEvent(type, ReflowGrouping::Yes, m_current_geometry); auto event = RelayoutSubtreeEvent();
dispatch_to_main_widget(event); dispatch_to_main_widget(event);
} }
void Window::reflow(Box &box, ReflowType type) { void Window::reflow() {
if (type == ReflowType::RelayoutRect) { relayout();
m_did_relayout_during_batch = true; repaint();
} 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::start_batch() { void Window::start_batch() {
@ -125,9 +127,9 @@ void Window::end_batch() {
if (m_is_batching) { if (m_is_batching) {
m_is_batching = false; m_is_batching = false;
if (m_did_relayout_during_batch) { if (m_did_relayout_during_batch) {
reflow(ReflowType::RelayoutRect); reflow();
} else if (m_did_repaint_during_batch) { } else if (m_did_repaint_during_batch) {
reflow(ReflowType::RelayoutRect); repaint();
} }
} }
} }
@ -161,7 +163,7 @@ void Window::run(bool block) {
} }
case Expose: { case Expose: {
if (e.xexpose.count == 0) { if (e.xexpose.count == 0) {
reflow(ReflowType::RelayoutRect); reflow();
} }
break; break;
} }

View file

@ -36,9 +36,12 @@ public:
std::shared_ptr<TopLevelStyles> top_level_styles() { return m_top_level_styles; } std::shared_ptr<TopLevelStyles> top_level_styles() { return m_top_level_styles; }
void reflow(Widget *target, ReflowType type); void repaint();
void reflow(Box &box, ReflowType type); void repaint(Box geometry);
void reflow(ReflowType type); void repaint(Widget *target);
void relayout(Widget *target);
void relayout();
void reflow();
Box &current_geometry() { return m_current_geometry; } Box &current_geometry() { return m_current_geometry; }

View file

@ -5,6 +5,7 @@
#include "Box.hpp" #include "Box.hpp"
#include "Label.hpp" #include "Label.hpp"
#include "Layout.hpp" #include "Layout.hpp"
#include "RGB.hpp"
#include "src/DocumentLayout.hpp" #include "src/DocumentLayout.hpp"
#include "src/Events.hpp" #include "src/Events.hpp"
#include <iostream> #include <iostream>
@ -17,9 +18,13 @@ int main() {
auto main_widget = window.set_main_widget<Raven::Widget>(); auto main_widget = window.set_main_widget<Raven::Widget>();
auto inner_widget = main_widget->add<Raven::Widget>(); auto second_widget = main_widget->add<Raven::Widget>();
second_widget->set_layout<Raven::DocumentLayout>(16.0);
second_widget->resize(800, 800);
auto inner_widget = second_widget->add<Raven::Widget>();
inner_widget->set_layout<Raven::DocumentLayout>(8.0); inner_widget->set_layout<Raven::DocumentLayout>(8.0);
inner_widget->resize(400, 400); inner_widget->resize(600, 600);
int number = 0; int number = 0;