From e178428742182aea8caf2d7ac0f06fa3545b74a0 Mon Sep 17 00:00:00 2001 From: hippoz <10706925-hippoz@users.noreply.gitlab.com> Date: Sun, 16 Oct 2022 22:52:23 +0300 Subject: [PATCH] add basic TextInput class with support for minimal text editing --- meson.build | 1 + src/Events.hpp | 37 +++++++++++++ src/Painter.cpp | 16 +++++- src/Painter.hpp | 2 +- src/Styles.cpp | 22 ++++++++ src/Styles.hpp | 2 + src/TextInput.cpp | 132 ++++++++++++++++++++++++++++++++++++++++++++++ src/TextInput.hpp | 26 +++++++++ src/Widget.cpp | 15 +++++- src/Widget.hpp | 6 +++ src/Window.cpp | 32 ++++++++++- src/Window.hpp | 2 + src/main.cpp | 49 +++++++++++++---- 13 files changed, 326 insertions(+), 16 deletions(-) create mode 100644 src/TextInput.cpp create mode 100644 src/TextInput.hpp diff --git a/meson.build b/meson.build index 500c12c..ddbbf42 100644 --- a/meson.build +++ b/meson.build @@ -37,6 +37,7 @@ raven_source_files = [ './src/BoxLayout.cpp', './src/Label.cpp', './src/ListView.cpp', + './src/TextInput.cpp', ] raven_header_files = [ diff --git a/src/Events.hpp b/src/Events.hpp index bcc3743..81d757e 100644 --- a/src/Events.hpp +++ b/src/Events.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include "Point.hpp" #include "Box.hpp" @@ -15,6 +16,7 @@ enum class EventType { RepaintRect, FocusUpdate, ActivationUpdate, + Key }; class Event { @@ -27,6 +29,41 @@ public: virtual ~Event() = default; }; +class KeyEvent : public Event { +public: + using RavenKeySym = KeySym; + + enum class KeyStatus { + HasNone = 0, + HasKeySym, + HasKeyChars, + HasBoth + }; + + KeyEvent(RavenKeySym key_sym, std::string key_chars, KeyStatus status, bool control_pressed, bool shift_pressed) + : Event() + , m_key_sym(key_sym) + , m_key_chars(key_chars) + , m_status(status) + , m_control_pressed(control_pressed) + , m_shift_pressed(shift_pressed) {} + + EventType type() { return EventType::Key; } + const char *name() { return "Key"; } + + RavenKeySym key_sym() { return m_key_sym; } + std::string &key_chars() { return m_key_chars; } + KeyStatus status() { return m_status; } + bool control_pressed() { return m_control_pressed; } + bool shift_pressed() { return m_shift_pressed; } +private: + RavenKeySym m_key_sym {}; + std::string m_key_chars {}; + KeyStatus m_status { KeyStatus::HasNone }; + bool m_control_pressed { false }; + bool m_shift_pressed { false }; +}; + class MouseButtonEvent : public Event { private: bool m_was_left_button_pressed; diff --git a/src/Painter.cpp b/src/Painter.cpp index 775b3e6..7027488 100644 --- a/src/Painter.cpp +++ b/src/Painter.cpp @@ -4,6 +4,7 @@ #include "pango/pango-layout.h" #include "pango/pango-types.h" #include "pango/pangocairo.h" +#include "Box.hpp" namespace Raven { @@ -49,7 +50,7 @@ Point Painter::compute_text_size(Box &widget_geometry, std::string &text, PangoF return { pango_units_to_double(font_width), pango_units_to_double(font_height) }; } -void Painter::text(Box &geometry, std::string &text, PaintTextAlign align, PangoEllipsizeMode ellipsize, PangoFontDescription *pango_font_description) { +void Painter::text(Box &geometry, std::string &text, PaintTextAlign align, PangoEllipsizeMode ellipsize, PangoFontDescription *pango_font_description, int cursor_pos) { PangoLayout *layout = pango_cairo_create_layout(m_cairo->cobj()); int font_width; @@ -79,6 +80,19 @@ void Painter::text(Box &geometry, std::string &text, PaintTextAlign align, Pango } pango_cairo_show_layout(m_cairo->cobj(), layout); + if (cursor_pos >= 0) { + PangoRectangle pango_cursor_rect; + pango_layout_get_cursor_pos(layout, cursor_pos, &pango_cursor_rect, NULL); + Box cursor_rect = { + static_cast(pango_units_to_double(pango_cursor_rect.x)), + pango_units_to_double(pango_cursor_rect.y) + y, + 1.0, + static_cast(pango_units_to_double(pango_cursor_rect.height)) + }; + + m_cairo->rectangle(cursor_rect.x(), cursor_rect.y(), cursor_rect.width(), cursor_rect.height()); + } + g_object_unref(layout); } diff --git a/src/Painter.hpp b/src/Painter.hpp index b862104..fe930c2 100644 --- a/src/Painter.hpp +++ b/src/Painter.hpp @@ -25,7 +25,7 @@ public: void rounded_rectangle(Box &geometry, double border_radius); Point compute_text_size(Box &widget_geometry, std::string &text, PangoFontDescription *pango_font_description); - void text(Box &geometry, std::string &text, PaintTextAlign align, PangoEllipsizeMode ellipsize, PangoFontDescription *pango_font_description); + void text(Box &geometry, std::string &text, PaintTextAlign align, PangoEllipsizeMode ellipsize, PangoFontDescription *pango_font_description, int cursor_pos = -1); bool can_paint() { if (m_cairo) return true; else return false; } diff --git a/src/Styles.cpp b/src/Styles.cpp index b6bd79e..87874cc 100644 --- a/src/Styles.cpp +++ b/src/Styles.cpp @@ -124,4 +124,26 @@ GenericStyle flat_listview_style { false }; +GenericStyle flat_textinput_style { + pango_font_description_from_string("sans-serif"), + black1, + white2, + white2, + white1, + 6.0, + true, + true +}; + +GenericStyle raised_textinput_style { + pango_font_description_from_string("sans-serif"), + black1, + white1, + white1, + white0, + 6.0, + true, + true +}; + } diff --git a/src/Styles.hpp b/src/Styles.hpp index e6b652d..2522814 100644 --- a/src/Styles.hpp +++ b/src/Styles.hpp @@ -33,5 +33,7 @@ extern GenericStyle accent_button_style; extern GenericStyle flat_label_style; extern GenericStyle flat_listview_style; extern GenericStyle raised_listview_style; +extern GenericStyle flat_textinput_style; +extern GenericStyle raised_textinput_style; } diff --git a/src/TextInput.cpp b/src/TextInput.cpp new file mode 100644 index 0000000..8e7e1df --- /dev/null +++ b/src/TextInput.cpp @@ -0,0 +1,132 @@ +#include "TextInput.hpp" +#include "Events.hpp" +#include "Logging.hpp" +#include "Painter.hpp" +#include "src/Styles.hpp" +#include +#include +#include + +namespace Raven { + +void TextInput::set_text(std::string text) { + m_text = text; + m_cursor = m_text.length(); + + // We won't use the fit_text() function here, since reflowing is a little slower than repainting. + // It's better for text input to feel snappier, thus we will make the assumption that all text input + // widgets will not scale themselves to fit the text inside. We will rely on the parent layout or + // the user of the library explicitly sizing this widget. + + repaint(); +} + +void TextInput::on_init() { + set_remains_active(true); + set_style_pure(&flat_textinput_style); + + set_did_init(true); + if (!fit_text(m_text)) { + reflow(); + } + m_cursor = m_text.length(); +} + +void TextInput::on_paint() { + auto painter = window()->painter(); + auto geometry = rect().max_geometry(); + + int displayed_cursor_pos = m_cursor; + if (!is_active()) { + displayed_cursor_pos = -1; + } + + painter.source_rgb(style()->foreground()); + painter.text(geometry, m_text, Raven::PaintTextAlign::Left, PANGO_ELLIPSIZE_END, style()->font_description(), displayed_cursor_pos); + painter.fill(); +} + +void TextInput::text_did_change() { + repaint(); +} + +void TextInput::insert(std::string chars) { + m_text.insert(m_cursor, chars); + m_cursor++; + text_did_change(); +} + +void TextInput::on_key(KeyEvent &event) { + INFO << "on_key: " << event.key_chars() << std::endl; + switch (event.status()) { + case KeyEvent::KeyStatus::HasKeyChars: { + insert(event.key_chars()); + break; + } + case KeyEvent::KeyStatus::HasBoth: /* fallthrough */ + case KeyEvent::KeyStatus::HasKeySym: { + auto key_sym = event.key_sym(); + + if (event.control_pressed()) { + switch (key_sym) { + case XK_a: key_sym = XK_Home; break; + case XK_e: key_sym = XK_End; break; + case XK_f: key_sym = XK_Right; break; + case XK_b: key_sym = XK_Left; break; + default: { + return; + } + } + } + + switch (key_sym) { + default: { + // FIXME + if (!iscntrl((unsigned char)*event.key_chars().c_str())) { + insert(event.key_chars()); + } + break; + } + case XK_BackSpace: { + if (!m_cursor) return; + m_cursor--; + m_text.erase(m_cursor, 1); + text_did_change(); + break; + } + case XK_KP_Left: + case XK_Left: { + m_cursor = std::max(m_cursor - 1, 0); + text_did_change(); + break; + } + case XK_KP_Right: + case XK_Right: { + m_cursor = std::min(m_cursor + 1, static_cast(m_text.length())); + text_did_change(); + break; + } + case XK_Home: + case XK_KP_Home: { + m_cursor = 0; + text_did_change(); + break; + } + case XK_End: + case XK_KP_End: { + m_cursor = m_text.length(); + text_did_change(); + break; + } + } + } + + case KeyEvent::KeyStatus::HasNone: + default: { + return; + } + } +} + + +} \ No newline at end of file diff --git a/src/TextInput.hpp b/src/TextInput.hpp new file mode 100644 index 0000000..2b323e4 --- /dev/null +++ b/src/TextInput.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include "Widget.hpp" + +namespace Raven { + +class TextInput : public Raven::Widget { +public: + TextInput() + : Raven::Widget() {} + + std::string &text() { return m_text; } + void set_text(std::string text); +protected: + void on_init() override; + void on_paint() override; + void on_key(KeyEvent &event) override; +private: + void insert(std::string chars); + void text_did_change(); + + std::string m_text {""}; + int m_cursor { 0 }; +}; + +} diff --git a/src/Widget.cpp b/src/Widget.cpp index 71e55e0..256a7e0 100644 --- a/src/Widget.cpp +++ b/src/Widget.cpp @@ -244,17 +244,20 @@ void Widget::handle_mouse_move_event(MouseMoveEvent &event) { void Widget::handle_mouse_button_event(MouseButtonEvent &event) { bool update_activation_to = event.was_left_button_pressed(); + if (!update_activation_to && m_remains_active) { + update_activation_to = true; + } + if (!m_rect.contains(event.point())) { update_activation_to = false; } on_mouse_button(event); - if (m_is_active != update_activation_to) { + if (m_is_active != update_activation_to || (update_activation_to && m_window->active_widget() != this)) { m_is_active = update_activation_to; if (m_is_active && m_window) m_window->set_active_widget(this); - auto activation_update_event = ActivationUpdateEvent(update_activation_to); on_activation_update(activation_update_event); @@ -279,6 +282,10 @@ void Widget::handle_mouse_button_event(MouseButtonEvent &event) { } } +void Widget::handle_key_event(KeyEvent &event) { + on_key(event); +} + void Widget::dispatch_event(Event &event) { if (!m_accepts_events || !m_did_init) return; @@ -302,6 +309,10 @@ void Widget::dispatch_event(Event &event) { handle_relayout_subtree(reinterpret_cast(event)); break; } + case EventType::Key: { + handle_key_event(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: diff --git a/src/Widget.hpp b/src/Widget.hpp index eeb37f4..956a194 100644 --- a/src/Widget.hpp +++ b/src/Widget.hpp @@ -89,6 +89,9 @@ public: bool accepts_events() { return m_accepts_events; } void set_accepts_events(bool accepts_events) { m_accepts_events = accepts_events; } + bool remains_active() { return m_remains_active; } + void set_remains_active(bool remains_active) { m_remains_active = remains_active; } + bool absolute() { return m_absolute; } void set_absolute(bool absolute) { m_absolute = absolute; } @@ -124,6 +127,7 @@ protected: virtual void on_paint() {} virtual void on_layout() {} virtual void on_after_layout() {} + virtual void on_key(KeyEvent &event) {} void set_did_init(bool did_init) { m_did_init = did_init; } Point compute_window_relative(); @@ -135,6 +139,7 @@ private: void handle_relayout_subtree(RelayoutSubtreeEvent &event); void handle_mouse_move_event(MouseMoveEvent &event); void handle_mouse_button_event(MouseButtonEvent &event); + void handle_key_event(KeyEvent &event); Point m_window_relative { 0, 0 }; Box m_rect { 0, 0, 0, 0 }; @@ -150,6 +155,7 @@ private: bool m_accepts_events { true }; bool m_absolute { false }; bool m_grows { false }; + bool m_remains_active { false }; ControlWidgetType m_control_type { ControlWidgetType::Widget }; }; diff --git a/src/Window.cpp b/src/Window.cpp index 2848599..9620a4e 100644 --- a/src/Window.cpp +++ b/src/Window.cpp @@ -22,9 +22,8 @@ void Window::set_main_widget(std::shared_ptr main_widget) { bool Window::spawn_window() { Display *dsp = XOpenDisplay(NULL); - XSynchronize(dsp, False); if (dsp == NULL) { - std::cerr << "error: XOpenDisplay(NULL)" << "\n"; + std::cerr << "error: XOpenDisplay failed" << "\n"; return false; } m_x_display = dsp; @@ -46,6 +45,14 @@ bool Window::spawn_window() { ); XSelectInput(dsp, da, ButtonPressMask | ButtonReleaseMask | KeyPressMask | PointerMotionMask | StructureNotifyMask | ExposureMask); + + if ((m_xim = XOpenIM(dsp, NULL, NULL, NULL)) == NULL) { + std::cerr << "error: XOpenIM failed" << "\n"; + return false; + } + + m_xic = XCreateIC(m_xim, XNInputStyle, XIMPreeditNothing | XIMStatusNothing, XNClientWindow, da, XNFocusWindow, da, NULL); + XMapWindow(dsp, da); m_xlib_surface = Cairo::XlibSurface::create( @@ -214,6 +221,27 @@ void Window::run(bool block) { auto point = Point(e.xmotion.x, e.xmotion.y); auto event = MouseMoveEvent(point); dispatch_to_main_widget(event); + break; + } + case KeyPress: { + if (m_active_widget) { + KeySym keysym; + char chars[32]; + Status status; + + XmbLookupString(m_xic, &e.xkey, chars, sizeof(chars), &keysym, &status); + + Raven::KeyEvent::KeyStatus key_status = Raven::KeyEvent::KeyStatus::HasNone; + switch (status) { + case XLookupChars: key_status = Raven::KeyEvent::KeyStatus::HasKeyChars; break; + case XLookupKeySym: key_status = Raven::KeyEvent::KeyStatus::HasKeySym; break; + case XLookupBoth: key_status = Raven::KeyEvent::KeyStatus::HasBoth; break; + default: key_status = Raven::KeyEvent::KeyStatus::HasNone; break; + } + auto event = KeyEvent(keysym, chars, key_status, e.xkey.state & ControlMask, e.xkey.state & ShiftMask); + m_active_widget->dispatch_event(event); + } + break; } default: { break; diff --git a/src/Window.hpp b/src/Window.hpp index 9ca6599..ce11525 100644 --- a/src/Window.hpp +++ b/src/Window.hpp @@ -66,6 +66,8 @@ private: std::queue> m_microtasks; Display *m_x_display { nullptr }; + XIM m_xim { nullptr }; + XIC m_xic { nullptr }; bool dispatch_to_main_widget(Event &event); }; diff --git a/src/main.cpp b/src/main.cpp index da79380..f11642c 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,6 +1,7 @@ #include "Label.hpp" #include "ColumnLayout.hpp" #include "ListView.hpp" +#include "TextInput.hpp" #include "Window.hpp" #include "Widget.hpp" #include "Button.hpp" @@ -40,16 +41,44 @@ int main() { auto selected_label = content->add("No selection"); selected_label->rect().set_max_height(28.0); - auto next_button = content->add("Next Item", Raven::Button::Accent); - - auto delete_button = content->add("Delete Item"); - delete_button->set_style(&Raven::raised_button_style); + auto control_row = content->add(); + control_row->set_style(&Raven::raised_widget_style); + control_row->rect().set_min_height(28.0); + auto control_row_layout = control_row->set_layout(Raven::Direction::Horizontal); + control_row_layout->set_spacing(6.0); - delete_button->on_click = [list_view, &window]() { - window.queue_microtask([list_view](){ - list_view->elements.erase(list_view->elements.begin() + list_view->active_element()); - list_view->set_active_element(list_view->active_element() - 1); - }); + auto next_button = control_row->add("Next Item", Raven::Button::Accent); + next_button->set_grows(true); + + auto delete_button = control_row->add("Delete Item"); + delete_button->set_style(&Raven::raised_button_style); + delete_button->set_grows(true); + + content->add("Edit an item"); + + auto edit_row = content->add(); + edit_row->set_style(&Raven::raised_widget_style); + edit_row->rect().set_min_height(28.0); + auto edit_row_layout = edit_row->set_layout(Raven::Direction::Horizontal); + edit_row_layout->set_spacing(6.0); + + auto edit_input = edit_row->add(); + edit_input->set_style(&Raven::raised_textinput_style); + edit_input->set_grows(true); + + auto edit_button = edit_row->add("Edit"); + edit_button->set_style(&Raven::accent_button_style); + + edit_button->on_click = [list_view, edit_input]() { + if (list_view->active_element() >= 0) { + list_view->elements[list_view->active_element()] = edit_input->text(); + list_view->elements_updated(); + } + }; + + delete_button->on_click = [list_view]() { + list_view->elements.erase(list_view->elements.begin() + list_view->active_element()); + list_view->set_active_element(list_view->active_element() - 1); }; next_button->on_click = [list_view]() { @@ -57,7 +86,7 @@ int main() { }; list_view->on_selection = [selected_label](unsigned int index, std::string item) { - selected_label->set_text("You have selected: " + item); + selected_label->set_text("Selected: " + item); }; int i = 1000;