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:
parent
f14d88c776
commit
94c0be051b
11 changed files with 164 additions and 136 deletions
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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();
|
||||
};
|
||||
|
||||
}
|
||||
|
|
147
src/Widget.cpp
147
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<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;
|
||||
}
|
||||
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<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 */
|
||||
|
|
|
@ -59,13 +59,14 @@ public:
|
|||
void remove_child(std::shared_ptr<Widget> 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);
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 ¤t_geometry() { return m_current_geometry; }
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
Loading…
Reference in a new issue