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
No known key found for this signature in database
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() {
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);

View file

@ -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;

View file

@ -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;

View file

@ -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();

View file

@ -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:

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_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();
};
}

View file

@ -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<Widget> 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<Widget> 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;
}
}
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_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 != 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<MouseButtonEvent&>(event));
break;
}
case EventType::Reflow: {
handle_reflow(reinterpret_cast<ReflowEvent&>(event));
case EventType::RepaintRect: {
handle_repaint_rect(reinterpret_cast<RepaintRectEvent&>(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 */

View file

@ -59,13 +59,14 @@ public:
void remove_child(std::shared_ptr<Widget> child);
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, 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);

View file

@ -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) {
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) {
void Window::relayout() {
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, 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;
}

View file

@ -36,9 +36,12 @@ public:
std::shared_ptr<TopLevelStyles> 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 &current_geometry() { return m_current_geometry; }

View file

@ -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 <iostream>
@ -17,9 +18,13 @@ int main() {
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->resize(400, 400);
inner_widget->resize(600, 600);
int number = 0;