#include #include #include #include #include "jlayout_p.h" #include "util.h" #include #include #include #define WIDGET_TYPES_MAX 32 /* Polymorphic functions for jwidget */ static jwidget_poly_csize_t jwidget_poly_csize; /* jwidget type definition */ static jwidget_poly type_jwidget = { .name = "jwidget", .csize = jwidget_poly_csize, .layout = NULL, .render = jwidget_poly_render, .event = jwidget_poly_event, .destroy = NULL, }; /* List of registered widget types */ static jwidget_poly *widget_types[WIDGET_TYPES_MAX] = { &type_jwidget, NULL, }; /* Events */ J_DEFINE_EVENTS( JWIDGET_KEY, JWIDGET_FOCUS_CHANGED, JWIDGET_FOCUS_TARGET_CHANGED); //--- // Polymorphic functions for widgets //--- void jwidget_poly_render(void *w0, int x, int y) { J_CAST(w) jlayout_stack *l; /* If there is a stack layout, render active child */ if((l = jlayout_get_stack(w)) && l->active >= 0) { jwidget *child = w->children[l->active]; if(child->visible && !child->floating) jwidget_render(child, x+child->x, y+child->y); } /* Otherwise, simply render all non-floating children */ else for(int k = 0; k < w->child_count; k++) { jwidget *child = w->children[k]; if(child->visible)// && !child->floating) jwidget_render(child, x+child->x, y+child->y); } /* Render floating widgets over the layout */ for(int k = 0; k < w->child_count; k++) { jwidget *child = w->children[k]; if(child->visible && child->floating) jwidget_render(child, x+child->x, y+child->y); } } static void jwidget_poly_csize(void *w0) { J_CAST(w) /* The content box of this children is the union of the border boxes of the children. This function is called only for widgets without a layout, in which case the children must have their positions set. */ w->w = 0; w->h = 0; for(int k = 0; k < w->child_count; k++) { jwidget *child = w->children[k]; if(!child->visible) continue; jwidget_msize(child); w->w = max(w->w, child->x + child->w); w->h = max(w->h, child->y + child->h); } } bool jwidget_poly_event(void *w0, jevent e) { J_CAST(w) (void)w; (void)e; return false; } //--- // Initialization //--- jwidget *jwidget_create(void *parent) { jwidget *w = malloc(sizeof *w); if(!w) return NULL; /* Type ID 0 is for jwidget */ jwidget_init(w, 0, parent); return w; } void jwidget_init(jwidget *w, int type, void *parent) { w->parent = NULL; w->children = NULL; w->child_count = 0; w->child_alloc = 0; w->dirty = 1; 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; w->w = 0; w->h = 0; w->min_w = 0; w->min_h = 0; w->max_w = 0x7fff; w->max_h = 0x7fff; w->layout = J_LAYOUT_NONE; w->stretch_x = 0; w->stretch_y = 0; w->stretch_force = 0; jwidget_set_parent(w, parent); } void jwidget_destroy(void *w0) { J_CAST(w) jwidget_set_parent(w, NULL); /* Run the custom destructor */ jwidget_poly const *poly = widget_types[w->type]; if(poly->destroy) poly->destroy(w); for(int i = 0; i < w->child_count; i++) { /* This will prevent us from unregistering the child from w in the recursive call, which saves some time in pointer manipulation */ w->children[i]->parent = NULL; jwidget_destroy(w->children[i]); } if(w->children) free(w->children); if(w->geometry) free(w->geometry); free(w); } //--- // Ownership //--- void jwidget_set_parent(void *w0, void *parent0) { J_CAST(w, parent) if(w->parent == parent) return; if(w->parent != NULL) jwidget_remove_child(w->parent, w); if(parent != NULL) jwidget_add_child(parent, w); } void jwidget_add_child(void *w0, void *child0) { J_CAST(w, child) jwidget_insert_child(w, child, w->child_count); } void jwidget_insert_child(void *w0, void *child0, int position) { J_CAST(w, child) if(child->parent == w) return; if(position < 0 || position > w->child_count) return; /* Don't overflow the child_count and child_alloc fields! */ if(w->child_count >= 256 - 4) return; /* Make room for a new child if needed */ if(w->child_count + 1 > w->child_alloc) { size_t new_size = (w->child_alloc + 4) * sizeof(jwidget *); jwidget **new_children = realloc(w->children, new_size); if(!new_children) return; w->children = new_children; w->child_alloc += 4; } /* Insert new child */ for(int k = w->child_count; k > position; k--) w->children[k] = w->children[k - 1]; w->children[position] = child; w->child_count++; /* Remove the existing parent at the last moment, in order to keep the tree in a safe state if allocations fail or parameters are invalid */ if(child->parent != NULL) jwidget_remove_child(child->parent, child); child->parent = w; /* Update stack layouts to keep showing the same child */ if(w->layout == J_LAYOUT_STACK && w->layout_stack.active >= position) { w->layout_stack.active++; } /* Update stack layout to show the first child if none was visible yet */ if(w->layout == J_LAYOUT_STACK && w->layout_stack.active == -1) { w->layout_stack.active = 0; } /* Force a later recomputation of the layout */ w->dirty = 1; } void jwidget_remove_child(void *w0, void *child0) { J_CAST(w, child) if(child->parent != w) return; // TODO[jwidget_remove_child]: Remove focus int write = 0; int index = -1; /* Remove all occurrences of (child) from (w->children) */ for(int read = 0; read < w->child_count; read++) { if(w->children[read] != child) { w->children[write++] = w->children[read]; } else if(index < 0) { index = read; } } w->child_count = write; /* Remove the parent from the child */ child->parent = NULL; /* Update stack layouts to not show the removed child */ if(w->layout == J_LAYOUT_STACK && w->layout_stack.active == index) { w->layout_stack.active = -1; } /* Force a later recomputation of the layout */ w->dirty = 1; } int jwidget_child_position(void *w0, void *child0) { J_CAST(w, child) for(int i = 0; i < w->child_count; i++) { if(w->children[i] == child) return i; } return -1; } //--- // Sizing and stretching //--- void jwidget_set_minimum_width(void *w0, int min_width) { J_CAST(w) w->min_w = clamp(min_width, 0, 0x7fff); w->dirty = 1; } void jwidget_set_minimum_height(void *w0, int min_height) { J_CAST(w) w->min_h = clamp(min_height, 0, 0x7fff); w->dirty = 1; } void jwidget_set_minimum_size(void *w, int min_width, int min_height) { jwidget_set_minimum_width(w, min_width); jwidget_set_minimum_height(w, min_height); } void jwidget_set_maximum_width(void *w0, int max_width) { J_CAST(w) w->max_w = clamp(max_width, 0, 0x7fff); w->dirty = 1; } void jwidget_set_maximum_height(void *w0, int max_height) { J_CAST(w) w->max_h = clamp(max_height, 0, 0x7fff); w->dirty = 1; } void jwidget_set_maximum_size(void *w, int max_width, int max_height) { jwidget_set_maximum_width(w, max_width); jwidget_set_maximum_height(w, max_height); } void jwidget_set_fixed_width(void *w, int width) { jwidget_set_minimum_width(w, width); jwidget_set_maximum_width(w, width); } void jwidget_set_fixed_height(void *w, int height) { jwidget_set_minimum_height(w, height); jwidget_set_maximum_height(w, height); } void jwidget_set_fixed_size(void *w, int width, int height) { jwidget_set_minimum_size(w, width, height); jwidget_set_maximum_size(w, width, height); } void jwidget_set_stretch(void *w0, int stretch_x, int stretch_y, bool force) { J_CAST(w) w->stretch_x = clamp(stretch_x, 0, 15); w->stretch_y = clamp(stretch_y, 0, 15); w->stretch_force = force; w->dirty = 1; } //--- // Geometry //--- static jwidget_geometry default_geometry = { .margins = { 0, 0, 0, 0 }, .borders = { 0, 0, 0, 0 }, .paddings = { 0, 0, 0, 0 }, .border_color = C_NONE, .border_style = J_BORDER_NONE, .background_color = C_NONE, }; jwidget_geometry const *jwidget_geometry_r(void *w0) { J_CAST(w) return (w->geometry == NULL) ? &default_geometry : w->geometry; } jwidget_geometry *jwidget_geometry_rw(void *w0) { J_CAST(w) /* Duplicate default geometry as a copy-on-write tactic to save memory */ if(w->geometry == NULL) { w->geometry = malloc(sizeof *w->geometry); if(!w->geometry) return NULL; *w->geometry = default_geometry; } /* Assume layout will need to be recomputed */ w->dirty = 1; return w->geometry; } void jwidget_set_border(void *w, jwidget_border_style s, int width, int color) { jwidget_geometry *g = jwidget_geometry_rw(w); g->border_style = s; g->border_color = color; for(int i = 0; i < 4; i++) g->borders[i] = width; } void jwidget_set_borders(void *w, jwidget_border_style s, int color, int top, int right, int bottom, int left) { jwidget_geometry *g = jwidget_geometry_rw(w); g->border_style = s; g->border_color = color; g->border.top = top; g->border.right = right; g->border.bottom = bottom; g->border.left = left; } void jwidget_set_padding(void *w, int top, int right, int bottom, int left) { jwidget_geometry *g = jwidget_geometry_rw(w); g->padding.top = top; g->padding.right = right; g->padding.bottom = bottom; g->padding.left = left; } void jwidget_set_margin(void *w, int top, int right, int bottom, int left) { jwidget_geometry *g = jwidget_geometry_rw(w); g->margin.top = top; g->margin.right = right; g->margin.bottom = bottom; g->margin.left = left; } void jwidget_set_background(void *w, int color) { jwidget_geometry *g = jwidget_geometry_rw(w); g->background_color = color; } //--- // Layout //--- void jlayout_set_manual(void *w0) { J_CAST(w) w->layout = J_LAYOUT_NONE; w->dirty = 1; } void jwidget_msize(void *w0) { J_CAST(w) int t = w->layout; /* Size of contents */ if(t == J_LAYOUT_NONE) { jwidget_poly const *poly = widget_types[w->type]; if(poly->csize) poly->csize(w); } else if(t == J_LAYOUT_HBOX || t == J_LAYOUT_VBOX) jlayout_box_csize(w); else if(t == J_LAYOUT_STACK) jlayout_stack_csize(w); else if(t == J_LAYOUT_GRID) jlayout_grid_csize(w); /* Add the size of the geometry */ jwidget_geometry const *g = jwidget_geometry_r(w); w->w += g->margin.left + g->border.left + g->padding.left; w->w += g->margin.right + g->border.right + g->padding.right; w->h += g->margin.top + g->border.top + g->padding.top; w->h += g->margin.bottom + g->border.bottom + g->padding.bottom; w->dirty = 1; } bool jwidget_layout_dirty(void *w0) { J_CAST(w) if(w->dirty) return true; for(int k = 0; k < w->child_count; k++) { if(!w->children[k]->visible) continue; if(jwidget_layout_dirty(w->children[k])) return true; } return false; } bool jwidget_visible(void *w0) { J_CAST(w) return w->visible; } void jwidget_set_visible(void *w0, bool visible) { J_CAST(w) if(w->visible == visible) return; w->visible = (visible != 0); if(w->parent) w->parent->dirty = 1; } bool jwidget_needs_update(void *w0) { J_CAST(w) if(w->update) return true; jlayout_stack *l = jlayout_get_stack(w); /* Ignore invisible children, because their update bits are not reset after a repaint, resulting in infinite REPAINT events */ for(int k = 0; k < w->child_count; k++) { if(l && l->active != k) continue; if(!w->children[k]->visible) continue; if(jwidget_needs_update(w->children[k])) return true; } return false; } /* Apply layout recursively on this widget and its children */ static void jwidget_layout_apply(void *w0) { J_CAST(w) if(!w->visible) return; int t = w->layout; if(t == J_LAYOUT_NONE) { jwidget_poly const *poly = widget_types[w->type]; if(poly->layout) poly->layout(w); } else if(t == J_LAYOUT_HBOX || t == J_LAYOUT_VBOX) { jlayout_box_apply(w); } else if(t == J_LAYOUT_STACK) { jlayout_stack_apply(w); } else if(t == J_LAYOUT_GRID) { jlayout_grid_apply(w); } /* The layout is now up-to-date and will not be recomputed until a widget in the hierarchy requires it */ w->dirty = 0; for(int k = 0; k < w->child_count; k++) jwidget_layout_apply(w->children[k]); } void jwidget_layout(void *root0) { J_CAST(root) if(!jwidget_layout_dirty(root)) return; /* Phase 1: Compute the natural margin size of every widget (bottom-up) */ jwidget_msize(root); /* Decide on the size of the root; first make sure it's acceptable */ root->w = clamp(root->w, root->min_w, root->max_w); root->h = clamp(root->h, root->min_h, root->max_h); /* Now if there is stretch and a maximum size, stretch */ if(root->stretch_x && root->max_w != 0x7fff) root->w = root->max_w; if(root->stretch_y && root->max_h != 0x7fff) root->h = root->max_h; /* Phase 2: Distribute space recursively (top-down) */ jwidget_layout_apply(root); } int jwidget_absolute_x(void *w0) { J_CAST(w) return w ? w->x + jwidget_absolute_x(w->parent) : 0; } int jwidget_absolute_y(void *w0) { J_CAST(w) return w ? w->y + jwidget_absolute_y(w->parent) : 0; } int jwidget_absolute_content_x(void *w0) { J_CAST(w) if(!w) return 0; jwidget_geometry const *g = jwidget_geometry_r(w); return jwidget_absolute_content_x(w->parent) + w->x + g->margin.left + g->border.left + g->padding.left; } int jwidget_absolute_content_y(void *w0) { J_CAST(w) if(!w) return 0; jwidget_geometry const *g = jwidget_geometry_r(w); return jwidget_absolute_content_y(w->parent) + w->y + g->margin.top + g->border.top + g->padding.top; } int jwidget_content_width(void *w0) { J_CAST(w) jwidget_geometry const *g = jwidget_geometry_r(w); return w->w - g->margin.left - g->border.left - g->padding.left - g->margin.right - g->border.right - g->padding.right; } int jwidget_content_height(void *w0) { J_CAST(w) jwidget_geometry const *g = jwidget_geometry_r(w); return w->h - g->margin.top - g->border.top - g->padding.top - g->margin.bottom - g->border.bottom - g->padding.bottom; } int jwidget_full_width(void *w0) { J_CAST(w) return w->w; } int jwidget_full_height(void *w0) { J_CAST(w) return w->h; } bool jwidget_floating(void *w0) { J_CAST(w) return w->floating; } void jwidget_set_floating(void *w0, bool floating) { J_CAST(w) if(w->floating == floating) return; w->floating = (floating != 0); if(w->parent) w->parent->dirty = 1; } //--- // Rendering //--- bool jwidget_clipped(void *w0) { J_CAST(w) return w->clipped; } void jwidget_set_clipped(void *w0, bool clipped) { J_CAST(w) if(w->clipped == clipped) return; w->clipped = (clipped != 0); w->update = 1; } void jwidget_render(void *w0, int x, int y) { J_CAST(w) if(!w->visible) return; /* Render widget border */ jwidget_geometry const *g = jwidget_geometry_r(w); jdirs b = g->border; int color = g->border_color; int cw = jwidget_content_width(w); int ch = jwidget_content_height(w); int x1 = x + g->margin.left; int y1 = y + g->margin.top; int x2 = x1 + b.left + g->padding.left + cw + g->padding.right; int y2 = y1 + b.top + g->padding.top + ch + g->padding.bottom; if(g->border_style == J_BORDER_NONE || color == C_NONE) { } else if(g->border_style == J_BORDER_SOLID) { if(b.top > 0) drect(x1, y1, x2 + b.right - 1, y1 + b.top - 1, color); if(b.bottom > 0) drect(x1, y2, x2 + b.right - 1, y2 + b.bottom - 1, color); if(b.left > 0) drect(x1, y1 + b.top, x1 + b.left - 1, y2 - 1, color); if(b.right > 0) drect(x2, y1 + b.top, x2 + b.right - 1, y2 - 1, color); } /* TODO: jwidget_render(): More border types */ if(g->background_color != C_NONE) { int bgx = x1 + b.left; int bgy = y1 + b.top; if(bgx == 0 && bgy == 0 && x2 == DWIDTH && y2 == DHEIGHT) dclear(g->background_color); else drect(bgx, bgy, x2-1, y2-1, g->background_color); } /* Call the polymorphic render function at the top-left content point */ x += g->margin.left + b.left + g->padding.left; y += g->margin.top + b.top + g->padding.top; jwidget_poly const *poly = widget_types[w->type]; if(poly->render) { if(w->clipped) { struct dwindow win = { x, y, x+cw, y+ch }; win = intersect_dwindow(win, dwindow); /* 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); } } 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 //--- bool jwidget_event(void *w0, jevent e) { J_CAST(w) jwidget_poly const *poly = widget_types[w->type]; return poly->event && poly->event(w, e); } void jwidget_emit(void *w0, jevent e) { J_CAST(w) if(!w) return; if(e.source == NULL) e.source = w; if(!strcmp(jwidget_type(w), "jscene")) { jscene_queue_event((jscene *)w, e); } else { jwidget_emit(w->parent, e); } } //--- // Extension API //--- char const *jwidget_type(void *w0) { J_CAST(w) return widget_types[w->type]->name; } int j_register_widget(jwidget_poly *poly) { /* Resolve default behaviors */ if(!poly->csize) poly->csize = jwidget_poly_csize; if(!poly->layout) poly->layout = NULL; if(!poly->render) poly->render = jwidget_poly_render; if(!poly->event) poly->event = jwidget_poly_event; if(!poly->destroy) poly->destroy = NULL; for(int i = 0; i < WIDGET_TYPES_MAX; i++) { if(widget_types[i] == NULL) { widget_types[i] = poly; return i; } } return -1; } int j_register_event(void) { static int event_id = 0; event_id++; return event_id; }