Browse Source

LibGUI: Start adding an automatic widget layout system.

My needs are really quite simple, so I'm just going to add what I need
as I go along. The first thing I needed was a simple box layout with
widgets being able to say whether they prefer fixed or fill for both
their vertical and horizontal sizes.

I also made a simple GStatusBar so FileManager can show how many bytes
worth of files are in the current directory.
Andreas Kling 6 years ago
parent
commit
2def3d8d3f

+ 1 - 1
Applications/Clock/Makefile

@@ -23,7 +23,7 @@ LDFLAGS = -static --strip-debug -melf_i386 -e _start --gc-sections
 all: $(APP)
 
 $(APP): $(OBJS)
-	$(LD) -o $(APP) $(LDFLAGS) $(OBJS) ../LibGUI/LibGUI.a ../LibC/LibC.a
+	$(LD) -o $(APP) $(LDFLAGS) $(OBJS) ../../LibGUI/LibGUI.a ../../LibC/LibC.a
 
 .cpp.o:
 	@echo "CXX $<"; $(CXX) $(CXXFLAGS) -o $@ -c $<

+ 20 - 1
Applications/FileManager/DirectoryView.cpp

@@ -53,6 +53,8 @@ void DirectoryView::reload()
     }
     m_directories.clear();
     m_files.clear();
+
+    size_t bytes_in_files = 0;
     while (auto* de = readdir(dirp)) {
         Entry entry;
         entry.name = de->d_name;
@@ -66,10 +68,21 @@ void DirectoryView::reload()
         entry.mode = st.st_mode;
         auto& entries = S_ISDIR(st.st_mode) ? m_directories : m_files;
         entries.append(move(entry));
+
+        if (S_ISREG(entry.mode))
+            bytes_in_files += st.st_size;
     }
     closedir(dirp);
     int excess_height = max(0, (item_count() * item_height()) - height());
     m_scrollbar->set_range(0, excess_height);
+
+
+
+    set_status_message(String::format("%d item%s (%u byte%s)",
+                                      item_count(),
+                                      item_count() != 1 ? "s" : "",
+                                      bytes_in_files,
+                                      bytes_in_files != 1 ? "s" : ""));
 }
 
 const GraphicsBitmap& DirectoryView::icon_for(const Entry& entry) const
@@ -128,7 +141,7 @@ void DirectoryView::paint_event(GPaintEvent&)
             Rect icon_rect(horizontal_padding, y, icon_size, item_height());
             Rect name_rect(icon_rect.right() + horizontal_padding, y, 100, item_height());
             Rect size_rect(name_rect.right() + horizontal_padding, y, 64, item_height());
-            painter.fill_rect(row_rect(painted_item_index), i % 2 ? Color::LightGray : Color::White);
+            painter.fill_rect(row_rect(painted_item_index), i % 2 ? Color(210, 210, 210) : Color::White);
             painter.blit_with_alpha(icon_rect.location(), icon_for(entry), { 0, 0, icon_size, icon_size });
             painter.draw_text(name_rect, entry.name, Painter::TextAlignment::CenterLeft, Color::Black);
             if (should_show_size_for(entry))
@@ -143,3 +156,9 @@ void DirectoryView::paint_event(GPaintEvent&)
     unpainted_rect.intersect(rect());
     painter.fill_rect(unpainted_rect, Color::White);
 }
+
+void DirectoryView::set_status_message(String&& message)
+{
+    if (on_status_message)
+        on_status_message(move(message));
+}

+ 3 - 0
Applications/FileManager/DirectoryView.h

@@ -15,6 +15,7 @@ public:
     void reload();
 
     Function<void(const String&)> on_path_change;
+    Function<void(String)> on_status_message;
 
     int item_height() const { return 16; }
     int item_count() const { return m_directories.size() + m_files.size(); }
@@ -24,6 +25,8 @@ private:
     virtual void resize_event(GResizeEvent&) override;
     virtual void mousedown_event(GMouseEvent&) override;
 
+    void set_status_message(String&&);
+
     Rect row_rect(int item_index) const;
 
     struct Entry {

+ 1 - 1
Applications/FileManager/Makefile

@@ -23,7 +23,7 @@ LDFLAGS = -static --strip-debug -melf_i386 -e _start --gc-sections
 all: $(APP)
 
 $(APP): $(OBJS)
-	$(LD) -o $(APP) $(LDFLAGS) $(OBJS) ../LibGUI/LibGUI.a ../LibC/LibC.a
+	$(LD) -o $(APP) $(LDFLAGS) $(OBJS) ../../LibGUI/LibGUI.a ../../LibC/LibC.a
 
 .cpp.o:
 	@echo "CXX $<"; $(CXX) $(CXXFLAGS) -o $@ -c $<

+ 11 - 7
Applications/FileManager/main.cpp

@@ -1,14 +1,10 @@
-#include <SharedGraphics/GraphicsBitmap.h>
 #include <LibGUI/GWindow.h>
 #include <LibGUI/GWidget.h>
-#include <LibGUI/GButton.h>
-#include <LibGUI/GListBox.h>
+#include <LibGUI/GBoxLayout.h>
 #include <LibGUI/GEventLoop.h>
-#include <sys/wait.h>
-#include <signal.h>
+#include <LibGUI/GStatusBar.h>
 #include <unistd.h>
 #include <stdio.h>
-#include <errno.h>
 #include "DirectoryView.h"
 
 static GWindow* make_window();
@@ -33,13 +29,21 @@ GWindow* make_window()
     auto* widget = new GWidget;
     window->set_main_widget(widget);
 
+    widget->set_layout(make<GBoxLayout>(Orientation::Vertical));
+
     auto* directory_view = new DirectoryView(widget);
-    directory_view->set_relative_rect({ 0, 0, 240, 300 });
+
+    auto* statusbar = new GStatusBar(widget);
+    statusbar->set_text("Welcome!");
 
     directory_view->on_path_change = [window] (const String& new_path) {
         window->set_title(String::format("FileManager: %s", new_path.characters()));
     };
 
+    directory_view->on_status_message = [statusbar] (String message) {
+        statusbar->set_text(move(message));
+    };
+
     directory_view->open("/");
 
     return window;

+ 1 - 1
Applications/FontEditor/Makefile

@@ -23,7 +23,7 @@ LDFLAGS = -static --strip-debug -melf_i386 -e _start --gc-sections
 all: $(APP)
 
 $(APP): $(OBJS)
-	$(LD) -o $(APP) $(LDFLAGS) $(OBJS) ../LibGUI/LibGUI.a ../LibC/LibC.a
+	$(LD) -o $(APP) $(LDFLAGS) $(OBJS) ../../LibGUI/LibGUI.a ../../LibC/LibC.a
 
 .cpp.o:
 	@echo "CXX $<"; $(CXX) $(CXXFLAGS) -o $@ -c $<

+ 1 - 1
Applications/Launcher/Makefile

@@ -22,7 +22,7 @@ LDFLAGS = -static --strip-debug -melf_i386 -e _start --gc-sections
 all: $(APP)
 
 $(APP): $(OBJS)
-	$(LD) -o $(APP) $(LDFLAGS) $(OBJS) ../LibGUI/LibGUI.a ../LibC/LibC.a
+	$(LD) -o $(APP) $(LDFLAGS) $(OBJS) ../../LibGUI/LibGUI.a ../../LibC/LibC.a
 
 .cpp.o:
 	@echo "CXX $<"; $(CXX) $(CXXFLAGS) -o $@ -c $<

+ 1 - 1
Applications/Terminal/Makefile

@@ -23,7 +23,7 @@ LDFLAGS = -static --strip-debug -melf_i386 -e _start --gc-sections
 all: $(APP)
 
 $(APP): $(OBJS)
-	$(LD) -o $(APP) $(LDFLAGS) $(OBJS) ../LibC/LibC.a
+	$(LD) -o $(APP) $(LDFLAGS) $(OBJS) ../../LibC/LibC.a
 
 .cpp.o:
 	@echo "CXX $<"; $(CXX) $(CXXFLAGS) -o $@ -c $<

+ 10 - 10
Kernel/makeall.sh

@@ -8,16 +8,16 @@ make -C ../LibGUI clean && \
 make -C ../LibGUI && \
 make -C ../Userland clean && \
 make -C ../Userland && \
-make -C ../Application/Terminal clean && \
-make -C ../Application/Terminal && \
-make -C ../Application/FontEditor clean && \
-make -C ../Application/FontEditor && \
-make -C ../Application/Clock clean && \
-make -C ../Application/Clock && \
-make -C ../Application/Launcher clean && \
-make -C ../Application/Launcher && \
-make -C ../Application/FileManager clean && \
-make -C ../Application/FileManager && \
+make -C ../Applications/Terminal clean && \
+make -C ../Applications/Terminal && \
+make -C ../Applications/FontEditor clean && \
+make -C ../Applications/FontEditor && \
+make -C ../Applications/Clock clean && \
+make -C ../Applications/Clock && \
+make -C ../Applications/Launcher clean && \
+make -C ../Applications/Launcher && \
+make -C ../Applications/FileManager clean && \
+make -C ../Applications/FileManager && \
 make clean &&\
 make && \
 sudo ./sync.sh

+ 6 - 0
LibC/entry.cpp

@@ -38,3 +38,9 @@ epilogue:
     // Birger's birthday <3
     return 20150614;
 }
+
+extern "C" void __cxa_pure_virtual() NORETURN;
+extern "C" void __cxa_pure_virtual()
+{
+    ASSERT_NOT_REACHED();
+}

+ 89 - 0
LibGUI/GBoxLayout.cpp

@@ -0,0 +1,89 @@
+#include <LibGUI/GBoxLayout.h>
+#include <LibGUI/GWidget.h>
+
+GBoxLayout::GBoxLayout(Orientation orientation)
+    : m_orientation(orientation)
+{
+}
+
+GBoxLayout::~GBoxLayout()
+{
+}
+
+#if 0
+Size GLayout::compute_preferred_size() const
+{
+
+}
+
+
+static Size compute_preferred_size(GLayout::Entry& entry)
+{
+    if (entry.layout)
+        return entry.layout->compute_preferred_size();
+    else {
+        return entry.widget->preferred_size();
+    }
+}
+#endif
+
+void GBoxLayout::run(GWidget& widget)
+{
+    if (m_entries.is_empty())
+        return;
+
+    Size available_size = widget.size();
+    int number_of_entries_with_fixed_size = 0;
+
+    for (auto& entry : m_entries) {
+        if (entry.widget && entry.widget->size_policy(orientation()) == SizePolicy::Fixed) {
+            available_size -= entry.widget->preferred_size();
+            ++number_of_entries_with_fixed_size;
+        }
+    }
+
+    int number_of_entries_with_automatic_size = m_entries.size() - number_of_entries_with_fixed_size;
+
+    dbgprintf("GBoxLayout: available_size=%d, fixed=%d, fill=%d\n", available_size.height(), number_of_entries_with_fixed_size, number_of_entries_with_automatic_size);
+
+    Size automatic_size;
+
+    if (m_orientation == Orientation::Horizontal) {
+        automatic_size.set_width(available_size.width() / number_of_entries_with_automatic_size);
+        automatic_size.set_height(widget.height());
+    } else {
+        automatic_size.set_width(widget.width());
+        automatic_size.set_height(available_size.height() / number_of_entries_with_automatic_size);
+    }
+
+    dbgprintf("GBoxLayout: automatic_size=%s\n", automatic_size.to_string().characters());
+
+    int current_x = 0;
+    int current_y = 0;
+ for (auto& entry : m_entries) {
+        Rect rect(current_x, current_y, 0, 0);
+        if (entry.layout) {
+            // FIXME: Implement recursive layout.
+            ASSERT_NOT_REACHED();
+        }
+        ASSERT(entry.widget);
+        if (entry.widget->size_policy(orientation()) == SizePolicy::Fixed) {
+            rect.set_size(automatic_size);
+            if (orientation() == Orientation::Vertical) {
+                rect.set_height(entry.widget->preferred_size().height());
+            } else {
+                rect.set_width(entry.widget->preferred_size().height());
+            }
+        } else {
+            rect.set_size(automatic_size);
+        }
+
+        dbgprintf("GBoxLayout: apply, %s{%p} <- %s\n", entry.widget->class_name(), entry.widget.ptr(), rect.to_string().characters());
+        entry.widget->set_relative_rect(rect);
+
+        if (orientation() == Orientation::Horizontal)
+            current_x += rect.width();
+        else
+            current_y += rect.height();
+    }
+}

+ 18 - 0
LibGUI/GBoxLayout.h

@@ -0,0 +1,18 @@
+#pragma once
+
+#include <LibGUI/GLayout.h>
+#include <LibGUI/GWidget.h>
+
+class GBoxLayout final : public GLayout {
+public:
+    explicit GBoxLayout(Orientation);
+    virtual ~GBoxLayout() override;
+
+    Orientation orientation() const { return m_orientation; }
+
+    virtual void run(GWidget&) override;
+
+private:
+    Orientation m_orientation;
+};
+

+ 41 - 0
LibGUI/GLayout.cpp

@@ -0,0 +1,41 @@
+#include <LibGUI/GLayout.h>
+#include <LibGUI/GWidget.h>
+
+GLayout::GLayout()
+{
+}
+
+GLayout::~GLayout()
+{
+}
+
+void GLayout::notify_adopted(Badge<GWidget>, GWidget& widget)
+{
+    if (m_owner.ptr() == &widget)
+        return;
+    m_owner = widget.make_weak_ptr();
+}
+
+void GLayout::notify_disowned(Badge<GWidget>, GWidget& widget)
+{
+    ASSERT(m_owner.ptr() == &widget);
+    m_owner.clear();
+}
+
+void GLayout::add_layout(OwnPtr<GLayout>&& layout)
+{
+    Entry entry;
+    entry.layout = move(layout);
+    m_entries.append(move(entry));
+    if (m_owner)
+        m_owner->notify_layout_changed(Badge<GLayout>());
+}
+
+void GLayout::add_widget(GWidget& widget)
+{
+    Entry entry;
+    entry.widget = widget.make_weak_ptr();
+    m_entries.append(move(entry));
+    if (m_owner)
+        m_owner->notify_layout_changed(Badge<GLayout>());
+}

+ 31 - 0
LibGUI/GLayout.h

@@ -0,0 +1,31 @@
+#pragma once
+
+#include <AK/Badge.h>
+#include <AK/OwnPtr.h>
+#include <AK/Vector.h>
+#include <AK/WeakPtr.h>
+
+class GWidget;
+
+class GLayout {
+public:
+    GLayout();
+    virtual ~GLayout();
+
+    void add_widget(GWidget&);
+    void add_layout(OwnPtr<GLayout>&&);
+
+    virtual void run(GWidget&) = 0;
+
+    void notify_adopted(Badge<GWidget>, GWidget&);
+    void notify_disowned(Badge<GWidget>, GWidget&);
+
+protected:
+    struct Entry {
+        WeakPtr<GWidget> widget;
+        OwnPtr<GLayout> layout;
+    };
+    WeakPtr<GWidget> m_owner;
+    Vector<Entry> m_entries;
+};
+

+ 35 - 0
LibGUI/GStatusBar.cpp

@@ -0,0 +1,35 @@
+#include <LibGUI/GStatusBar.h>
+#include <LibGUI/GLabel.h>
+#include <LibGUI/GBoxLayout.h>
+#include <SharedGraphics/Painter.h>
+
+GStatusBar::GStatusBar(GWidget* parent)
+    : GWidget(parent)
+{
+    set_size_policy(SizePolicy::Fill, SizePolicy::Fixed);
+    set_preferred_size({ 0, 16 });
+    set_layout(make<GBoxLayout>(Orientation::Horizontal));
+    m_label = new GLabel(this);
+    m_label->set_fill_with_background_color(false);
+}
+
+GStatusBar::~GStatusBar()
+{
+}
+
+void GStatusBar::set_text(String&& text)
+{
+    m_label->set_text(move(text));
+}
+
+String GStatusBar::text() const
+{
+    return m_label->text();
+}
+
+void GStatusBar::paint_event(GPaintEvent&)
+{
+    Painter painter(*this);
+    painter.fill_rect({ 0, 1, width(), height() - 1 }, Color::LightGray);
+    painter.draw_line({ 0, 0 }, { width() - 1, 0 }, Color::DarkGray);
+}

+ 20 - 0
LibGUI/GStatusBar.h

@@ -0,0 +1,20 @@
+#pragma once
+
+#include <LibGUI/GWidget.h>
+
+class GLabel;
+
+class GStatusBar : public GWidget {
+public:
+    explicit GStatusBar(GWidget* parent);
+    virtual ~GStatusBar() override;
+
+    String text() const;
+    void set_text(String&&);
+
+private:
+    virtual const char* class_name() const override { return "GStatusBar"; }
+    virtual void paint_event(GPaintEvent&) override;
+
+    GLabel* m_label { nullptr };
+};

+ 67 - 1
LibGUI/GWidget.cpp

@@ -2,6 +2,7 @@
 #include "GEvent.h"
 #include "GEventLoop.h"
 #include "GWindow.h"
+#include <LibGUI/GLayout.h>
 #include <AK/Assertions.h>
 #include <SharedGraphics/GraphicsBitmap.h>
 #include <SharedGraphics/Painter.h>
@@ -12,6 +13,9 @@ GWidget::GWidget(GWidget* parent)
     set_font(nullptr);
     m_background_color = Color::LightGray;
     m_foreground_color = Color::Black;
+
+    if (parent && parent->layout())
+        parent->layout()->add_widget(*this);
 }
 
 GWidget::~GWidget()
@@ -42,7 +46,7 @@ void GWidget::event(GEvent& event)
         m_has_pending_paint_event = false;
         return handle_paint_event(static_cast<GPaintEvent&>(event));
     case GEvent::Resize:
-        return resize_event(static_cast<GResizeEvent&>(event));
+        return handle_resize_event(static_cast<GResizeEvent&>(event));
     case GEvent::FocusIn:
         return focusin_event(event);
     case GEvent::FocusOut:
@@ -87,6 +91,41 @@ void GWidget::handle_paint_event(GPaintEvent& event)
     }
 }
 
+void GWidget::set_layout(OwnPtr<GLayout>&& layout)
+{
+    if (m_layout.ptr() == layout.ptr())
+        return;
+    if (m_layout)
+        m_layout->notify_disowned(Badge<GWidget>(), *this);
+    m_layout = move(layout);
+    if (m_layout) {
+        m_layout->notify_adopted(Badge<GWidget>(), *this);
+        do_layout();
+    } else {
+        update();
+    }
+}
+
+void GWidget::do_layout()
+{
+    if (!m_layout)
+        return;
+    m_layout->run(*this);
+    update();
+}
+
+void GWidget::notify_layout_changed(Badge<GLayout>)
+{
+    do_layout();
+}
+
+void GWidget::handle_resize_event(GResizeEvent& event)
+{
+    if (layout())
+        do_layout();
+    return resize_event(event);
+}
+
 void GWidget::resize_event(GResizeEvent&)
 {
 }
@@ -216,3 +255,30 @@ bool GWidget::global_cursor_tracking() const
         return false;
     return win->global_cursor_tracking_widget() == this;
 }
+
+void GWidget::set_preferred_size(const Size& size)
+{
+    if (m_preferred_size == size)
+        return;
+    m_preferred_size = size;
+    invalidate_layout();
+}
+
+void GWidget::set_size_policy(SizePolicy horizontal_policy, SizePolicy vertical_policy)
+{
+    if (m_horizontal_size_policy == horizontal_policy && m_vertical_size_policy == vertical_policy)
+        return;
+    m_horizontal_size_policy = horizontal_policy;
+    m_vertical_size_policy = vertical_policy;
+    invalidate_layout();
+}
+
+void GWidget::invalidate_layout()
+{
+    auto* w = window();
+    if (!w)
+        return;
+    if (!w->main_widget())
+        return;
+    w->main_widget()->do_layout();
+}

+ 26 - 0
LibGUI/GWidget.h

@@ -5,16 +5,32 @@
 #include <SharedGraphics/Rect.h>
 #include <SharedGraphics/Color.h>
 #include <SharedGraphics/Font.h>
+#include <AK/Badge.h>
 #include <AK/AKString.h>
 
 class GraphicsBitmap;
+class GLayout;
 class GWindow;
 
+enum class SizePolicy { Fixed, Fill };
+enum class Orientation { Horizontal, Vertical };
+
 class GWidget : public GObject {
 public:
     explicit GWidget(GWidget* parent = nullptr);
     virtual ~GWidget() override;
 
+    GLayout* layout() { return m_layout.ptr(); }
+    void set_layout(OwnPtr<GLayout>&&);
+
+    SizePolicy horizontal_size_policy() const { return m_horizontal_size_policy; }
+    SizePolicy vertical_size_policy() const { return m_vertical_size_policy; }
+    SizePolicy size_policy(Orientation orientation) { return orientation == Orientation::Horizontal ? m_horizontal_size_policy : m_vertical_size_policy; }
+    void set_size_policy(SizePolicy horizontal_policy, SizePolicy vertical_policy);
+
+    Size preferred_size() const { return m_preferred_size; }
+    void set_preferred_size(const Size&);
+
     virtual void event(GEvent&) override;
     virtual void paint_event(GPaintEvent&);
     virtual void resize_event(GResizeEvent&);
@@ -100,16 +116,26 @@ public:
     void set_global_cursor_tracking(bool);
     bool global_cursor_tracking() const;
 
+    void notify_layout_changed(Badge<GLayout>);
+
 private:
     void handle_paint_event(GPaintEvent&);
+    void handle_resize_event(GResizeEvent&);
+    void do_layout();
+    void invalidate_layout();
 
     GWindow* m_window { nullptr };
+    OwnPtr<GLayout> m_layout;
 
     Rect m_relative_rect;
     Color m_background_color { 0xffffff };
     Color m_foreground_color { 0x000000 };
     RetainPtr<Font> m_font;
 
+    SizePolicy m_horizontal_size_policy { SizePolicy::Fill };
+    SizePolicy m_vertical_size_policy { SizePolicy::Fill };
+    Size m_preferred_size;
+
     bool m_has_pending_paint_event { false };
     bool m_fill_with_background_color { true };
 };

+ 3 - 0
LibGUI/Makefile

@@ -15,8 +15,11 @@ LIBGUI_OBJS = \
     GObject.o \
     GTextBox.o \
     GScrollBar.o \
+    GStatusBar.o \
     GWidget.o \
     GStyle.o \
+    GLayout.o \
+    GBoxLayout.o \
     GWindow.o
 
 OBJS = $(SHAREDGRAPHICS_OBJS) $(LIBGUI_OBJS)

+ 5 - 5
SharedGraphics/Painter.cpp

@@ -4,7 +4,7 @@
 #include <AK/Assertions.h>
 #include <AK/StdLibExtras.h>
 
-#ifdef LIBGUI
+#ifdef USERLAND
 #include <LibGUI/GWidget.h>
 #include <LibGUI/GWindow.h>
 #include <LibC/gui.h>
@@ -20,7 +20,7 @@ Painter::Painter(GraphicsBitmap& bitmap)
     m_clip_rect = { { 0, 0 }, bitmap.size() };
 }
 
-#ifdef LIBGUI
+#ifdef USERLAND
 Painter::Painter(GWidget& widget)
     : m_font(&widget.font())
 {
@@ -34,9 +34,9 @@ Painter::Painter(GWidget& widget)
     m_target = GraphicsBitmap::create_wrapper(backing.size, backing.pixels);
     ASSERT(m_target);
     m_window = widget.window();
-    m_translation.move_by(widget.relative_position());
+    m_translation.move_by(widget.window_relative_rect().location());
     // NOTE: m_clip_rect is in Window coordinates since we are painting into its backing store.
-    m_clip_rect = widget.relative_rect();
+    m_clip_rect = widget.window_relative_rect();
     m_clip_rect.intersect(m_target->rect());
 
 #ifdef DEBUG_WIDGET_UNDERDRAW
@@ -49,7 +49,7 @@ Painter::Painter(GWidget& widget)
 
 Painter::~Painter()
 {
-#ifdef LIBGUI
+#ifdef USERLAND
     m_target = nullptr;
     int rc = gui_release_window_backing_store(m_backing_store_id);
     ASSERT(rc == 0);

+ 10 - 0
SharedGraphics/Rect.h

@@ -52,6 +52,16 @@ public:
         return { x() + width() / 2, y() + height() / 2 };
     }
 
+    void set_location(const Point& location)
+    {
+        m_location = location;
+    }
+
+    void set_size(const Size& size)
+    {
+        m_size = size;
+    }
+
     void inflate(int w, int h)
     {
         set_x(x() - w / 2);

+ 11 - 0
SharedGraphics/Size.h

@@ -1,5 +1,7 @@
 #pragma once
 
+#include <AK/AKString.h>
+
 struct GUI_Size;
 
 class Size {
@@ -29,8 +31,17 @@ public:
         return !(*this == other);
     }
 
+    Size& operator-=(const Size& other)
+    {
+        m_width -= other.m_width;
+        m_height -= other.m_height;
+        return *this;
+    }
+
     operator GUI_Size() const;
 
+    String to_string() const { return String::format("[%d,%d]", m_width, m_height); }
+
 private:
     int m_width { 0 };
     int m_height { 0 };