add basic TextInput class with support for minimal text editing

This commit is contained in:
hippoz 2022-10-16 22:52:23 +03:00
parent 9ef5cbab9b
commit e178428742
No known key found for this signature in database
GPG key ID: 7C52899193467641
13 changed files with 326 additions and 16 deletions

View file

@ -37,6 +37,7 @@ raven_source_files = [
'./src/BoxLayout.cpp', './src/BoxLayout.cpp',
'./src/Label.cpp', './src/Label.cpp',
'./src/ListView.cpp', './src/ListView.cpp',
'./src/TextInput.cpp',
] ]
raven_header_files = [ raven_header_files = [

View file

@ -1,5 +1,6 @@
#pragma once #pragma once
#include <X11/X.h>
#include <stdint.h> #include <stdint.h>
#include "Point.hpp" #include "Point.hpp"
#include "Box.hpp" #include "Box.hpp"
@ -15,6 +16,7 @@ enum class EventType {
RepaintRect, RepaintRect,
FocusUpdate, FocusUpdate,
ActivationUpdate, ActivationUpdate,
Key
}; };
class Event { class Event {
@ -27,6 +29,41 @@ public:
virtual ~Event() = default; 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 { class MouseButtonEvent : public Event {
private: private:
bool m_was_left_button_pressed; bool m_was_left_button_pressed;

View file

@ -4,6 +4,7 @@
#include "pango/pango-layout.h" #include "pango/pango-layout.h"
#include "pango/pango-types.h" #include "pango/pango-types.h"
#include "pango/pangocairo.h" #include "pango/pangocairo.h"
#include "Box.hpp"
namespace Raven { 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) }; 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()); PangoLayout *layout = pango_cairo_create_layout(m_cairo->cobj());
int font_width; 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); 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<double>(pango_units_to_double(pango_cursor_rect.x)),
pango_units_to_double(pango_cursor_rect.y) + y,
1.0,
static_cast<double>(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); g_object_unref(layout);
} }

View file

@ -25,7 +25,7 @@ public:
void rounded_rectangle(Box &geometry, double border_radius); void rounded_rectangle(Box &geometry, double border_radius);
Point compute_text_size(Box &widget_geometry, std::string &text, PangoFontDescription *pango_font_description); 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; } bool can_paint() { if (m_cairo) return true; else return false; }

View file

@ -124,4 +124,26 @@ GenericStyle flat_listview_style {
false 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
};
} }

View file

@ -33,5 +33,7 @@ extern GenericStyle accent_button_style;
extern GenericStyle flat_label_style; extern GenericStyle flat_label_style;
extern GenericStyle flat_listview_style; extern GenericStyle flat_listview_style;
extern GenericStyle raised_listview_style; extern GenericStyle raised_listview_style;
extern GenericStyle flat_textinput_style;
extern GenericStyle raised_textinput_style;
} }

132
src/TextInput.cpp Normal file
View file

@ -0,0 +1,132 @@
#include "TextInput.hpp"
#include "Events.hpp"
#include "Logging.hpp"
#include "Painter.hpp"
#include "src/Styles.hpp"
#include <cctype>
#include <string>
#include <locale>
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<int>(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;
}
}
}
}

26
src/TextInput.hpp Normal file
View file

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

View file

@ -244,17 +244,20 @@ void Widget::handle_mouse_move_event(MouseMoveEvent &event) {
void Widget::handle_mouse_button_event(MouseButtonEvent &event) { void Widget::handle_mouse_button_event(MouseButtonEvent &event) {
bool update_activation_to = event.was_left_button_pressed(); 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())) { if (!m_rect.contains(event.point())) {
update_activation_to = false; update_activation_to = false;
} }
on_mouse_button(event); 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; m_is_active = update_activation_to;
if (m_is_active && m_window) if (m_is_active && m_window)
m_window->set_active_widget(this); m_window->set_active_widget(this);
auto activation_update_event = ActivationUpdateEvent(update_activation_to); auto activation_update_event = ActivationUpdateEvent(update_activation_to);
on_activation_update(activation_update_event); 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) { void Widget::dispatch_event(Event &event) {
if (!m_accepts_events || !m_did_init) if (!m_accepts_events || !m_did_init)
return; return;
@ -302,6 +309,10 @@ void Widget::dispatch_event(Event &event) {
handle_relayout_subtree(reinterpret_cast<RelayoutSubtreeEvent&>(event)); handle_relayout_subtree(reinterpret_cast<RelayoutSubtreeEvent&>(event));
break; break;
} }
case EventType::Key: {
handle_key_event(reinterpret_cast<KeyEvent&>(event));
break;
}
/* these events aren't handled here, as they won't be dispatched to us from other places */ /* these events aren't handled here, as they won't be dispatched to us from other places */
case EventType::FocusUpdate: case EventType::FocusUpdate:
case EventType::ActivationUpdate: case EventType::ActivationUpdate:

View file

@ -89,6 +89,9 @@ public:
bool accepts_events() { return m_accepts_events; } bool accepts_events() { return m_accepts_events; }
void set_accepts_events(bool accepts_events) { m_accepts_events = 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; } bool absolute() { return m_absolute; }
void set_absolute(bool absolute) { m_absolute = absolute; } void set_absolute(bool absolute) { m_absolute = absolute; }
@ -124,6 +127,7 @@ protected:
virtual void on_paint() {} virtual void on_paint() {}
virtual void on_layout() {} virtual void on_layout() {}
virtual void on_after_layout() {} virtual void on_after_layout() {}
virtual void on_key(KeyEvent &event) {}
void set_did_init(bool did_init) { m_did_init = did_init; } void set_did_init(bool did_init) { m_did_init = did_init; }
Point compute_window_relative(); Point compute_window_relative();
@ -135,6 +139,7 @@ private:
void handle_relayout_subtree(RelayoutSubtreeEvent &event); void handle_relayout_subtree(RelayoutSubtreeEvent &event);
void handle_mouse_move_event(MouseMoveEvent &event); void handle_mouse_move_event(MouseMoveEvent &event);
void handle_mouse_button_event(MouseButtonEvent &event); void handle_mouse_button_event(MouseButtonEvent &event);
void handle_key_event(KeyEvent &event);
Point m_window_relative { 0, 0 }; Point m_window_relative { 0, 0 };
Box m_rect { 0, 0, 0, 0 }; Box m_rect { 0, 0, 0, 0 };
@ -150,6 +155,7 @@ private:
bool m_accepts_events { true }; bool m_accepts_events { true };
bool m_absolute { false }; bool m_absolute { false };
bool m_grows { false }; bool m_grows { false };
bool m_remains_active { false };
ControlWidgetType m_control_type { ControlWidgetType::Widget }; ControlWidgetType m_control_type { ControlWidgetType::Widget };
}; };

View file

@ -22,9 +22,8 @@ void Window::set_main_widget(std::shared_ptr<Widget> main_widget) {
bool Window::spawn_window() { bool Window::spawn_window() {
Display *dsp = XOpenDisplay(NULL); Display *dsp = XOpenDisplay(NULL);
XSynchronize(dsp, False);
if (dsp == NULL) { if (dsp == NULL) {
std::cerr << "error: XOpenDisplay(NULL)" << "\n"; std::cerr << "error: XOpenDisplay failed" << "\n";
return false; return false;
} }
m_x_display = dsp; m_x_display = dsp;
@ -46,6 +45,14 @@ bool Window::spawn_window() {
); );
XSelectInput(dsp, da, ButtonPressMask | ButtonReleaseMask | KeyPressMask | PointerMotionMask | StructureNotifyMask | ExposureMask); 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); XMapWindow(dsp, da);
m_xlib_surface = Cairo::XlibSurface::create( 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 point = Point(e.xmotion.x, e.xmotion.y);
auto event = MouseMoveEvent(point); auto event = MouseMoveEvent(point);
dispatch_to_main_widget(event); 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: { default: {
break; break;

View file

@ -66,6 +66,8 @@ private:
std::queue<std::function<void()>> m_microtasks; std::queue<std::function<void()>> m_microtasks;
Display *m_x_display { nullptr }; Display *m_x_display { nullptr };
XIM m_xim { nullptr };
XIC m_xic { nullptr };
bool dispatch_to_main_widget(Event &event); bool dispatch_to_main_widget(Event &event);
}; };

View file

@ -1,6 +1,7 @@
#include "Label.hpp" #include "Label.hpp"
#include "ColumnLayout.hpp" #include "ColumnLayout.hpp"
#include "ListView.hpp" #include "ListView.hpp"
#include "TextInput.hpp"
#include "Window.hpp" #include "Window.hpp"
#include "Widget.hpp" #include "Widget.hpp"
#include "Button.hpp" #include "Button.hpp"
@ -40,16 +41,44 @@ int main() {
auto selected_label = content->add<Raven::Label>("No selection"); auto selected_label = content->add<Raven::Label>("No selection");
selected_label->rect().set_max_height(28.0); selected_label->rect().set_max_height(28.0);
auto next_button = content->add<Raven::Button>("Next Item", Raven::Button::Accent); auto control_row = content->add<Raven::Widget>();
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::BoxLayout>(Raven::Direction::Horizontal);
control_row_layout->set_spacing(6.0);
auto delete_button = content->add<Raven::Button>("Delete Item"); auto next_button = control_row->add<Raven::Button>("Next Item", Raven::Button::Accent);
next_button->set_grows(true);
auto delete_button = control_row->add<Raven::Button>("Delete Item");
delete_button->set_style(&Raven::raised_button_style); delete_button->set_style(&Raven::raised_button_style);
delete_button->set_grows(true);
delete_button->on_click = [list_view, &window]() { content->add<Raven::Label>("Edit an item");
window.queue_microtask([list_view](){
list_view->elements.erase(list_view->elements.begin() + list_view->active_element()); auto edit_row = content->add<Raven::Widget>();
list_view->set_active_element(list_view->active_element() - 1); 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::BoxLayout>(Raven::Direction::Horizontal);
edit_row_layout->set_spacing(6.0);
auto edit_input = edit_row->add<Raven::TextInput>();
edit_input->set_style(&Raven::raised_textinput_style);
edit_input->set_grows(true);
auto edit_button = edit_row->add<Raven::Button>("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]() { next_button->on_click = [list_view]() {
@ -57,7 +86,7 @@ int main() {
}; };
list_view->on_selection = [selected_label](unsigned int index, std::string item) { 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; int i = 1000;