JustUI/src/jframe.c

393 lines
11 KiB
C

#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;
f->visibility_margin_x = 4;
f->visibility_margin_y = 4;
#else
f->scrollbar_width = 2;
f->scrollbar_spacing = 2;
f->visibility_margin_x = 8;
f->visibility_margin_y = 8;
#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 *f)
{
if(f->widget.child_count == 0)
return NULL;
return f->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 *f, jalign halign, jalign valign)
{
if(f->halign == halign && f->valign == valign)
return;
f->halign = halign;
f->valign = valign;
f->widget.update = 1;
}
void jframe_set_scrollbars_always_visible(jframe *f, bool always_visible)
{
if(f->scrollbars_always_visible == always_visible)
return;
f->scrollbars_always_visible = always_visible;
f->widget.dirty = 1;
}
void jframe_set_floating_scrollbars(jframe *f, bool floating_scrollbars)
{
if(f->floating_scrollbars == floating_scrollbars)
return;
f->floating_scrollbars = floating_scrollbars;
f->widget.dirty = 1;
}
void jframe_set_keyboard_control(jframe *f, bool keyboard_control)
{
f->keyboard_control = keyboard_control;
}
void jframe_set_match_size(jframe *f, bool match_width, bool match_height)
{
if(f->match_width == match_width && f->match_height == match_height)
return;
f->match_width = match_width;
f->match_height = match_height;
f->widget.dirty = 1;
}
void jframe_set_visibility_margin(jframe *f, int margin_x, int margin_y)
{
f->visibility_margin_x = margin_x;
f->visibility_margin_y = margin_y;
}
//---
// Scrolling
//---
void jframe_scroll_to_region(jframe *f, jrect region, bool do_clamp)
{
jwidget *child = frame_child(f);
if(!child)
return;
int x1 = region.x, x2 = x1 + region.w;
int y1 = region.y, y2 = y1 + region.h;
/* Clipping */
x1 = max(x1, 0);
y1 = max(y1, 0);
x2 = min(x2, jwidget_full_width(child));
y2 = min(y2, jwidget_full_height(child));
/* Viewport region
TODO: Handle oversized visibility margin properly */
int vp_x1 = f->visibility_margin_x;
int vp_x2 = jwidget_content_width(f) - f->visibility_margin_x;
int vp_y1 = f->visibility_margin_y;
int vp_y2 = jwidget_content_height(f) - f->visibility_margin_y;
/* If the requested region doesn't fit in the viewport, center on it */
if(x2 - x1 > vp_x2 - vp_x1)
f->scroll_x = (x1 + x2) / 2 - (vp_x1 + vp_x2) / 2;
/* The visible region for some scroll_x is
scroll_x + vp_x1 ... scroll_x + vp_x2
The minimum/maximum value for scroll_x are when
x2 - scroll_x = vp_x2 (region x2 touches viewport x2)
x1 - scroll_x = vp_x1 (region x1 touches viewport x1)
The max is >= the min due to the guard on the if. */
else
f->scroll_x = clamp(f->scroll_x, x2 - vp_x2, x1 - vp_x1);
if(y2 - y1 > vp_y2 - vp_y1)
f->scroll_y = (y1 + y2) / 2 - (vp_y1 + vp_y2) / 2;
else
f->scroll_y = clamp(f->scroll_y, y2 - vp_y2, y1 - vp_y1);
/* Safety clamp */
if(do_clamp) {
f->scroll_x = clamp(f->scroll_x, 0, f->max_scroll_x);
f->scroll_y = clamp(f->scroll_y, 0, f->max_scroll_y);
}
f->widget.update = 1;
}
//---
// 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");
}