diff --git a/.gitignore b/.gitignore index e2be6ae..dceeaad 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ builddir/ +.cache/ +compile_commands.json diff --git a/src/Box.cpp b/src/Box.cpp index 9e67b73..ed791f1 100644 --- a/src/Box.cpp +++ b/src/Box.cpp @@ -1,4 +1,5 @@ #include "Box.hpp" +#include "Point.hpp" namespace Raven { @@ -6,6 +7,10 @@ bool Box::contains_point(double x, double y) { return x >= m_x && m_x + m_width >= x && y >= m_y && m_y + m_height >= y; } +bool Box::contains_point(Point &point) { + return point.get_x() >= m_x && m_x + m_width >= point.get_x() && point.get_y() >= m_y && m_y + m_height >= point.get_y(); +} + bool Box::contains_box(Box &other) const { double ax1 = m_x; double ax2 = m_x + m_width; diff --git a/src/Box.hpp b/src/Box.hpp index 30fc523..f36e953 100644 --- a/src/Box.hpp +++ b/src/Box.hpp @@ -1,5 +1,7 @@ #pragma once +#include "Point.hpp" + namespace Raven { class Box { @@ -30,6 +32,7 @@ public: void set_height(double height) { m_height = height; } bool contains_point(double x, double y); + bool contains_point(Point &point); bool contains_box(Box &other) const; }; diff --git a/src/Button.cpp b/src/Button.cpp index c245c26..cb84e62 100644 --- a/src/Button.cpp +++ b/src/Button.cpp @@ -1,16 +1,35 @@ +#include #include "Button.hpp" #include "Box.hpp" #include "Window.hpp" +#include "src/Painter.hpp" +#include namespace Raven { +void Button::on_init() { + set_style_do_background_fill(true); + set_style_background_fill_color(m_style_normal_background_fill_color); + set_style_font_description(get_top_level_styles()->get_style_controls_font_description()); + + set_did_init(true); +} + void Button::on_paint() { auto painter = get_window()->get_painter(); - auto cr = painter.get_cairo(); - cr->set_source_rgb(0.866, 0.713, 0.949); - painter.rounded_rectangle(get_current_geometry(), 8); - cr->fill(); + painter.source_rgb(m_style_text_fill_color); + painter.set_pango_font_description(m_style_font_description); + painter.text(get_current_geometry(), m_text, PaintTextAlign::Center); + painter.fill(); +} + +void Button::on_focus_update(FocusUpdateEvent &event) { + if (is_focused()) { + set_style_background_fill_color(m_style_focused_background_fill_color); + } else { + set_style_background_fill_color(m_style_normal_background_fill_color); + } } } diff --git a/src/Button.hpp b/src/Button.hpp index 8f1c6c6..655a49b 100644 --- a/src/Button.hpp +++ b/src/Button.hpp @@ -1,22 +1,35 @@ #include #include "Widget.hpp" +#include "RGB.hpp" +#include "pango/pango-font.h" +#include "Window.hpp" #pragma once namespace Raven { class Button : public Widget { +DEF_STYLE(text_fill_color, RGB, 0.0, 0.0, 0.0) + +DEF_STYLE(normal_background_fill_color, RGB, 0.6941, 0.3843, 0.5254) +DEF_STYLE(focused_background_fill_color, RGB, 0.5607, 0.2470, 0.4431) +DEF_STYLE(active_background_fill_color, RGB, 0.896, 0.743, 0.979) + +DEF_STYLE(font_description, PangoFontDescription*, nullptr) + private: std::string m_text; public: Button(std::string text) - : m_text(text) - , Widget() {} + : Widget() + , m_text(text) {} void set_text(std::string text) { m_text = text; wants_repaint(); } std::string &get_text() { return m_text; } void on_paint(); + void on_init(); + void on_focus_update(FocusUpdateEvent &event); }; } diff --git a/src/Events.hpp b/src/Events.hpp index 1e3c50d..e2d8564 100644 --- a/src/Events.hpp +++ b/src/Events.hpp @@ -11,7 +11,9 @@ enum class EventType { MouseButton, MouseMove, - WidgetRepaintRequested + WidgetRepaintRequested, + FocusUpdate, + ActivationUpdate }; class Event { @@ -20,11 +22,12 @@ private: public: Event() {} - void accept() { m_accepted = true; } - bool is_accepted() { return m_accepted; } virtual EventType get_type() { return EventType::NoneEvent; } virtual const char *get_name() { return "NoneEvent"; } + void accept() { m_accepted = true; } + bool get_accepted() { return m_accepted; } + virtual ~Event() = default; }; @@ -70,4 +73,30 @@ public: Box &get_repaint_area() { return m_repaint_area; } }; +class FocusUpdateEvent : public Event { +private: + bool m_focus_status; +public: + FocusUpdateEvent(bool focus_status) + : m_focus_status(focus_status) {} + + EventType get_type() { return EventType::FocusUpdate; } + const char *get_name() { return "FocusUpdate"; } + + bool get_focus_status() { return m_focus_status; } +}; + +class ActivationUpdateEvent : public Event { +private: + bool m_activation_status; +public: + ActivationUpdateEvent(bool activation_status) + : m_activation_status(activation_status) {} + + EventType get_type() { return EventType::ActivationUpdate; } + const char *get_name() { return "ActivationUpdate"; } + + bool get_activation_status() { return m_activation_status; } +}; + } diff --git a/src/Forward.hpp b/src/Forward.hpp index 759a3c2..8961741 100644 --- a/src/Forward.hpp +++ b/src/Forward.hpp @@ -4,6 +4,7 @@ namespace Raven { class Events; class Painter; class Point; + class TopLevelStyles; class Widget; class Window; diff --git a/src/Painter.cpp b/src/Painter.cpp index ffa6f54..a682c47 100644 --- a/src/Painter.cpp +++ b/src/Painter.cpp @@ -1,4 +1,5 @@ #include "Painter.hpp" +#include "src/RGB.hpp" namespace Raven { @@ -19,4 +20,60 @@ void Painter::rounded_rectangle(Box &geometry, double border_radius) { m_cairo->close_path(); } +bool Painter::text(Point &where, std::string &text) { + if (m_pango_font_description == nullptr) + return false; + + PangoLayout *layout = pango_cairo_create_layout(m_cairo->cobj()); + + pango_layout_set_font_description(layout, m_pango_font_description); + pango_layout_set_text(layout, text.c_str(), -1); + + m_cairo->move_to(where.get_x(), where.get_y()); + pango_cairo_show_layout(m_cairo->cobj(), layout); + + g_object_unref(layout); + + return true; +} + +bool Painter::text(Box &geometry, std::string &text, PaintTextAlign align) { + if (m_pango_font_description == nullptr) + return false; + + PangoLayout *layout = pango_cairo_create_layout(m_cairo->cobj()); + + int font_width; + int font_height; + + pango_layout_set_font_description(layout, m_pango_font_description); + pango_layout_set_text(layout, text.c_str(), -1); + + pango_layout_get_pixel_size(layout, &font_width, &font_height); + + double x = -1; + double y = geometry.get_y() + ((geometry.get_height() - font_height) / 2); + + if (align == PaintTextAlign::Center) { + x = geometry.get_x() + ((geometry.get_width() - font_width) / 2); + } else { + x = 0; + } + + m_cairo->move_to(x, y); + pango_cairo_show_layout(m_cairo->cobj(), layout); + + g_object_unref(layout); + + return true; +} + +void Painter::source_rgb(RGB &source_rgb) { + m_cairo->set_source_rgb(source_rgb.get_r(), source_rgb.get_g(), source_rgb.get_b()); +} + +void Painter::fill() { + m_cairo->fill(); +} + } diff --git a/src/Painter.hpp b/src/Painter.hpp index a6658e8..6f53882 100644 --- a/src/Painter.hpp +++ b/src/Painter.hpp @@ -1,20 +1,37 @@ #pragma once #include "Box.hpp" +#include "Point.hpp" +#include "src/RGB.hpp" #include +#include namespace Raven { +enum class PaintTextAlign { + Center = 0, + Left +}; + class Painter { private: Cairo::RefPtr m_cairo; + PangoFontDescription *m_pango_font_description; public: Painter() {} Cairo::RefPtr get_cairo() { return m_cairo; } void set_cairo(Cairo::RefPtr cairo) { m_cairo = cairo; } + void set_pango_font_description(PangoFontDescription *pango_font_description) { m_pango_font_description = pango_font_description; } + PangoFontDescription *get_pango_font_description() { return m_pango_font_description; } + void rounded_rectangle(Box &geometry, double border_radius); + bool text(Point &where, std::string &text); + bool text(Box &geometry, std::string &text, PaintTextAlign align); + + void source_rgb(RGB &source_rgb); + void fill(); }; } diff --git a/src/RGB.hpp b/src/RGB.hpp new file mode 100644 index 0000000..66ea5b4 --- /dev/null +++ b/src/RGB.hpp @@ -0,0 +1,26 @@ +#pragma once + +namespace Raven { + +class RGB { +private: + double m_r {0.0}; + double m_g {0.0}; + double m_b {0.0}; +public: + RGB() {} + RGB(double r, double g, double b) + : m_r(r) + , m_g(g) + , m_b(b) {} + + void set_r(double r) { m_r = r; } + void set_g(double g) { m_g = g; } + void set_b(double b) { m_b = b; } + + double get_r() { return m_r; } + double get_g() { return m_g; } + double get_b() { return m_b; } +}; + +} \ No newline at end of file diff --git a/src/StyleMacros.hpp b/src/StyleMacros.hpp new file mode 100644 index 0000000..fcacc7f --- /dev/null +++ b/src/StyleMacros.hpp @@ -0,0 +1,9 @@ +#pragma once + +#define DEF_STYLE(name, type, ...) \ + private: \ + type m_style_##name {__VA_ARGS__}; \ + public: \ + void set_style_##name(type new_style_value) { m_style_##name = new_style_value; wants_repaint(); } \ + type get_style_##name() { return m_style_##name; } \ + private: diff --git a/src/TopLevelStyles.cpp b/src/TopLevelStyles.cpp new file mode 100644 index 0000000..ae8fff7 --- /dev/null +++ b/src/TopLevelStyles.cpp @@ -0,0 +1,12 @@ +#include "TopLevelStyles.hpp" +#include "Window.hpp" + +namespace Raven { + +void TopLevelStyles::wants_repaint() { + // when one of the styles here changes, we will dispatch + // a full repaint to the entire window :^) + m_window->dispatch_full_repaint(); +} + +} diff --git a/src/TopLevelStyles.hpp b/src/TopLevelStyles.hpp new file mode 100644 index 0000000..0dbf330 --- /dev/null +++ b/src/TopLevelStyles.hpp @@ -0,0 +1,40 @@ +#pragma once + +#include +#include "Forward.hpp" +#include "pango/pango-font.h" +#include "StyleMacros.hpp" + +namespace Raven { + +class TopLevelStyles { +DEF_STYLE(controls_font_description, PangoFontDescription*, nullptr) + +private: + Window *m_window; +public: + TopLevelStyles(Window *window) + : m_window(window) + { + PangoFontDescription *font_description = pango_font_description_new(); + + pango_font_description_set_family(font_description, "sans-serif"); + pango_font_description_set_weight(font_description, PANGO_WEIGHT_NORMAL); + pango_font_description_set_absolute_size(font_description, 16 * PANGO_SCALE); + + m_style_controls_font_description = font_description; + } + + ~TopLevelStyles() { + if (m_style_controls_font_description) { + pango_font_description_free(m_style_controls_font_description); + } + } + + Window *get_window() { return m_window; } + void set_window(Window *window) { m_window = window; } + + void wants_repaint(); +}; + +} diff --git a/src/Widget.cpp b/src/Widget.cpp index f223960..92e55b6 100644 --- a/src/Widget.cpp +++ b/src/Widget.cpp @@ -6,6 +6,13 @@ namespace Raven { +void Widget::set_window(Window *window) { + m_window = window; + m_top_level_styles = m_window->get_top_level_styles(); + + on_init(); +} + void Widget::wants_repaint() { if (!m_window) return; @@ -29,72 +36,111 @@ void Widget::remove_child(Widget *child) { m_children.erase(std::remove(m_children.begin(), m_children.end(), child), m_children.end()); } +void Widget::do_generic_paint() { + if (m_style_do_background_fill) { + auto painter = m_window->get_painter(); + auto cr = painter.get_cairo(); + + cr->save(); + + painter.source_rgb(m_style_background_fill_color); + painter.rounded_rectangle(m_current_geometry, m_style_background_border_radius); + painter.fill(); + + cr->restore(); + } +} + void Widget::handle_repaint_requested(WidgetRepaintRequestedEvent &event) { - std::cout << "-------\n"; - - std::cout << "-- repaint_area: " - << event.get_repaint_area().get_x() - << ", " - << event.get_repaint_area().get_y() - << ", " - << event.get_repaint_area().get_width() - << ", " - << event.get_repaint_area().get_height() - << " --\n"; - - std::cout << "-- m_current_geometry: " - << m_current_geometry.get_x() - << ", " - << m_current_geometry.get_y() - << ", " - << m_current_geometry.get_width() - << ", " - << m_current_geometry.get_height() - << " --\n"; - - std::cout << "-------\n"; + if (!m_did_init) + return; // widgets contain all of thier children inside their geometry // we will check if the event's repaint area (the area that needs to be repainted :)) // overlaps with our geometry. If it does, we will proceed with the repaint process if (!event.get_repaint_area().contains_box(m_current_geometry)) { + event.accept(); // consume this event so that it won't bubble down to the other children return; } - std::cout << "--- PASSED\n"; + // before calling this widget's paint function, we + // will perform generic widget painting behavior (background color, etc) + do_generic_paint(); - on_paint(); // we're going to call this widget's paint function + // we're going to call this widget's paint handler function + // we will also save and then restore the cairo context + // to prevent the state (e.g. source rgb) for leaking onto other widgets + auto painter = m_window->get_painter(); + auto cr = painter.get_cairo(); - // tell all children about this event - // the repaint area matching the geometry is checked at the start of - // this function, which means they'll repaint as well if it is needed - for (auto& child : m_children) { - child->dispatch_event(event); + cr->save(); + on_paint(); + cr->restore(); + + // we are not going to call accept() on this event as we want it to bubble down + // to all the other relevant children +} + +void Widget::handle_mouse_move_event(MouseMoveEvent &event) { + bool update_focus_to = true; + + if (!m_current_geometry.contains_point(event.get_point())) { + // we just became unfocused + if (m_is_focused) { + update_focus_to = false; + } else { + return; + } } + + m_is_focused = update_focus_to; + auto focus_update_event = FocusUpdateEvent(update_focus_to); + on_mouse_move(event); + on_focus_update(focus_update_event); + + if (m_consumes_hits) + event.accept(); +} + +void Widget::handle_mouse_button_event(MouseButtonEvent &event) { + // the mouse has updated its click state in our bounds + m_is_active = event.get_was_left_button_pressed(); + + auto activation_update_event = ActivationUpdateEvent(m_is_active); + on_mouse_button(event); + on_activation_update(activation_update_event); + + if (m_consumes_hits) + event.accept(); } void Widget::dispatch_event(Event &event) { - process_event(event); -} - -void Widget::process_event(Event &event) { switch (event.get_type()) { case EventType::MouseMove: { - on_mouse_move(reinterpret_cast(event)); + handle_mouse_move_event(reinterpret_cast(event)); break; } case EventType::MouseButton: { - on_mouse_button(reinterpret_cast(event)); + handle_mouse_button_event(reinterpret_cast(event)); break; } case EventType::WidgetRepaintRequested: { handle_repaint_requested(reinterpret_cast(event)); break; } + /* these events aren't handled here, as they won't be dispatched to us from other places */ + case EventType::FocusUpdate: + case EventType::ActivationUpdate: case EventType::NoneEvent: { break; } } + + if (!event.get_accepted()) { + for (auto& child : m_children) { + child->dispatch_event(event); + } + } } } \ No newline at end of file diff --git a/src/Widget.hpp b/src/Widget.hpp index f2d3df7..dc86b2d 100644 --- a/src/Widget.hpp +++ b/src/Widget.hpp @@ -1,18 +1,34 @@ #pragma once +#include #include +#include #include "Box.hpp" #include "Events.hpp" #include "Forward.hpp" +#include "RGB.hpp" +#include "TopLevelStyles.hpp" +#include "StyleMacros.hpp" namespace Raven { class Widget { + +DEF_STYLE(do_background_fill, bool, false) +DEF_STYLE(background_fill_color, RGB, 0, 0, 0) +DEF_STYLE(background_border_radius, double, 0.0) + + private: Box m_current_geometry {}; std::vector m_children; Widget *m_parent { nullptr }; Window *m_window { nullptr }; + std::shared_ptr m_top_level_styles = nullptr; + bool m_did_init { false }; + bool m_is_focused { false }; + bool m_is_active { false }; + bool m_consumes_hits { false }; public: Widget() {} @@ -27,21 +43,39 @@ public: void set_parent(Widget *parent) { m_parent = parent; } Window *get_window() { return m_window; } - void set_window(Window *window) { m_window = window; } + void set_window(Window *window); + + std::shared_ptr get_top_level_styles() { return m_top_level_styles; } + void set_top_level_styles(std::shared_ptr top_level_styles) { m_top_level_styles = top_level_styles; } + + void set_did_init(bool did_init) { m_did_init = did_init; } + bool get_did_init() { return m_did_init; } + + bool is_focused() { return m_is_focused; } + + bool is_active() { return m_is_active; } + + bool get_consumes_hits() { return m_consumes_hits; } + void set_consumes_hits(bool consumes_hits) { m_consumes_hits = consumes_hits; } void dispatch_event(Event &event); void wants_repaint(); - Widget *walk_until_top_level(); - virtual void on_mouse_button(MouseButtonEvent &event) {}; - virtual void on_mouse_move(MouseMoveEvent &event) {}; - virtual void on_paint() {}; + virtual void on_init() { set_did_init(true); } + virtual void on_mouse_button(MouseButtonEvent &event) {} + virtual void on_mouse_move(MouseMoveEvent &event) {} + virtual void on_focus_update(FocusUpdateEvent &event) {} + virtual void on_activation_update(ActivationUpdateEvent &event) {} + virtual void on_paint() {} + virtual ~Widget() {}; private: - void process_event(Event &event); void handle_repaint_requested(WidgetRepaintRequestedEvent &event); + void do_generic_paint(); + void handle_mouse_move_event(MouseMoveEvent &event); + void handle_mouse_button_event(MouseButtonEvent &event); }; } \ No newline at end of file diff --git a/src/Window.cpp b/src/Window.cpp index 6b0131d..562aa05 100644 --- a/src/Window.cpp +++ b/src/Window.cpp @@ -9,6 +9,7 @@ #include "Window.hpp" #include "Events.hpp" +#include "src/Point.hpp" namespace Raven { @@ -45,17 +46,21 @@ bool Window::spawn_window() { return true; } -void Window::dispatch_repaint_on_box(Box box) { +bool Window::dispatch_to_main_widget(Event &event) { if (!m_main_widget) - return; - - auto event = WidgetRepaintRequestedEvent(std::move(box)); + return false; m_main_widget->dispatch_event(event); + return true; } -void Window::dispatch_full_repaint() { - dispatch_repaint_on_box(m_current_geometry); +bool Window::dispatch_repaint_on_box(Box box) { + auto event = WidgetRepaintRequestedEvent(std::move(box)); + return dispatch_to_main_widget(event); +} + +bool Window::dispatch_full_repaint() { + return dispatch_repaint_on_box(m_current_geometry); } void Window::run(bool block) { @@ -68,13 +73,23 @@ void Window::run(bool block) { break; switch (e.type) { - case MapNotify: - case Expose: - std::cout << "gotta repaint!\n"; + case MapNotify: { dispatch_full_repaint(); break; - default: + } + case Expose: { + auto box = Box(e.xexpose.x, e.xexpose.y, e.xexpose.width, e.xexpose.height); + dispatch_repaint_on_box(std::move(box)); break; + } + case MotionNotify: { + auto point = Point(e.xmotion.x, e.xmotion.y); + auto event = MouseMoveEvent(std::move(point)); + dispatch_to_main_widget(event); + } + default: { + break; + } } } } diff --git a/src/Window.hpp b/src/Window.hpp index ef77a06..36f093e 100644 --- a/src/Window.hpp +++ b/src/Window.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include "Widget.hpp" #include "Painter.hpp" #include @@ -12,6 +13,7 @@ private: Widget *m_main_widget { nullptr }; Box m_current_geometry { 0, 0, 800, 600 }; Painter m_painter {}; + std::shared_ptr m_top_level_styles = std::make_shared(this); Display *m_x_display { nullptr }; public: @@ -25,9 +27,13 @@ public: Widget *get_main_widget() { return m_main_widget; } void set_main_widget(Widget *main_widget); + // TODO: add setter for top_level_styles + std::shared_ptr get_top_level_styles() { return m_top_level_styles; } - void dispatch_repaint_on_box(Box box); - void dispatch_full_repaint(); + + bool dispatch_repaint_on_box(Box box); + bool dispatch_full_repaint(); + bool dispatch_to_main_widget(Event &event); Box &get_current_geometry() { return m_current_geometry; } diff --git a/src/main.cpp b/src/main.cpp index ca3c660..f1122df 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -6,12 +6,18 @@ int main() { Raven::Window window {}; + Raven::Widget main_widget {}; Raven::Button button {"click me!"}; window.spawn_window(); button.set_current_geometry(Raven::Box(10, 10, 100, 30)); - window.set_main_widget(&button); + main_widget.set_current_geometry(window.get_current_geometry()); + main_widget.set_style_background_fill_color(Raven::RGB(0.9764, 0.9607, 0.8431)); + main_widget.set_style_do_background_fill(true); + window.set_main_widget(&main_widget); + + main_widget.add_child(&button); window.run(true); return 0;