Compare commits

...

3 commits

Author SHA1 Message Date
Lephenixnoir
4728c6ecbe
jlabel: align top by default and wrap lines in a smarter way
With this change, unless wrapped_newline_spadcing is set (which it's not
by default), spaces around explicit newlines will no longer be skipped.
2024-09-14 12:21:07 +02:00
Lephenixnoir
33e9962209
jlabel: default line spacing to new font_t.line_distance property 2024-09-14 07:19:39 +02:00
Lephenixnoir
57a460894f
the entire new keyboard focus system, save for a few bits
I need to move on to gintctl so that'll be enough for now.
2024-09-13 22:00:29 +02:00
10 changed files with 267 additions and 67 deletions

View file

@ -46,8 +46,9 @@ New keyboard focus system.
b. There is a widget-scene call to give a widget active focus. If the widget
has policy FOCUS_ACCEPT or FOCUS_SCOPE, this call walks up the scope
chain and assigns new targets until it reaches the scene. If the widget
has policy FOCUS_NONE, this call just removes the surrounding scope's
target.
has is NULL, this call just removes the surrounding scope's target.
TODO: If the widget is FOCUS_REJECT, clear scopes from that widget up
until reaching the scene.
X. There is a widget-context call to relinquish one's own focus within the
parent scope. This is intended to differ from 2.b in that scopes might
implement special logic for moving focus to a nearby widget.
@ -73,19 +74,20 @@ New keyboard focus system.
4. Behavior of built-in widgets
a. `jfileselect`, `jinput`, `jlist` have policy FOCUS_ACCEPT
b. `jscene` has policy FOCUS_SCOPE
TODO: What about jframe?
c. `jfkeys` has policy `FOCUS_REJECT` and must be given events manually
d. Widgets with a stack layout will try and give focus (within their
surrounding scope) to the current element. If the element is complex, it
should be a focus scope. Similarly, widgets with invisible children will
move focus around to their direct children as needed to make sure focus
remains on a visible child.
TODO: Implement stack layout logic
5. Implementation
a. Events are `FOCUS_CHANGED` when own FOCUSED and ACTIVE_FOCUSED flags have
changed, and `FOCUS_TARGET_CHANGED` for scopes when the target changes.
NOTES:
- Update focus flags when removing from parent
- Focus events don't propagate but are replicated, which adds some complexity
- Focus scopes could have background focus for jfkeys (left for later). Hard
part is this isn't specified by jfkeys.

View file

@ -67,7 +67,7 @@ typedef struct {
jalign block_valign;
/* Text alignment */
jalign text_align;
/* Pixels of spacing between each line, in addition to font->height */
/* Pixels of spacing between lines, in addition to font->line_distance */
int8_t line_spacing;
/* Text to display */
@ -76,12 +76,16 @@ typedef struct {
of line number n */
DECLARE_VEC(uint16_t, breaks);
/* Block width (maximum length of a rendered line) */
uint16_t block_width;
/* Text wrapping mode */
enum jwrapmode wrap_mode;
/* Whether the text has been allocated by the label or supplied by user */
int8_t owns_text;
/* Block width (maximum length of a rendered line) */
uint16_t block_width;
bool owns_text :1;
/* Whether to preserve spaces around line-wrapping newlines */
bool wrapped_newline_spacing :1;
uint :5;
/* Color and font of text; if NULL, gint's default font is used */
int color;
@ -130,5 +134,6 @@ void jlabel_set_line_spacing(jlabel *l, int line_spacing);
void jlabel_set_wrap_mode(jlabel *l, jwrapmode mode);
void jlabel_set_text_color(jlabel *l, int color);
void jlabel_set_font(jlabel *l, font_t const *font);
void jlabel_set_wrapped_newline_spacing(jlabel *l, bool preserve);
#endif /* _J_JLABEL */

View file

@ -21,8 +21,6 @@ typedef struct {
/* Location on screen */
int16_t x, y;
/* Widget with focus */
jwidget *focus;
/* Circular event queue */
jevent queue[JSCENE_QUEUE_SIZE];

View file

@ -67,6 +67,9 @@ typedef struct jwidget {
jlayout_grid layout_grid;
};
/* Focused subwidget for focus scopes */
struct jwidget *focus_scope_target;
/* Widget type, used to find polymorphic operations */
uint8_t type;
/* Number of children */
@ -91,8 +94,13 @@ typedef struct jwidget {
uint floating :1;
/* Widget is clipped during rendering */
uint clipped :1;
/* Focus policy */
uint focus_policy :2;
/* Focus flags */
uint focused :1;
uint active_focused :1;
uint :22;
uint :18;
} jwidget;
@ -106,6 +114,17 @@ typedef enum {
} jwidget_border_style;
/* jwidget_focus_policy: Options for receiving and handling keyboard focus */
typedef enum {
/* The widget does not accept focus or interact with keyboard (default) */
J_FOCUS_POLICY_REJECT,
/* The widget accepts keyboard input and hides it from descendants */
J_FOCUS_POLICY_ACCEPT,
/* The widget accepts keyboard focus and forwards it to a descendant */
J_FOCUS_POLICY_SCOPE,
} jwidget_focus_policy_t;
/* jwidget_geometry: Built-in positioning and border geometry
Every widget has a "geometry", which consists of a border and two layers of
@ -143,14 +162,13 @@ typedef struct jwidget_geometry {
} jwidget_geometry;
/* Downwards key event: widget is notified of a key press that ocurred while it
had active focus.
/* Key press event; the widget has active focus and a key was pressed.
-> .data.key: Key event */
extern uint16_t JWIDGET_KEY;
/* Downwards focus-in event: the widget has just received focus */
extern uint16_t JWIDGET_FOCUS_IN;
/* Downwards focus-out event: the widget has just lost focus */
extern uint16_t JWIDGET_FOCUS_OUT;
/* The widget's focus state (either .focused or .active_focused) changed. */
extern uint16_t JWIDGET_FOCUS_CHANGED;
/* The widget is a scope and its target changed. */
extern uint16_t JWIDGET_FOCUS_TARGET_CHANGED;
//---
// Creation and destruction
@ -395,6 +413,53 @@ bool jwidget_needs_update(void *w);
widgets if there has been changes. */
void jwidget_render(void *w, int x, int y);
//---
// Keyboard focus
//---
/* Change the widget's focus policy. This is usually done in the constructor
but can be done anytime. If the policy is changed to JWIDGET_FOCUS_REJECT
while the widget has focus, the focus will be lost. */
void jwidget_set_focus_policy(void *w, jwidget_focus_policy_t fp);
/* Check whether a widget is currently focused within its surrounding scope. */
GINLINE static bool jwidget_has_focus(void *w)
{
return ((jwidget *)w)->focused;
}
/* Check whether a widget is current in the key event propagation chain. */
GINLINE static bool jwidget_has_active_focus(void *w)
{
return ((jwidget *)w)->active_focused;
}
/* Change the target of a focus scope widget. */
void jwidget_scope_set_target(void *fs, void *target);
/* Get the target of a focus scope, NULL if none or not a scope. */
GINLINE static jwidget *jwidget_scope_get_target(void *fs)
{
return ((jwidget *)fs)->focus_scope_target;
}
/* Clear the target of a focus scope widget. */
GINLINE static void jwidget_scope_clear_focus(void *fs)
{
return jwidget_scope_set_target(fs, NULL);
}
/* Context function that returns the immediately surrounding scope that owns
this widget, NULL if there's none. Since jscene is a scope, in general there
should always be one. */
jwidget *jwidgetctx_enclosing_focus_scope(void *w);
/* Context function that gives w focus within its enclosing focus scope. */
void jwidgetctx_grab_focus(void *w);
/* Context function that drops the focus from w (if it has it) within its
enclosing focus scope. */
void jwidgetctx_drop_focus(void *w);
//---
// Misc
//---

View file

@ -52,6 +52,7 @@ jfileselect *jfileselect_create(void *parent)
if(!fs) return NULL;
jwidget_init(&fs->widget, jfileselect_type_id, parent);
jwidget_set_focus_policy(fs, J_FOCUS_POLICY_SCOPE);
jinput *input = jinput_create("Filename: ", 32, fs);
if(!input) {
@ -116,14 +117,14 @@ static void start_input(jfileselect *fs)
{
fs->input_mode = true;
jinput_clear(fs->saveas_input);
jwidget_event(fs->saveas_input, (jevent){ .type = JWIDGET_FOCUS_IN });
jwidget_scope_set_target(fs, fs->saveas_input);
fs->widget.update = true;
}
static void stop_input(jfileselect *fs)
{
fs->input_mode = false;
jwidget_event(fs->saveas_input, (jevent){ .type = JWIDGET_FOCUS_OUT });
jwidget_scope_set_target(fs, NULL);
fs->widget.update = true;
}

View file

@ -45,6 +45,7 @@ jinput *jinput_create(char const *prompt, size_t length, void *parent)
if(!i) return NULL;
jwidget_init(&i->widget, jinput_type_id, parent);
jwidget_set_focus_policy(i, J_FOCUS_POLICY_ACCEPT);
i->color = C_BLACK;
jinput_set_font(i, NULL);
@ -242,14 +243,8 @@ static bool jinput_poly_event(void *i0, jevent e)
{
jinput *i = i0;
if(e.type == JWIDGET_FOCUS_IN) {
i->cursor = i->size - 1;
i->mode = 0;
i->widget.update = 1;
}
if(e.type == JWIDGET_FOCUS_OUT) {
i->cursor = -1;
if(e.type == JWIDGET_FOCUS_CHANGED) {
i->cursor = jwidget_has_focus(i) ? (i->size - 1) : -1;
i->mode = 0;
i->widget.update = 1;
}

View file

@ -21,16 +21,17 @@ jlabel *jlabel_create(char const *text, void *parent)
jwidget_init(&l->widget, jlabel_type_id, parent);
l->block_halign = J_ALIGN_LEFT;
l->block_valign = J_ALIGN_MIDDLE;
l->block_valign = J_ALIGN_TOP;
l->text_align = J_ALIGN_LEFT;
l->line_spacing = 1;
l->line_spacing = 0;
l->color = C_BLACK;
l->font = NULL;
l->wrap_mode = J_WRAP_NONE;
l->text = text;
l->owns_text = false;
l->wrapped_newline_spacing = false;
vec_init(&l->breaks_vec, sizeof *l->breaks);
@ -188,6 +189,12 @@ void jlabel_set_font(jlabel *l, font_t const *font)
l->widget.dirty = 1;
}
void jlabel_set_wrapped_newline_spacing(jlabel *l, bool preserve)
{
l->wrapped_newline_spacing = preserve;
l->widget.dirty = 1;
}
//---
// Polymorphic widget operations
//---
@ -236,7 +243,7 @@ static void jlabel_poly_layout(void *l0)
/* Start of line */
add_break(l, str - l->text);
/* A "\n" forces a newline in all wrap omdes */
/* A "\n" forces a newline in all wrap modes */
char const *end_of_line = strchrnul(str, '\n');
/* Also consider word or letters boundaries */
@ -257,14 +264,17 @@ static void jlabel_poly_layout(void *l0)
}
}
bool natural_break = (*end_of_line == '\n');
char const *next_start = end_of_line + (*end_of_line == '\n');
/* Skip trailing spaces on this line */
while(end_of_line > str && end_of_line[-1] == ' ')
end_of_line--;
/* Skip leading spaces on the next line */
while(next_start[0] == ' ')
next_start++;
if(!natural_break && !l->wrapped_newline_spacing) {
/* Skip trailing spaces on this line */
while(end_of_line > str && end_of_line[-1] == ' ')
end_of_line--;
/* Skip leading spaces on the next line */
while(next_start[0] == ' ')
next_start++;
}
add_break(l, end_of_line - l->text);
@ -295,7 +305,7 @@ static void jlabel_poly_render(void *l0, int x, int y)
/* Position the block vertically */
int lines = l->breaks_vec.size / 2;
int block_height = lines * (f->line_height + l->line_spacing) -
int block_height = lines * (f->line_distance + l->line_spacing) -
l->line_spacing;
if(l->block_valign == J_ALIGN_MIDDLE)
@ -331,7 +341,7 @@ static void jlabel_poly_render(void *l0, int x, int y)
dtext_opt(x + dx, y, l->color, C_NONE, DTEXT_LEFT, DTEXT_TOP,
str, line_length);
y += f->line_height + l->line_spacing;
y += f->line_distance + l->line_spacing;
str = l->text + l->breaks[i];
}

View file

@ -31,6 +31,7 @@ jlist *jlist_create(jlist_item_info_function info_function,
return NULL;
jwidget_init(&l->widget, jlist_type_id, parent);
jwidget_set_focus_policy(l, J_FOCUS_POLICY_ACCEPT);
l->item_count = 0;
l->items = NULL;

View file

@ -44,13 +44,17 @@ jscene *jscene_create(int x, int y, int w, int h, void *parent)
if(!s) return NULL;
jwidget_init(&s->widget, jscene_type_id, parent);
jwidget_set_focus_policy(s, J_FOCUS_POLICY_SCOPE);
jwidget_set_fixed_size(s, w, h);
jlayout_set_vbox(s);
/* The scene is where active focus originates */
s->widget.focused = 1;
s->widget.active_focused = 1;
s->x = x;
s->y = y;
s->focus = NULL;
s->queue_first = 0;
s->queue_next = 0;
s->lost_events = 0;
@ -128,27 +132,37 @@ void jscene_queue_event(jscene *s, jevent e)
void *jscene_focused_widget(jscene *s)
{
return s->focus;
jwidget *w = &s->widget;
while(w->focus_scope_target)
w = w->focus_scope_target;
return w;
}
void jscene_set_focused_widget(jscene *s, void *w0)
{
J_CAST(w)
/* Unfocus only at the top level */
if(!w) {
jwidget_scope_set_target(s, NULL);
return;
}
if(w->focus_policy == J_FOCUS_POLICY_REJECT)
return;
/* Check that (s) is an ancestor of (w) */
if(w) for(jwidget *anc = w; anc != (jwidget *)s; anc = anc->parent) {
for(jwidget *anc = w; anc != (jwidget *)s; anc = anc->parent) {
if(anc == NULL) return;
}
/* Focus out old focused widget */
if(s->focus) jwidget_event(s->focus,
(jevent){ .type = JWIDGET_FOCUS_OUT, .source = s->focus });
s->focus = w;
/* Focus in newly-selected widget */
if(w) jwidget_event(w,
(jevent){ .type = JWIDGET_FOCUS_IN, .source = w });
/* Set targets in every scope along the way up */
jwidget *scope = w;
while(scope != NULL) {
scope = jwidgetctx_enclosing_focus_scope(scope);
jwidget_scope_set_target(scope, w);
w = scope;
}
}
void jscene_show_and_focus(jscene *scene, void *w0)
@ -182,12 +196,13 @@ void jscene_show_and_focus(jscene *scene, void *w0)
bool jscene_process_key_event(jscene *scene, key_event_t event)
{
jwidget *candidate = scene->focus;
jwidget *candidate = jscene_focused_widget(scene);
jevent e = { .type = JWIDGET_KEY, .key = event };
while(candidate) {
if(jwidget_event(candidate, e)) return true;
candidate = candidate->parent;
if(jwidget_event(candidate, e))
return true;
candidate = jwidgetctx_enclosing_focus_scope(candidate);
}
return false;

View file

@ -31,9 +31,10 @@ static jwidget_poly *widget_types[WIDGET_TYPES_MAX] = {
};
/* Events */
uint16_t JWIDGET_KEY;
uint16_t JWIDGET_FOCUS_IN;
uint16_t JWIDGET_FOCUS_OUT;
J_DEFINE_EVENTS(
JWIDGET_KEY,
JWIDGET_FOCUS_CHANGED,
JWIDGET_FOCUS_TARGET_CHANGED);
//---
// Polymorphic functions for widgets
@ -117,9 +118,13 @@ void jwidget_init(jwidget *w, int type, void *parent)
w->visible = 1;
w->floating = 0;
w->clipped = 0;
w->focus_policy = J_FOCUS_POLICY_REJECT;
w->focused = 0;
w->active_focused = 0;
w->type = type;
w->geometry = NULL;
w->focus_scope_target = NULL;
w->x = 0;
w->y = 0;
@ -605,6 +610,10 @@ void jwidget_set_floating(void *w0, bool floating)
if(w->parent) w->parent->dirty = 1;
}
//---
// Rendering
//---
bool jwidget_clipped(void *w0)
{
J_CAST(w)
@ -620,10 +629,6 @@ void jwidget_set_clipped(void *w0, bool clipped)
w->update = 1;
}
//---
// Rendering
//---
void jwidget_render(void *w0, int x, int y)
{
J_CAST(w)
@ -692,6 +697,117 @@ void jwidget_render(void *w0, int x, int y)
w->update = 0;
}
//---
// Keyboard focus
//---
void jwidget_set_focus_policy(void *w0, jwidget_focus_policy_t fp)
{
J_CAST(w)
if(w->focus_policy == fp)
return;
/* If this was a scope, clear it (otherwise the surrounding scope, which is
going to expand, could pick up a second, untargeted focused widget). */
if(w->focus_policy == J_FOCUS_POLICY_SCOPE)
jwidget_scope_clear_focus(w);
/* Remove focus if we're no longer accepting it */
if(fp == J_FOCUS_POLICY_REJECT && jwidget_has_focus(w))
jwidgetctx_drop_focus(w);
w->focus_policy = fp;
}
static void notify_focus_changed(jwidget *w)
{
jevent e;
e.source = w;
e.type = JWIDGET_FOCUS_CHANGED;
jwidget_event(w, e);
}
static void set_focus_chain_active(
jwidget *w, bool set, bool notify_toplevel, bool bottom_up)
{
bool recursive =
(w->focus_policy == J_FOCUS_POLICY_SCOPE && w->focus_scope_target);
if(recursive && bottom_up)
set_focus_chain_active(w->focus_scope_target, set, true, bottom_up);
w->active_focused = set;
if(notify_toplevel)
notify_focus_changed(w);
if(recursive && !bottom_up)
set_focus_chain_active(w->focus_scope_target, set, true, bottom_up);
}
void jwidget_scope_set_target(void *fs0, void *target0)
{
J_CAST(fs, target)
if(!fs || fs->focus_policy != J_FOCUS_POLICY_SCOPE
|| fs->focus_scope_target == target)
return;
jwidget *oldt = fs->focus_scope_target;
if(oldt) {
/* First, if we have active focus, remove it from the entire chain */
if(jwidget_has_active_focus(fs))
set_focus_chain_active(oldt, false, false, true);
/* Then, remove the focus flag from the scope target and notify it */
oldt->focused = 0;
notify_focus_changed(oldt);
}
fs->focus_scope_target = target;
if(target) {
/* Now, give focus to the new scope target */
target->focused = 1;
notify_focus_changed(target);
if(jwidget_has_active_focus(fs))
set_focus_chain_active(target, true, false, false);
}
jevent e;
e.source = fs;
e.type = JWIDGET_FOCUS_TARGET_CHANGED;
jwidget_event(fs, e);
}
jwidget *jwidgetctx_enclosing_focus_scope(void *w0)
{
J_CAST(w)
do w = w->parent;
while(w && w->focus_policy != J_FOCUS_POLICY_SCOPE);
return w;
}
void jwidgetctx_drop_focus(void *w0)
{
J_CAST(w)
if(!w || !jwidget_has_focus(w))
return;
jwidget *scope = jwidgetctx_enclosing_focus_scope(w);
if(scope)
jwidget_scope_clear_focus(scope);
}
void jwidgetctx_grab_focus(void *w0)
{
J_CAST(w)
if(!w || jwidget_has_focus(w))
return;
jwidget *scope = jwidgetctx_enclosing_focus_scope(w);
if(scope)
jwidget_scope_set_target(scope, w);
}
//---
// Event management
//---
@ -752,11 +868,3 @@ int j_register_event(void)
event_id++;
return event_id;
}
__attribute__((constructor(1000)))
static void j_register_jwidget(void)
{
JWIDGET_KEY = j_register_event();
JWIDGET_FOCUS_IN = j_register_event();
JWIDGET_FOCUS_OUT = j_register_event();
}