jframe: new widget that adds scrolling to a large child (basics)

This commit is contained in:
Lephenixnoir 2022-11-20 14:56:42 +01:00
parent 950c5b7152
commit 51fbf2f582
Signed by: Lephenixnoir
GPG Key ID: 1BBA026E13FC0495
4 changed files with 394 additions and 3 deletions

View File

@ -29,6 +29,7 @@ add_library(${NAME} STATIC
src/jpainted.c
src/jfkeys.c
src/jfileselect.c
src/jframe.c
src/vec.c
src/keymap.c
${ASSETS_${FXSDK_PLATFORM}}

65
include/justui/jframe.h Normal file
View File

@ -0,0 +1,65 @@
//---
// JustUI.jframe: Scrolling frame holding a widget
//---
#ifndef _J_JFRAME
#define _J_JFRAME
#include <justui/defs.h>
#include <justui/jwidget.h>
/* jframe: Scrolling frame holding a widget
This widget is used to implement scrolling widgets. It has a single child,
which is displayed fully if it's smaller than the frame, or partially (with
scrollbars) otherwise.
The child widget has horizontal and vertical alignments, which specify its
position within the frame when smaller than the frame. Its position when
larger than the frame is determined by the scrolling offsets, which can be
manipulated manually or left for the frame to control with arrow keys.
Scrollbars can be set to either render on top of the framed widget, or
occupy dedicated space. */
typedef struct {
jwidget widget;
/* Horizontal and vertical alignment for the child */
jalign halign, valign;
/* Force scrollbars even if the child is smaller than the frame */
bool scrollbars_always_visible;
/* Scrollbars render on top of the child widget */
bool floating_scrollbars;
/* Scrolling can be handled by the frame itself, with arrow keys */
bool keyboard_control;
/* Force matching the width and/or height of the child widget */
bool match_width, match_height;
/* Scrollbar width, in pixels */
uint8_t scrollbar_width;
/* If floating_scrollbars == false, spacing between scrollbars and child */
uint8_t scrollbar_spacing;
/* Whether scrollbars are shown */
bool scrollbar_x, scrollbar_y;
/* Current scroll offsets */
int16_t scroll_x, scroll_y;
/* Maximum scroll offsets for the current size of the child widget */
int16_t max_scroll_x, max_scroll_y;
} jframe;
/* jframe_create(): Create a new frame
The frame's inner widget is always its first child. It can be specified by
jwidget_set_parent() or by creating the child with the frame as a parent
directy. More children can be added, but they will not be rendered. */
jframe *jframe_create(void *parent);
/* Trivial properties */
void jframe_set_align(jframe *j, jalign halign, jalign valign);
void jframe_set_scrollbars_always_visible(jframe *j, bool always_visible);
void jframe_set_floating_scrollbars(jframe *j, bool floating_scrollbars);
void jframe_set_keyboard_control(jframe *j, bool keyboard_control);
void jframe_set_match_size(jframe *j, bool match_width, bool match_height);
#endif /* _J_JFRAME */

321
src/jframe.c Normal file
View File

@ -0,0 +1,321 @@
#include <justui/jwidget.h>
#include <justui/jwidget-api.h>
#include <justui/jframe.h>
#include "util.h"
#include <gint/display.h>
#include <stdlib.h>
/* Type identifier for jframe */
static int jframe_type_id = -1;
jframe *jframe_create(void *parent)
{
if(jframe_type_id < 0)
return NULL;
jframe *f = malloc(sizeof *f);
if(!f)
return NULL;
jwidget_init(&f->widget, jframe_type_id, parent);
jwidget_set_clipped(f, true);
f->halign = J_ALIGN_CENTER;
f->valign = J_ALIGN_MIDDLE;
f->scrollbars_always_visible = false;
f->floating_scrollbars = false;
f->keyboard_control = false;
f->match_width = false;
f->match_height = false;
#ifdef FX9860G
f->scrollbar_width = 1;
f->scrollbar_spacing = 1;
#else
f->scrollbar_width = 2;
f->scrollbar_spacing = 2;
#endif
f->scroll_x = 0;
f->scroll_y = 0;
f->max_scroll_x = 0;
f->max_scroll_y = 0;
return f;
}
static jwidget *frame_child(jframe *j)
{
if(j->widget.child_count == 0)
return NULL;
return j->widget.children[0];
}
static int frame_scrollbar_space_size(jframe *f)
{
if(f->floating_scrollbars)
return 0;
return f->scrollbar_width + f->scrollbar_spacing;
}
static void shake(jframe *f)
{
f->scroll_x = clamp(f->scroll_x, 0, f->max_scroll_x);
f->scroll_y = clamp(f->scroll_y, 0, f->max_scroll_y);
}
/* Start position of a block of size B, aligned as specified, inside a space of
size S; returned as a value in [0..s). */
static int aligned_start_within(int B, int S, jalign align)
{
if(align == J_ALIGN_LEFT || align == J_ALIGN_TOP)
return 0;
else if(align == J_ALIGN_RIGHT || align == J_ALIGN_BOTTOM)
return S - B;
else
return (S - B) / 2;
}
//---
// Getters and setters
//---
void jframe_set_align(jframe *j, jalign halign, jalign valign)
{
if(j->halign == halign && j->valign == valign)
return;
j->halign = halign;
j->valign = valign;
j->widget.update = 1;
}
void jframe_set_scrollbars_always_visible(jframe *j, bool always_visible)
{
if(j->scrollbars_always_visible == always_visible)
return;
j->scrollbars_always_visible = always_visible;
j->widget.dirty = 1;
}
void jframe_set_floating_scrollbars(jframe *j, bool floating_scrollbars)
{
if(j->floating_scrollbars == floating_scrollbars)
return;
j->floating_scrollbars = floating_scrollbars;
j->widget.dirty = 1;
}
void jframe_set_keyboard_control(jframe *j, bool keyboard_control)
{
j->keyboard_control = keyboard_control;
}
//---
// Polymorphic widget operations
//---
static void jframe_poly_csize(void *f0)
{
jframe *f = f0;
jwidget *w = &f->widget;
jwidget *child = frame_child(f);
w->w = w->h = 16;
if(child) {
jwidget_msize(child);
if(f->match_width)
w->w = child->w;
if(f->match_height)
w->h = child->h;
}
int frame_sss = frame_scrollbar_space_size(f);
if(!f->floating_scrollbars) {
w->w += frame_sss;
w->h += frame_sss;
}
jwidget_set_minimum_size(f, frame_sss + 4, frame_sss + 4);
}
static void jframe_poly_layout(void *f0)
{
jframe *f = f0;
jwidget *child = frame_child(f);
if(!child) {
f->scrollbar_x = f->scrollbar_y = f->scrollbars_always_visible;
f->scroll_x = f->scroll_y = 0;
f->max_scroll_x = f->max_scroll_y = 0;
return;
}
int child_w = jwidget_full_width(child);
int child_h = jwidget_full_height(child);
int frame_w = jwidget_content_width(f);
int frame_h = jwidget_content_height(f);
int frame_sss = frame_scrollbar_space_size(f);
int sss_x = 0;
int sss_y = 0;
/* We enable scrollbars if:
(1) They were forced in; or
(2) The child widget wouldn't fit without them.
Scrollbars are linked; adding a scrollbar for one direction can reduce
the space available in the other direction, thus causing the other
scrollbar to appear. Hence, we need to iterate. */
f->scrollbar_y = f->scrollbars_always_visible || child_h + sss_y > frame_h;
if(f->scrollbar_y)
sss_x = frame_sss;
f->scrollbar_x = f->scrollbars_always_visible || child_w + sss_x > frame_w;
if(f->scrollbar_x)
sss_y = frame_sss;
f->scrollbar_y = f->scrollbars_always_visible || child_h + sss_y > frame_h;
if(f->scrollbar_y)
sss_x = frame_sss;
/* At this stage we have a fixpoint, because:
- x is up-to-date. x can only be outdated if the 2nd y check just
enabled scrollbar_y. But it can only do so if the x check enabled
scrollbar_x, in which case scrollbar_x is already a stable true.
- y is up-to-date since it was re-checked after x's last update. */
f->max_scroll_x = max(0, child_w - (frame_w - sss_x));
f->max_scroll_y = max(0, child_h - (frame_h - sss_y));
shake(f);
/* We can now set the inner widget's dimensions. The frame acts as a
container, and thus sets the child's size, applying strech etc. One
unique trait of the frame is that the child *always* gets its desired
size since we can scroll it. */
if(child->stretch_x > 0)
child->w = max(child->w, min(frame_w, child->max_w));
if(child->stretch_y > 0)
child->h = max(child->h, min(frame_h, child->max_h));
}
static void jframe_poly_render(void *f0, int x, int y)
{
jframe *f = f0;
jwidget *child = frame_child(f);
int child_w = jwidget_full_width(child);
int child_h = jwidget_full_height(child);
int frame_w = jwidget_content_width(f);
int frame_h = jwidget_content_height(f);
int frame_sss = frame_scrollbar_space_size(f);
int sss_x = f->scrollbar_y ? frame_sss : 0;
int sss_y = f->scrollbar_x ? frame_sss : 0;
/* In each dimension:
- If there is scrolling, we place according to the scroll offset;
- Otherwise, we place according to alignment settings. */
int render_x;
if(f->scrollbar_x)
render_x = x - f->scroll_x;
else
render_x = x + aligned_start_within(child_w, frame_w-sss_x, f->halign);
int render_y;
if(f->scrollbar_y)
render_y = y - f->scroll_y;
else
render_y = y + aligned_start_within(child_h, frame_h-sss_y, f->valign);
/* Render the child with dedicated clipping. */
if(child) {
struct dwindow win = {x, y, x + frame_w - sss_x, y + frame_h - sss_y};
win = intersect_dwindow(win, dwindow);
struct dwindow old_window = dwindow_set(win);
jwidget_render(child, render_x, render_y);
dwindow_set(old_window);
}
/* Render the scrollbars. */
if(f->scrollbar_x) {
int sb_x = x;
int sb_y = y + frame_h - f->scrollbar_width;
int sb_left = f->scroll_x * frame_w / child_w;
int sb_width = frame_w * frame_w / child_w;
drect(sb_x + sb_left, sb_y, sb_x + sb_left + sb_width - 1,
sb_y + f->scrollbar_width - 1, C_BLACK);
}
if(f->scrollbar_y) {
int sb_x = x + frame_w - f->scrollbar_width;
int sb_y = y;
int sb_top = f->scroll_y * frame_h / child_h;
int sb_height = frame_h * frame_h / child_h;
drect(sb_x, sb_y + sb_top, sb_x + f->scrollbar_width - 1,
sb_y + sb_top + sb_height - 1, C_BLACK);
}
}
static bool jframe_poly_event(void *f0, jevent e)
{
jframe *f = f0;
if(!f->keyboard_control || e.type != JWIDGET_KEY)
return false;
key_event_t ev = e.key;
if(ev.type != KEYEV_DOWN && ev.type != KEYEV_HOLD)
return false;
int key = ev.key;
if(key == KEY_LEFT && f->scrollbar_x && f->scroll_x > 0) {
f->scroll_x--;
f->widget.update = 1;
return true;
}
if(key == KEY_RIGHT && f->scrollbar_x && f->scroll_x < f->max_scroll_x-1) {
f->scroll_x++;
f->widget.update = 1;
return true;
}
if(key == KEY_UP && f->scrollbar_y && f->scroll_y > 0) {
f->scroll_y--;
f->widget.update = 1;
return true;
}
if(key == KEY_DOWN && f->scrollbar_y && f->scroll_y < f->max_scroll_y-1) {
f->scroll_y++;
f->widget.update = 1;
return true;
}
return false;
}
/* jframe type definition */
static jwidget_poly type_jframe = {
.name = "jframe",
.csize = jframe_poly_csize,
.layout = jframe_poly_layout,
.render = jframe_poly_render,
.event = jframe_poly_event,
};
__attribute__((constructor(1001)))
static void j_register_jframe(void)
{
jframe_type_id = j_register_widget(&type_jframe, "jwidget");
}

View File

@ -663,9 +663,13 @@ void jwidget_render(void *w0, int x, int y)
if(w->clipped) {
struct dwindow win = { x, y, x+cw, y+ch };
win = intersect_dwindow(win, dwindow);
struct dwindow old_window = dwindow_set(win);
poly->render(w, x, y);
dwindow_set(old_window);
/* Skip rendering out-of-view widgets */
if(win.right > win.left && win.bottom > win.top) {
struct dwindow old_window = dwindow_set(win);
poly->render(w, x, y);
dwindow_set(old_window);
}
}
else {
poly->render(w, x, y);