add basic TextInput class with support for minimal text editing
This commit is contained in:
parent
9ef5cbab9b
commit
e178428742
13 changed files with 326 additions and 16 deletions
|
@ -37,6 +37,7 @@ raven_source_files = [
|
|||
'./src/BoxLayout.cpp',
|
||||
'./src/Label.cpp',
|
||||
'./src/ListView.cpp',
|
||||
'./src/TextInput.cpp',
|
||||
]
|
||||
|
||||
raven_header_files = [
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
#pragma once
|
||||
|
||||
#include <X11/X.h>
|
||||
#include <stdint.h>
|
||||
#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;
|
||||
|
|
|
@ -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<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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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; }
|
||||
|
||||
|
|
|
@ -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
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
}
|
||||
|
|
132
src/TextInput.cpp
Normal file
132
src/TextInput.cpp
Normal 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
26
src/TextInput.hpp
Normal 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 };
|
||||
};
|
||||
|
||||
}
|
|
@ -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<RelayoutSubtreeEvent&>(event));
|
||||
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 */
|
||||
case EventType::FocusUpdate:
|
||||
case EventType::ActivationUpdate:
|
||||
|
|
|
@ -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 };
|
||||
};
|
||||
|
||||
|
|
|
@ -22,9 +22,8 @@ void Window::set_main_widget(std::shared_ptr<Widget> 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;
|
||||
|
|
|
@ -66,6 +66,8 @@ private:
|
|||
std::queue<std::function<void()>> m_microtasks;
|
||||
|
||||
Display *m_x_display { nullptr };
|
||||
XIM m_xim { nullptr };
|
||||
XIC m_xic { nullptr };
|
||||
|
||||
bool dispatch_to_main_widget(Event &event);
|
||||
};
|
||||
|
|
49
src/main.cpp
49
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<Raven::Label>("No selection");
|
||||
selected_label->rect().set_max_height(28.0);
|
||||
|
||||
auto next_button = content->add<Raven::Button>("Next Item", Raven::Button::Accent);
|
||||
|
||||
auto delete_button = content->add<Raven::Button>("Delete Item");
|
||||
delete_button->set_style(&Raven::raised_button_style);
|
||||
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);
|
||||
|
||||
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<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_grows(true);
|
||||
|
||||
content->add<Raven::Label>("Edit an item");
|
||||
|
||||
auto edit_row = content->add<Raven::Widget>();
|
||||
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]() {
|
||||
|
@ -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;
|
||||
|
|
Loading…
Reference in a new issue