aboutsummaryrefslogtreecommitdiff
path: root/src/libui_sdl/libui/unix
diff options
context:
space:
mode:
authorStapleButter <thetotalworm@gmail.com>2017-09-09 02:30:51 +0200
committerStapleButter <thetotalworm@gmail.com>2017-09-09 02:30:51 +0200
commit70e4841d311d68689724768157cc9cbfbde7a9fc (patch)
treeba9499f77d1258530a7e60aa6e1732c41d98161c /src/libui_sdl/libui/unix
parent81747d6c34eb159481a6ca3f283d065fa3568617 (diff)
another UI attempt, I guess.
sorry.
Diffstat (limited to 'src/libui_sdl/libui/unix')
-rw-r--r--src/libui_sdl/libui/unix/CMakeLists.txt85
-rw-r--r--src/libui_sdl/libui/unix/alloc.c84
-rw-r--r--src/libui_sdl/libui/unix/area.c633
-rw-r--r--src/libui_sdl/libui/unix/box.c159
-rw-r--r--src/libui_sdl/libui/unix/button.c55
-rw-r--r--src/libui_sdl/libui/unix/cellrendererbutton.c299
-rw-r--r--src/libui_sdl/libui/unix/checkbox.c78
-rw-r--r--src/libui_sdl/libui/unix/child.c120
-rw-r--r--src/libui_sdl/libui/unix/colorbutton.c80
-rw-r--r--src/libui_sdl/libui/unix/combobox.c66
-rw-r--r--src/libui_sdl/libui/unix/control.c14
-rw-r--r--src/libui_sdl/libui/unix/datetimepicker.c599
-rw-r--r--src/libui_sdl/libui/unix/debug.c14
-rw-r--r--src/libui_sdl/libui/unix/draw.c141
-rw-r--r--src/libui_sdl/libui/unix/draw.h13
-rw-r--r--src/libui_sdl/libui/unix/drawmatrix.c109
-rw-r--r--src/libui_sdl/libui/unix/drawpath.c199
-rw-r--r--src/libui_sdl/libui/unix/drawtext.c293
-rw-r--r--src/libui_sdl/libui/unix/editablecombo.c79
-rw-r--r--src/libui_sdl/libui/unix/entry.c97
-rw-r--r--src/libui_sdl/libui/unix/fontbutton.c70
-rw-r--r--src/libui_sdl/libui/unix/form.c159
-rw-r--r--src/libui_sdl/libui/unix/future.c42
-rw-r--r--src/libui_sdl/libui/unix/graphemes.c31
-rw-r--r--src/libui_sdl/libui/unix/grid.c141
-rw-r--r--src/libui_sdl/libui/unix/group.c89
-rw-r--r--src/libui_sdl/libui/unix/image.c120
-rw-r--r--src/libui_sdl/libui/unix/label.c36
-rw-r--r--src/libui_sdl/libui/unix/main.c108
-rw-r--r--src/libui_sdl/libui/unix/menu.c366
-rw-r--r--src/libui_sdl/libui/unix/multilineentry.c124
-rw-r--r--src/libui_sdl/libui/unix/progressbar.c71
-rw-r--r--src/libui_sdl/libui/unix/radiobuttons.c121
-rw-r--r--src/libui_sdl/libui/unix/separator.c34
-rw-r--r--src/libui_sdl/libui/unix/slider.c71
-rw-r--r--src/libui_sdl/libui/unix/spinbox.c72
-rw-r--r--src/libui_sdl/libui/unix/stddialogs.c66
-rw-r--r--src/libui_sdl/libui/unix/tab.c97
-rw-r--r--src/libui_sdl/libui/unix/text.c12
-rw-r--r--src/libui_sdl/libui/unix/uipriv_unix.h65
-rw-r--r--src/libui_sdl/libui/unix/util.c10
-rw-r--r--src/libui_sdl/libui/unix/window.c279
42 files changed, 5401 insertions, 0 deletions
diff --git a/src/libui_sdl/libui/unix/CMakeLists.txt b/src/libui_sdl/libui/unix/CMakeLists.txt
new file mode 100644
index 0000000..9300bcb
--- /dev/null
+++ b/src/libui_sdl/libui/unix/CMakeLists.txt
@@ -0,0 +1,85 @@
+# 3 june 2016
+
+find_package(PkgConfig REQUIRED)
+pkg_check_modules(GTK REQUIRED gtk+-3.0)
+
+list(APPEND _LIBUI_SOURCES
+ unix/alloc.c
+ unix/area.c
+ unix/box.c
+ unix/button.c
+ unix/cellrendererbutton.c
+ unix/checkbox.c
+ unix/child.c
+ unix/colorbutton.c
+ unix/combobox.c
+ unix/control.c
+ unix/datetimepicker.c
+ unix/debug.c
+ unix/draw.c
+ unix/drawmatrix.c
+ unix/drawpath.c
+ unix/drawtext.c
+ unix/editablecombo.c
+ unix/entry.c
+ unix/fontbutton.c
+ unix/form.c
+ unix/future.c
+ unix/graphemes.c
+ unix/grid.c
+ unix/group.c
+ unix/image.c
+ unix/label.c
+ unix/main.c
+ unix/menu.c
+ unix/multilineentry.c
+ unix/progressbar.c
+ unix/radiobuttons.c
+ unix/separator.c
+ unix/slider.c
+ unix/spinbox.c
+ unix/stddialogs.c
+ unix/tab.c
+ unix/text.c
+ unix/util.c
+ unix/window.c
+)
+set(_LIBUI_SOURCES ${_LIBUI_SOURCES} PARENT_SCOPE)
+
+list(APPEND _LIBUI_INCLUDEDIRS
+ unix
+)
+set(_LIBUI_INCLUDEDIRS _LIBUI_INCLUDEDIRS PARENT_SCOPE)
+
+set(_LIBUINAME libui PARENT_SCOPE)
+if(NOT BUILD_SHARED_LIBS)
+ set(_LIBUINAME libui-temporary PARENT_SCOPE)
+endif()
+macro(_handle_static)
+ set_target_properties(${_LIBUINAME} PROPERTIES
+ ARCHIVE_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}")
+ set(_aname $<TARGET_FILE:${_LIBUINAME}>)
+ set(_oname libui-combined.o)
+ add_custom_command(
+ OUTPUT ${_oname}
+ COMMAND
+ ld -r --whole-archive ${_aname} -o ${_oname}
+ COMMAND
+ objcopy --localize-hidden ${_oname}
+ COMMENT "Removing hidden symbols")
+ add_library(libui STATIC ${_oname})
+ # otherwise cmake won't know which linker to use
+ set_target_properties(libui PROPERTIES
+ LINKER_LANGUAGE C)
+ set(_aname)
+ set(_oname)
+endmacro()
+
+# TODO the other variables don't work?
+set(_LIBUI_CFLAGS
+ ${GTK_CFLAGS}
+PARENT_SCOPE)
+
+set(_LIBUI_LIBS
+ ${GTK_LDFLAGS} m ${CMAKE_DL_LIBS}
+PARENT_SCOPE)
diff --git a/src/libui_sdl/libui/unix/alloc.c b/src/libui_sdl/libui/unix/alloc.c
new file mode 100644
index 0000000..2561efa
--- /dev/null
+++ b/src/libui_sdl/libui/unix/alloc.c
@@ -0,0 +1,84 @@
+// 7 april 2015
+#include <string.h>
+#include "uipriv_unix.h"
+
+static GPtrArray *allocations;
+
+#define UINT8(p) ((uint8_t *) (p))
+#define PVOID(p) ((void *) (p))
+#define EXTRA (sizeof (size_t) + sizeof (const char **))
+#define DATA(p) PVOID(UINT8(p) + EXTRA)
+#define BASE(p) PVOID(UINT8(p) - EXTRA)
+#define SIZE(p) ((size_t *) (p))
+#define CCHAR(p) ((const char **) (p))
+#define TYPE(p) CCHAR(UINT8(p) + sizeof (size_t))
+
+void initAlloc(void)
+{
+ allocations = g_ptr_array_new();
+}
+
+static void uninitComplain(gpointer ptr, gpointer data)
+{
+ char **str = (char **) data;
+ char *str2;
+
+ if (*str == NULL)
+ *str = g_strdup_printf("");
+ str2 = g_strdup_printf("%s%p %s\n", *str, ptr, *TYPE(ptr));
+ g_free(*str);
+ *str = str2;
+}
+
+void uninitAlloc(void)
+{
+ char *str = NULL;
+
+ if (allocations->len == 0) {
+ g_ptr_array_free(allocations, TRUE);
+ return;
+ }
+ g_ptr_array_foreach(allocations, uninitComplain, &str);
+ userbug("Some data was leaked; either you left a uiControl lying around or there's a bug in libui itself. Leaked data:\n%s", str);
+ g_free(str);
+}
+
+void *uiAlloc(size_t size, const char *type)
+{
+ void *out;
+
+ out = g_malloc0(EXTRA + size);
+ *SIZE(out) = size;
+ *TYPE(out) = type;
+ g_ptr_array_add(allocations, out);
+ return DATA(out);
+}
+
+void *uiRealloc(void *p, size_t new, const char *type)
+{
+ void *out;
+ size_t *s;
+
+ if (p == NULL)
+ return uiAlloc(new, type);
+ p = BASE(p);
+ out = g_realloc(p, EXTRA + new);
+ s = SIZE(out);
+ if (new <= *s)
+ memset(((uint8_t *) DATA(out)) + *s, 0, new - *s);
+ *s = new;
+ if (g_ptr_array_remove(allocations, p) == FALSE)
+ implbug("%p not found in allocations array in uiRealloc()", p);
+ g_ptr_array_add(allocations, out);
+ return DATA(out);
+}
+
+void uiFree(void *p)
+{
+ if (p == NULL)
+ implbug("attempt to uiFree(NULL)");
+ p = BASE(p);
+ g_free(p);
+ if (g_ptr_array_remove(allocations, p) == FALSE)
+ implbug("%p not found in allocations array in uiFree()", p);
+}
diff --git a/src/libui_sdl/libui/unix/area.c b/src/libui_sdl/libui/unix/area.c
new file mode 100644
index 0000000..c46447c
--- /dev/null
+++ b/src/libui_sdl/libui/unix/area.c
@@ -0,0 +1,633 @@
+// 4 september 2015
+#include "uipriv_unix.h"
+
+// notes:
+// - G_DECLARE_DERIVABLE/FINAL_INTERFACE() requires glib 2.44 and that's starting with debian stretch (testing) (GTK+ 3.18) and ubuntu 15.04 (GTK+ 3.14) - debian jessie has 2.42 (GTK+ 3.14)
+#define areaWidgetType (areaWidget_get_type())
+#define areaWidget(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), areaWidgetType, areaWidget))
+#define isAreaWidget(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), areaWidgetType))
+#define areaWidgetClass(class) (G_TYPE_CHECK_CLASS_CAST((class), areaWidgetType, areaWidgetClass))
+#define isAreaWidgetClass(class) (G_TYPE_CHECK_CLASS_TYPE((class), areaWidget))
+#define getAreaWidgetClass(obj) (G_TYPE_INSTANCE_GET_CLASS((obj), areaWidgetType, areaWidgetClass))
+
+typedef struct areaWidget areaWidget;
+typedef struct areaWidgetClass areaWidgetClass;
+
+struct areaWidget {
+ GtkDrawingArea parent_instance;
+ uiArea *a;
+ // construct-only parameters aare not set until after the init() function has returned
+ // we need this particular object available during init(), so put it here instead of in uiArea
+ // keep a pointer in uiArea for convenience, though
+ clickCounter cc;
+};
+
+struct areaWidgetClass {
+ GtkDrawingAreaClass parent_class;
+};
+
+struct uiArea {
+ uiUnixControl c;
+ GtkWidget *widget; // either swidget or areaWidget depending on whether it is scrolling
+
+ GtkWidget *swidget;
+ GtkContainer *scontainer;
+ GtkScrolledWindow *sw;
+
+ GtkWidget *areaWidget;
+ GtkDrawingArea *drawingArea;
+ areaWidget *area;
+
+ uiAreaHandler *ah;
+
+ gboolean scrolling;
+ int scrollWidth;
+ int scrollHeight;
+
+ // note that this is a pointer; see above
+ clickCounter *cc;
+
+ // for user window drags
+ GdkEventButton *dragevent;
+};
+
+G_DEFINE_TYPE(areaWidget, areaWidget, GTK_TYPE_DRAWING_AREA)
+
+static void areaWidget_init(areaWidget *aw)
+{
+ // for events
+ gtk_widget_add_events(GTK_WIDGET(aw),
+ GDK_POINTER_MOTION_MASK |
+ GDK_BUTTON_MOTION_MASK |
+ GDK_BUTTON_PRESS_MASK |
+ GDK_BUTTON_RELEASE_MASK |
+ GDK_KEY_PRESS_MASK |
+ GDK_KEY_RELEASE_MASK |
+ GDK_ENTER_NOTIFY_MASK |
+ GDK_LEAVE_NOTIFY_MASK);
+
+ gtk_widget_set_can_focus(GTK_WIDGET(aw), TRUE);
+
+ clickCounterReset(&(aw->cc));
+}
+
+static void areaWidget_dispose(GObject *obj)
+{
+ G_OBJECT_CLASS(areaWidget_parent_class)->dispose(obj);
+}
+
+static void areaWidget_finalize(GObject *obj)
+{
+ G_OBJECT_CLASS(areaWidget_parent_class)->finalize(obj);
+}
+
+static void areaWidget_size_allocate(GtkWidget *w, GtkAllocation *allocation)
+{
+ areaWidget *aw = areaWidget(w);
+ uiArea *a = aw->a;
+
+ // GtkDrawingArea has a size_allocate() implementation; we need to call it
+ // this will call gtk_widget_set_allocation() for us
+ GTK_WIDGET_CLASS(areaWidget_parent_class)->size_allocate(w, allocation);
+
+ if (!a->scrolling)
+ // we must redraw everything on resize because Windows requires it
+ gtk_widget_queue_resize(w);
+}
+
+static void loadAreaSize(uiArea *a, double *width, double *height)
+{
+ GtkAllocation allocation;
+
+ *width = 0;
+ *height = 0;
+ // don't provide size information for scrolling areas
+ if (!a->scrolling) {
+ gtk_widget_get_allocation(a->areaWidget, &allocation);
+ // these are already in drawing space coordinates
+ // for drawing, the size of drawing space has the same value as the widget allocation
+ // thanks to tristan in irc.gimp.net/#gtk+
+ *width = allocation.width;
+ *height = allocation.height;
+ }
+}
+
+static gboolean areaWidget_draw(GtkWidget *w, cairo_t *cr)
+{
+ areaWidget *aw = areaWidget(w);
+ uiArea *a = aw->a;
+ uiAreaDrawParams dp;
+ double clipX0, clipY0, clipX1, clipY1;
+
+ dp.Context = newContext(cr);
+
+ loadAreaSize(a, &(dp.AreaWidth), &(dp.AreaHeight));
+
+ cairo_clip_extents(cr, &clipX0, &clipY0, &clipX1, &clipY1);
+ dp.ClipX = clipX0;
+ dp.ClipY = clipY0;
+ dp.ClipWidth = clipX1 - clipX0;
+ dp.ClipHeight = clipY1 - clipY0;
+
+ // no need to save or restore the graphics state to reset transformations; GTK+ does that for us
+ (*(a->ah->Draw))(a->ah, a, &dp);
+
+ freeContext(dp.Context);
+ return FALSE;
+}
+
+// to do this properly for scrolling areas, we need to
+// - return the same value for min and nat
+// - call gtk_widget_queue_resize() when the size changes
+// thanks to Company in irc.gimp.net/#gtk+
+static void areaWidget_get_preferred_height(GtkWidget *w, gint *min, gint *nat)
+{
+ areaWidget *aw = areaWidget(w);
+ uiArea *a = aw->a;
+
+ // always chain up just in case
+ GTK_WIDGET_CLASS(areaWidget_parent_class)->get_preferred_height(w, min, nat);
+ if (a->scrolling) {
+ *min = a->scrollHeight;
+ *nat = a->scrollHeight;
+ }
+}
+
+static void areaWidget_get_preferred_width(GtkWidget *w, gint *min, gint *nat)
+{
+ areaWidget *aw = areaWidget(w);
+ uiArea *a = aw->a;
+
+ // always chain up just in case
+ GTK_WIDGET_CLASS(areaWidget_parent_class)->get_preferred_width(w, min, nat);
+ if (a->scrolling) {
+ *min = a->scrollWidth;
+ *nat = a->scrollWidth;
+ }
+}
+
+static guint translateModifiers(guint state, GdkWindow *window)
+{
+ GdkModifierType statetype;
+
+ // GDK doesn't initialize the modifier flags fully; we have to explicitly tell it to (thanks to Daniel_S and daniels (two different people) in irc.gimp.net/#gtk+)
+ statetype = state;
+ gdk_keymap_add_virtual_modifiers(
+ gdk_keymap_get_for_display(gdk_window_get_display(window)),
+ &statetype);
+ return statetype;
+}
+
+static uiModifiers toModifiers(guint state)
+{
+ uiModifiers m;
+
+ m = 0;
+ if ((state & GDK_CONTROL_MASK) != 0)
+ m |= uiModifierCtrl;
+ if ((state & GDK_META_MASK) != 0)
+ m |= uiModifierAlt;
+ if ((state & GDK_MOD1_MASK) != 0) // GTK+ itself requires this to be Alt (just read through gtkaccelgroup.c)
+ m |= uiModifierAlt;
+ if ((state & GDK_SHIFT_MASK) != 0)
+ m |= uiModifierShift;
+ if ((state & GDK_SUPER_MASK) != 0)
+ m |= uiModifierSuper;
+ return m;
+}
+
+// capture on drag is done automatically on GTK+
+static void finishMouseEvent(uiArea *a, uiAreaMouseEvent *me, guint mb, gdouble x, gdouble y, guint state, GdkWindow *window)
+{
+ // on GTK+, mouse buttons 4-7 are for scrolling; if we got here, that's a mistake
+ if (mb >= 4 && mb <= 7)
+ return;
+ // if the button ID >= 8, continue counting from 4, as in the MouseEvent spec
+ if (me->Down >= 8)
+ me->Down -= 4;
+ if (me->Up >= 8)
+ me->Up -= 4;
+
+ state = translateModifiers(state, window);
+ me->Modifiers = toModifiers(state);
+
+ // the mb != # checks exclude the Up/Down button from Held
+ me->Held1To64 = 0;
+ if (mb != 1 && (state & GDK_BUTTON1_MASK) != 0)
+ me->Held1To64 |= 1 << 0;
+ if (mb != 2 && (state & GDK_BUTTON2_MASK) != 0)
+ me->Held1To64 |= 1 << 1;
+ if (mb != 3 && (state & GDK_BUTTON3_MASK) != 0)
+ me->Held1To64 |= 1 << 2;
+ // don't check GDK_BUTTON4_MASK or GDK_BUTTON5_MASK because those are for the scrolling buttons mentioned above
+ // GDK expressly does not support any more buttons in the GdkModifierType; see https://git.gnome.org/browse/gtk+/tree/gdk/x11/gdkdevice-xi2.c#n763 (thanks mclasen in irc.gimp.net/#gtk+)
+
+ // these are already in drawing space coordinates
+ // the size of drawing space has the same value as the widget allocation
+ // thanks to tristan in irc.gimp.net/#gtk+
+ me->X = x;
+ me->Y = y;
+
+ loadAreaSize(a, &(me->AreaWidth), &(me->AreaHeight));
+
+ (*(a->ah->MouseEvent))(a->ah, a, me);
+}
+
+static gboolean areaWidget_button_press_event(GtkWidget *w, GdkEventButton *e)
+{
+ areaWidget *aw = areaWidget(w);
+ uiArea *a = aw->a;
+ gint maxTime, maxDistance;
+ GtkSettings *settings;
+ uiAreaMouseEvent me;
+
+ // clicking doesn't automatically transfer keyboard focus; we must do so manually (thanks tristan in irc.gimp.net/#gtk+)
+ gtk_widget_grab_focus(w);
+
+ // we handle multiple clicks ourselves here, in the same way as we do on Windows
+ if (e->type != GDK_BUTTON_PRESS)
+ // ignore GDK's generated double-clicks and beyond
+ return GDK_EVENT_PROPAGATE;
+ settings = gtk_widget_get_settings(w);
+ g_object_get(settings,
+ "gtk-double-click-time", &maxTime,
+ "gtk-double-click-distance", &maxDistance,
+ NULL);
+ // don't unref settings; it's transfer-none (thanks gregier in irc.gimp.net/#gtk+)
+ // e->time is guint32
+ // e->x and e->y are floating-point; just make them 32-bit integers
+ // maxTime and maxDistance... are gint, which *should* fit, hopefully...
+ me.Count = clickCounterClick(a->cc, me.Down,
+ e->x, e->y,
+ e->time, maxTime,
+ maxDistance, maxDistance);
+
+ me.Down = e->button;
+ me.Up = 0;
+
+ // and set things up for window drags
+ a->dragevent = e;
+ finishMouseEvent(a, &me, e->button, e->x, e->y, e->state, e->window);
+ a->dragevent = NULL;
+ return GDK_EVENT_PROPAGATE;
+}
+
+static gboolean areaWidget_button_release_event(GtkWidget *w, GdkEventButton *e)
+{
+ areaWidget *aw = areaWidget(w);
+ uiArea *a = aw->a;
+ uiAreaMouseEvent me;
+
+ me.Down = 0;
+ me.Up = e->button;
+ me.Count = 0;
+ finishMouseEvent(a, &me, e->button, e->x, e->y, e->state, e->window);
+ return GDK_EVENT_PROPAGATE;
+}
+
+static gboolean areaWidget_motion_notify_event(GtkWidget *w, GdkEventMotion *e)
+{
+ areaWidget *aw = areaWidget(w);
+ uiArea *a = aw->a;
+ uiAreaMouseEvent me;
+
+ me.Down = 0;
+ me.Up = 0;
+ me.Count = 0;
+ finishMouseEvent(a, &me, 0, e->x, e->y, e->state, e->window);
+ return GDK_EVENT_PROPAGATE;
+}
+
+// we want switching away from the control to reset the double-click counter, like with WM_ACTIVATE on Windows
+// according to tristan in irc.gimp.net/#gtk+, doing this on both enter-notify-event and leave-notify-event is correct (and it seems to be true in my own tests; plus the events DO get sent when switching programs with the keyboard (just pointing that out))
+static gboolean onCrossing(areaWidget *aw, int left)
+{
+ uiArea *a = aw->a;
+
+ (*(a->ah->MouseCrossed))(a->ah, a, left);
+ clickCounterReset(a->cc);
+ return GDK_EVENT_PROPAGATE;
+}
+
+static gboolean areaWidget_enter_notify_event(GtkWidget *w, GdkEventCrossing *e)
+{
+ return onCrossing(areaWidget(w), 0);
+}
+
+static gboolean areaWidget_leave_notify_event(GtkWidget *w, GdkEventCrossing *e)
+{
+ return onCrossing(areaWidget(w), 1);
+}
+
+// note: there is no equivalent to WM_CAPTURECHANGED on GTK+; there literally is no way to break a grab like that (at least not on X11 and Wayland)
+// even if I invoke the task switcher and switch processes, the mouse grab will still be held until I let go of all buttons
+// therefore, no DragBroken()
+
+// we use GDK_KEY_Print as a sentinel because libui will never support the print screen key; that key belongs to the user
+
+static const struct {
+ guint keyval;
+ uiExtKey extkey;
+} extKeys[] = {
+ { GDK_KEY_Escape, uiExtKeyEscape },
+ { GDK_KEY_Insert, uiExtKeyInsert },
+ { GDK_KEY_Delete, uiExtKeyDelete },
+ { GDK_KEY_Home, uiExtKeyHome },
+ { GDK_KEY_End, uiExtKeyEnd },
+ { GDK_KEY_Page_Up, uiExtKeyPageUp },
+ { GDK_KEY_Page_Down, uiExtKeyPageDown },
+ { GDK_KEY_Up, uiExtKeyUp },
+ { GDK_KEY_Down, uiExtKeyDown },
+ { GDK_KEY_Left, uiExtKeyLeft },
+ { GDK_KEY_Right, uiExtKeyRight },
+ { GDK_KEY_F1, uiExtKeyF1 },
+ { GDK_KEY_F2, uiExtKeyF2 },
+ { GDK_KEY_F3, uiExtKeyF3 },
+ { GDK_KEY_F4, uiExtKeyF4 },
+ { GDK_KEY_F5, uiExtKeyF5 },
+ { GDK_KEY_F6, uiExtKeyF6 },
+ { GDK_KEY_F7, uiExtKeyF7 },
+ { GDK_KEY_F8, uiExtKeyF8 },
+ { GDK_KEY_F9, uiExtKeyF9 },
+ { GDK_KEY_F10, uiExtKeyF10 },
+ { GDK_KEY_F11, uiExtKeyF11 },
+ { GDK_KEY_F12, uiExtKeyF12 },
+ // numpad numeric keys and . are handled in events.c
+ { GDK_KEY_KP_Enter, uiExtKeyNEnter },
+ { GDK_KEY_KP_Add, uiExtKeyNAdd },
+ { GDK_KEY_KP_Subtract, uiExtKeyNSubtract },
+ { GDK_KEY_KP_Multiply, uiExtKeyNMultiply },
+ { GDK_KEY_KP_Divide, uiExtKeyNDivide },
+ { GDK_KEY_Print, 0 },
+};
+
+static const struct {
+ guint keyval;
+ uiModifiers mod;
+} modKeys[] = {
+ { GDK_KEY_Control_L, uiModifierCtrl },
+ { GDK_KEY_Control_R, uiModifierCtrl },
+ { GDK_KEY_Alt_L, uiModifierAlt },
+ { GDK_KEY_Alt_R, uiModifierAlt },
+ { GDK_KEY_Meta_L, uiModifierAlt },
+ { GDK_KEY_Meta_R, uiModifierAlt },
+ { GDK_KEY_Shift_L, uiModifierShift },
+ { GDK_KEY_Shift_R, uiModifierShift },
+ { GDK_KEY_Super_L, uiModifierSuper },
+ { GDK_KEY_Super_R, uiModifierSuper },
+ { GDK_KEY_Print, 0 },
+};
+
+static int areaKeyEvent(uiArea *a, int up, GdkEventKey *e)
+{
+ uiAreaKeyEvent ke;
+ guint state;
+ int i;
+
+ ke.Key = 0;
+ ke.ExtKey = 0;
+ ke.Modifier = 0;
+
+ state = translateModifiers(e->state, e->window);
+ ke.Modifiers = toModifiers(state);
+
+ ke.Up = up;
+
+ for (i = 0; extKeys[i].keyval != GDK_KEY_Print; i++)
+ if (extKeys[i].keyval == e->keyval) {
+ ke.ExtKey = extKeys[i].extkey;
+ goto keyFound;
+ }
+
+ for (i = 0; modKeys[i].keyval != GDK_KEY_Print; i++)
+ if (modKeys[i].keyval == e->keyval) {
+ ke.Modifier = modKeys[i].mod;
+ // don't include the modifier in ke.Modifiers
+ ke.Modifiers &= ~ke.Modifier;
+ goto keyFound;
+ }
+
+ if (fromScancode(e->hardware_keycode - 8, &ke))
+ goto keyFound;
+
+ // no supported key found; treat as unhandled
+ return 0;
+
+keyFound:
+ return (*(a->ah->KeyEvent))(a->ah, a, &ke);
+}
+
+static gboolean areaWidget_key_press_event(GtkWidget *w, GdkEventKey *e)
+{
+ areaWidget *aw = areaWidget(w);
+ uiArea *a = aw->a;
+
+ if (areaKeyEvent(a, 0, e))
+ return GDK_EVENT_STOP;
+ return GDK_EVENT_PROPAGATE;
+}
+
+static gboolean areaWidget_key_release_event(GtkWidget *w, GdkEventKey *e)
+{
+ areaWidget *aw = areaWidget(w);
+ uiArea *a = aw->a;
+
+ if (areaKeyEvent(a, 1, e))
+ return GDK_EVENT_STOP;
+ return GDK_EVENT_PROPAGATE;
+}
+
+enum {
+ pArea = 1,
+ nProps,
+};
+
+static GParamSpec *pspecArea;
+
+static void areaWidget_set_property(GObject *obj, guint prop, const GValue *value, GParamSpec *pspec)
+{
+ areaWidget *aw = areaWidget(obj);
+
+ switch (prop) {
+ case pArea:
+ aw->a = (uiArea *) g_value_get_pointer(value);
+ aw->a->cc = &(aw->cc);
+ return;
+ }
+ G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop, pspec);
+}
+
+static void areaWidget_get_property(GObject *obj, guint prop, GValue *value, GParamSpec *pspec)
+{
+ G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop, pspec);
+}
+
+static void areaWidget_class_init(areaWidgetClass *class)
+{
+ G_OBJECT_CLASS(class)->dispose = areaWidget_dispose;
+ G_OBJECT_CLASS(class)->finalize = areaWidget_finalize;
+ G_OBJECT_CLASS(class)->set_property = areaWidget_set_property;
+ G_OBJECT_CLASS(class)->get_property = areaWidget_get_property;
+
+ GTK_WIDGET_CLASS(class)->size_allocate = areaWidget_size_allocate;
+ GTK_WIDGET_CLASS(class)->draw = areaWidget_draw;
+ GTK_WIDGET_CLASS(class)->get_preferred_height = areaWidget_get_preferred_height;
+ GTK_WIDGET_CLASS(class)->get_preferred_width = areaWidget_get_preferred_width;
+ GTK_WIDGET_CLASS(class)->button_press_event = areaWidget_button_press_event;
+ GTK_WIDGET_CLASS(class)->button_release_event = areaWidget_button_release_event;
+ GTK_WIDGET_CLASS(class)->motion_notify_event = areaWidget_motion_notify_event;
+ GTK_WIDGET_CLASS(class)->enter_notify_event = areaWidget_enter_notify_event;
+ GTK_WIDGET_CLASS(class)->leave_notify_event = areaWidget_leave_notify_event;
+ GTK_WIDGET_CLASS(class)->key_press_event = areaWidget_key_press_event;
+ GTK_WIDGET_CLASS(class)->key_release_event = areaWidget_key_release_event;
+
+ pspecArea = g_param_spec_pointer("libui-area",
+ "libui-area",
+ "uiArea.",
+ G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+ g_object_class_install_property(G_OBJECT_CLASS(class), pArea, pspecArea);
+}
+
+// control implementation
+
+uiUnixControlAllDefaults(uiArea)
+
+void uiAreaSetSize(uiArea *a, int width, int height)
+{
+ if (!a->scrolling)
+ userbug("You cannot call uiAreaSetSize() on a non-scrolling uiArea. (area: %p)", a);
+ a->scrollWidth = width;
+ a->scrollHeight = height;
+ gtk_widget_queue_resize(a->areaWidget);
+}
+
+void uiAreaQueueRedrawAll(uiArea *a)
+{
+ gtk_widget_queue_draw(a->areaWidget);
+}
+
+void uiAreaScrollTo(uiArea *a, double x, double y, double width, double height)
+{
+ // TODO
+ // TODO adjust adjustments and find source for that
+}
+
+void uiAreaBeginUserWindowMove(uiArea *a)
+{
+ GtkWidget *toplevel;
+
+ if (a->dragevent == NULL)
+ userbug("cannot call uiAreaBeginUserWindowMove() outside of a Mouse() with Down != 0");
+ // TODO don't we have a libui function for this? did I scrap it?
+ // TODO widget or areaWidget?
+ toplevel = gtk_widget_get_toplevel(a->widget);
+ if (toplevel == NULL) {
+ // TODO
+ return;
+ }
+ // the docs say to do this
+ if (!gtk_widget_is_toplevel(toplevel)) {
+ // TODO
+ return;
+ }
+ if (!GTK_IS_WINDOW(toplevel)) {
+ // TODO
+ return;
+ }
+ gtk_window_begin_move_drag(GTK_WINDOW(toplevel),
+ a->dragevent->button,
+ a->dragevent->x_root, // TODO are these correct?
+ a->dragevent->y_root,
+ a->dragevent->time);
+}
+
+static const GdkWindowEdge edges[] = {
+ [uiWindowResizeEdgeLeft] = GDK_WINDOW_EDGE_WEST,
+ [uiWindowResizeEdgeTop] = GDK_WINDOW_EDGE_NORTH,
+ [uiWindowResizeEdgeRight] = GDK_WINDOW_EDGE_EAST,
+ [uiWindowResizeEdgeBottom] = GDK_WINDOW_EDGE_SOUTH,
+ [uiWindowResizeEdgeTopLeft] = GDK_WINDOW_EDGE_NORTH_WEST,
+ [uiWindowResizeEdgeTopRight] = GDK_WINDOW_EDGE_NORTH_EAST,
+ [uiWindowResizeEdgeBottomLeft] = GDK_WINDOW_EDGE_SOUTH_WEST,
+ [uiWindowResizeEdgeBottomRight] = GDK_WINDOW_EDGE_SOUTH_EAST,
+};
+
+void uiAreaBeginUserWindowResize(uiArea *a, uiWindowResizeEdge edge)
+{
+ GtkWidget *toplevel;
+
+ if (a->dragevent == NULL)
+ userbug("cannot call uiAreaBeginUserWindowResize() outside of a Mouse() with Down != 0");
+ // TODO don't we have a libui function for this? did I scrap it?
+ // TODO widget or areaWidget?
+ toplevel = gtk_widget_get_toplevel(a->widget);
+ if (toplevel == NULL) {
+ // TODO
+ return;
+ }
+ // the docs say to do this
+ if (!gtk_widget_is_toplevel(toplevel)) {
+ // TODO
+ return;
+ }
+ if (!GTK_IS_WINDOW(toplevel)) {
+ // TODO
+ return;
+ }
+ gtk_window_begin_resize_drag(GTK_WINDOW(toplevel),
+ edges[edge],
+ a->dragevent->button,
+ a->dragevent->x_root, // TODO are these correct?
+ a->dragevent->y_root,
+ a->dragevent->time);
+}
+
+uiArea *uiNewArea(uiAreaHandler *ah)
+{
+ uiArea *a;
+
+ uiUnixNewControl(uiArea, a);
+
+ a->ah = ah;
+ a->scrolling = FALSE;
+
+ a->areaWidget = GTK_WIDGET(g_object_new(areaWidgetType,
+ "libui-area", a,
+ NULL));
+ a->drawingArea = GTK_DRAWING_AREA(a->areaWidget);
+ a->area = areaWidget(a->areaWidget);
+
+ a->widget = a->areaWidget;
+
+ return a;
+}
+
+uiArea *uiNewScrollingArea(uiAreaHandler *ah, int width, int height)
+{
+ uiArea *a;
+
+ uiUnixNewControl(uiArea, a);
+
+ a->ah = ah;
+ a->scrolling = TRUE;
+ a->scrollWidth = width;
+ a->scrollHeight = height;
+
+ a->swidget = gtk_scrolled_window_new(NULL, NULL);
+ a->scontainer = GTK_CONTAINER(a->swidget);
+ a->sw = GTK_SCROLLED_WINDOW(a->swidget);
+
+ a->areaWidget = GTK_WIDGET(g_object_new(areaWidgetType,
+ "libui-area", a,
+ NULL));
+ a->drawingArea = GTK_DRAWING_AREA(a->areaWidget);
+ a->area = areaWidget(a->areaWidget);
+
+ a->widget = a->swidget;
+
+ gtk_container_add(a->scontainer, a->areaWidget);
+ // and make the area visible; only the scrolled window's visibility is controlled by libui
+ gtk_widget_show(a->areaWidget);
+
+ return a;
+}
diff --git a/src/libui_sdl/libui/unix/box.c b/src/libui_sdl/libui/unix/box.c
new file mode 100644
index 0000000..23fb7f7
--- /dev/null
+++ b/src/libui_sdl/libui/unix/box.c
@@ -0,0 +1,159 @@
+// 7 april 2015
+#include "uipriv_unix.h"
+
+struct boxChild {
+ uiControl *c;
+ int stretchy;
+ gboolean oldhexpand;
+ GtkAlign oldhalign;
+ gboolean oldvexpand;
+ GtkAlign oldvalign;
+};
+
+struct uiBox {
+ uiUnixControl c;
+ GtkWidget *widget;
+ GtkContainer *container;
+ GtkBox *box;
+ GArray *controls;
+ int vertical;
+ int padded;
+ GtkSizeGroup *stretchygroup; // ensures all stretchy controls have the same size
+};
+
+uiUnixControlAllDefaultsExceptDestroy(uiBox)
+
+#define ctrl(b, i) &g_array_index(b->controls, struct boxChild, i)
+
+static void uiBoxDestroy(uiControl *c)
+{
+ uiBox *b = uiBox(c);
+ struct boxChild *bc;
+ guint i;
+
+ // kill the size group
+ g_object_unref(b->stretchygroup);
+ // free all controls
+ for (i = 0; i < b->controls->len; i++) {
+ bc = ctrl(b, i);
+ uiControlSetParent(bc->c, NULL);
+ // and make sure the widget itself stays alive
+ uiUnixControlSetContainer(uiUnixControl(bc->c), b->container, TRUE);
+ uiControlDestroy(bc->c);
+ }
+ g_array_free(b->controls, TRUE);
+ // and then ourselves
+ g_object_unref(b->widget);
+ uiFreeControl(uiControl(b));
+}
+
+void uiBoxAppend(uiBox *b, uiControl *c, int stretchy)
+{
+ struct boxChild bc;
+ GtkWidget *widget;
+
+ bc.c = c;
+ bc.stretchy = stretchy;
+ widget = GTK_WIDGET(uiControlHandle(bc.c));
+ bc.oldhexpand = gtk_widget_get_hexpand(widget);
+ bc.oldhalign = gtk_widget_get_halign(widget);
+ bc.oldvexpand = gtk_widget_get_vexpand(widget);
+ bc.oldvalign = gtk_widget_get_valign(widget);
+
+ if (bc.stretchy) {
+ if (b->vertical) {
+ gtk_widget_set_vexpand(widget, TRUE);
+ gtk_widget_set_valign(widget, GTK_ALIGN_FILL);
+ } else {
+ gtk_widget_set_hexpand(widget, TRUE);
+ gtk_widget_set_halign(widget, GTK_ALIGN_FILL);
+ }
+ gtk_size_group_add_widget(b->stretchygroup, widget);
+ } else
+ if (b->vertical)
+ gtk_widget_set_vexpand(widget, FALSE);
+ else
+ gtk_widget_set_hexpand(widget, FALSE);
+ // and make them fill the opposite direction
+ if (b->vertical) {
+ gtk_widget_set_hexpand(widget, TRUE);
+ gtk_widget_set_halign(widget, GTK_ALIGN_FILL);
+ } else {
+ gtk_widget_set_vexpand(widget, TRUE);
+ gtk_widget_set_valign(widget, GTK_ALIGN_FILL);
+ }
+
+ uiControlSetParent(bc.c, uiControl(b));
+ uiUnixControlSetContainer(uiUnixControl(bc.c), b->container, FALSE);
+ g_array_append_val(b->controls, bc);
+}
+
+void uiBoxDelete(uiBox *b, int index)
+{
+ struct boxChild *bc;
+ GtkWidget *widget;
+
+ bc = ctrl(b, index);
+ widget = GTK_WIDGET(uiControlHandle(bc->c));
+
+ uiControlSetParent(bc->c, NULL);
+ uiUnixControlSetContainer(uiUnixControl(bc->c), b->container, TRUE);
+
+ if (bc->stretchy)
+ gtk_size_group_remove_widget(b->stretchygroup, widget);
+ gtk_widget_set_hexpand(widget, bc->oldhexpand);
+ gtk_widget_set_halign(widget, bc->oldhalign);
+ gtk_widget_set_vexpand(widget, bc->oldvexpand);
+ gtk_widget_set_valign(widget, bc->oldvalign);
+
+ g_array_remove_index(b->controls, index);
+}
+
+int uiBoxPadded(uiBox *b)
+{
+ return b->padded;
+}
+
+void uiBoxSetPadded(uiBox *b, int padded)
+{
+ b->padded = padded;
+ if (b->padded)
+ if (b->vertical)
+ gtk_box_set_spacing(b->box, gtkYPadding);
+ else
+ gtk_box_set_spacing(b->box, gtkXPadding);
+ else
+ gtk_box_set_spacing(b->box, 0);
+}
+
+static uiBox *finishNewBox(GtkOrientation orientation)
+{
+ uiBox *b;
+
+ uiUnixNewControl(uiBox, b);
+
+ b->widget = gtk_box_new(orientation, 0);
+ b->container = GTK_CONTAINER(b->widget);
+ b->box = GTK_BOX(b->widget);
+
+ b->vertical = orientation == GTK_ORIENTATION_VERTICAL;
+
+ if (b->vertical)
+ b->stretchygroup = gtk_size_group_new(GTK_SIZE_GROUP_VERTICAL);
+ else
+ b->stretchygroup = gtk_size_group_new(GTK_SIZE_GROUP_HORIZONTAL);
+
+ b->controls = g_array_new(FALSE, TRUE, sizeof (struct boxChild));
+
+ return b;
+}
+
+uiBox *uiNewHorizontalBox(void)
+{
+ return finishNewBox(GTK_ORIENTATION_HORIZONTAL);
+}
+
+uiBox *uiNewVerticalBox(void)
+{
+ return finishNewBox(GTK_ORIENTATION_VERTICAL);
+}
diff --git a/src/libui_sdl/libui/unix/button.c b/src/libui_sdl/libui/unix/button.c
new file mode 100644
index 0000000..00a87f4
--- /dev/null
+++ b/src/libui_sdl/libui/unix/button.c
@@ -0,0 +1,55 @@
+// 10 june 2015
+#include "uipriv_unix.h"
+
+struct uiButton {
+ uiUnixControl c;
+ GtkWidget *widget;
+ GtkButton *button;
+ void (*onClicked)(uiButton *, void *);
+ void *onClickedData;
+};
+
+uiUnixControlAllDefaults(uiButton)
+
+static void onClicked(GtkButton *button, gpointer data)
+{
+ uiButton *b = uiButton(data);
+
+ (*(b->onClicked))(b, b->onClickedData);
+}
+
+static void defaultOnClicked(uiButton *b, void *data)
+{
+ // do nothing
+}
+
+char *uiButtonText(uiButton *b)
+{
+ return uiUnixStrdupText(gtk_button_get_label(b->button));
+}
+
+void uiButtonSetText(uiButton *b, const char *text)
+{
+ gtk_button_set_label(b->button, text);
+}
+
+void uiButtonOnClicked(uiButton *b, void (*f)(uiButton *, void *), void *data)
+{
+ b->onClicked = f;
+ b->onClickedData = data;
+}
+
+uiButton *uiNewButton(const char *text)
+{
+ uiButton *b;
+
+ uiUnixNewControl(uiButton, b);
+
+ b->widget = gtk_button_new_with_label(text);
+ b->button = GTK_BUTTON(b->widget);
+
+ g_signal_connect(b->widget, "clicked", G_CALLBACK(onClicked), b);
+ uiButtonOnClicked(b, defaultOnClicked, NULL);
+
+ return b;
+}
diff --git a/src/libui_sdl/libui/unix/cellrendererbutton.c b/src/libui_sdl/libui/unix/cellrendererbutton.c
new file mode 100644
index 0000000..e3bbf48
--- /dev/null
+++ b/src/libui_sdl/libui/unix/cellrendererbutton.c
@@ -0,0 +1,299 @@
+// 28 june 2016
+#include "uipriv_unix.h"
+
+// TODOs
+// - it's a rather tight fit
+// - selected row text color is white
+// - resizing a column with a button in it crashes the program
+// - accessibility
+// - right side too big?
+
+#define cellRendererButtonType (cellRendererButton_get_type())
+#define cellRendererButton(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), cellRendererButtonType, cellRendererButton))
+#define isCellRendererButton(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), cellRendererButtonType))
+#define cellRendererButtonClass(class) (G_TYPE_CHECK_CLASS_CAST((class), cellRendererButtonType, cellRendererButtonClass))
+#define isCellRendererButtonClass(class) (G_TYPE_CHECK_CLASS_TYPE((class), cellRendererButton))
+#define getCellRendererButtonClass(obj) (G_TYPE_INSTANCE_GET_CLASS((obj), cellRendererButtonType, cellRendererButtonClass))
+
+typedef struct cellRendererButton cellRendererButton;
+typedef struct cellRendererButtonClass cellRendererButtonClass;
+
+struct cellRendererButton {
+ GtkCellRenderer parent_instance;
+ char *text;
+};
+
+struct cellRendererButtonClass {
+ GtkCellRendererClass parent_class;
+};
+
+G_DEFINE_TYPE(cellRendererButton, cellRendererButton, GTK_TYPE_CELL_RENDERER)
+
+static void cellRendererButton_init(cellRendererButton *c)
+{
+ g_object_set(c, "mode", GTK_CELL_RENDERER_MODE_ACTIVATABLE, NULL);
+ // the standard cell renderers all do this
+ gtk_cell_renderer_set_padding(GTK_CELL_RENDERER(c), 2, 2);
+}
+
+static void cellRendererButton_dispose(GObject *obj)
+{
+ G_OBJECT_CLASS(cellRendererButton_parent_class)->dispose(obj);
+}
+
+static void cellRendererButton_finalize(GObject *obj)
+{
+ cellRendererButton *c = cellRendererButton(obj);
+
+ if (c->text != NULL) {
+ g_free(c->text);
+ c->text = NULL;
+ }
+ G_OBJECT_CLASS(cellRendererButton_parent_class)->finalize(obj);
+}
+
+static GtkSizeRequestMode cellRendererButton_get_request_mode(GtkCellRenderer *r)
+{
+ return GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH;
+}
+
+// this is basically what GtkCellRendererToggle did in 3.10 and does in 3.20, as well as what the Foreign Drawing gtk3-demo demo does
+static GtkStyleContext *setButtonStyle(GtkWidget *widget)
+{
+ GtkStyleContext *base, *context;
+ GtkWidgetPath *path;
+
+ base = gtk_widget_get_style_context(widget);
+ context = gtk_style_context_new();
+
+ path = gtk_widget_path_copy(gtk_style_context_get_path(base));
+ gtk_widget_path_append_type(path, G_TYPE_NONE);
+ if (!FUTURE_gtk_widget_path_iter_set_object_name(path, -1, "button"))
+ // not on 3.20; try the type
+ gtk_widget_path_iter_set_object_type(path, -1, GTK_TYPE_BUTTON);
+
+ gtk_style_context_set_path(context, path);
+ gtk_style_context_set_parent(context, base);
+ // the gtk3-demo example (which says we need to do this) uses gtk_widget_path_iter_get_state(path, -1) but that's not available until 3.14
+ // TODO make a future for that too
+ gtk_style_context_set_state(context, gtk_style_context_get_state(base));
+ gtk_widget_path_unref(path);
+
+ // and if the above widget path screwery stil doesn't work, this will
+ gtk_style_context_add_class(context, GTK_STYLE_CLASS_BUTTON);
+
+ return context;
+}
+
+void unsetButtonStyle(GtkStyleContext *context)
+{
+ g_object_unref(context);
+}
+
+// this is based on what GtkCellRendererText does
+static void cellRendererButton_get_preferred_width(GtkCellRenderer *r, GtkWidget *widget, gint *minimum, gint *natural)
+{
+ cellRendererButton *c = cellRendererButton(r);
+ gint xpad;
+ PangoLayout *layout;
+ PangoRectangle rect;
+ gint out;
+
+ gtk_cell_renderer_get_padding(GTK_CELL_RENDERER(c), &xpad, NULL);
+
+ layout = gtk_widget_create_pango_layout(widget, c->text);
+ pango_layout_set_width(layout, -1);
+ pango_layout_get_extents(layout, NULL, &rect);
+ g_object_unref(layout);
+
+ out = 2 * xpad + PANGO_PIXELS_CEIL(rect.width);
+ if (minimum != NULL)
+ *minimum = out;
+ if (natural != NULL)
+ *natural = out;
+}
+
+// this is based on what GtkCellRendererText does
+static void cellRendererButton_get_preferred_height_for_width(GtkCellRenderer *r, GtkWidget *widget, gint width, gint *minimum, gint *natural)
+{
+ cellRendererButton *c = cellRendererButton(r);
+ gint xpad, ypad;
+ PangoLayout *layout;
+ gint height;
+ gint out;
+
+ gtk_cell_renderer_get_padding(GTK_CELL_RENDERER(c), &xpad, &ypad);
+
+ layout = gtk_widget_create_pango_layout(widget, c->text);
+ pango_layout_set_width(layout, ((2 * xpad + width) * PANGO_SCALE));
+ pango_layout_get_pixel_size(layout, NULL, &height);
+ g_object_unref(layout);
+
+ out = 2 * ypad + height;
+ if (minimum != NULL)
+ *minimum = out;
+ if (natural != NULL)
+ *natural = out;
+}
+
+// this is basically what GtkCellRendererText does
+static void cellRendererButton_get_preferred_height(GtkCellRenderer *r, GtkWidget *widget, gint *minimum, gint *natural)
+{
+ gint width;
+
+ gtk_cell_renderer_get_preferred_width(r, widget, &width, NULL);
+ gtk_cell_renderer_get_preferred_height_for_width(r, widget, width, minimum, natural);
+}
+
+// this is based on what GtkCellRendererText does
+static void cellRendererButton_get_aligned_area(GtkCellRenderer *r, GtkWidget *widget, GtkCellRendererState flags, const GdkRectangle *cell_area, GdkRectangle *aligned_area)
+{
+ cellRendererButton *c = cellRendererButton(r);
+ gint xpad, ypad;
+ PangoLayout *layout;
+ PangoRectangle rect;
+ gfloat xalign, yalign;
+ gint xoffset, yoffset;
+
+ gtk_cell_renderer_get_padding(GTK_CELL_RENDERER(c), &xpad, &ypad);
+
+ layout = gtk_widget_create_pango_layout(widget, c->text);
+ pango_layout_set_width(layout, -1);
+ pango_layout_get_pixel_extents(layout, NULL, &rect);
+
+ xoffset = 0;
+ yoffset = 0;
+ if (cell_area != NULL) {
+ gtk_cell_renderer_get_alignment(GTK_CELL_RENDERER(c), &xalign, &yalign);
+ xoffset = cell_area->width - (2 * xpad + rect.width);
+ // use explicit casts just to be safe
+ if (gtk_widget_get_direction(widget) == GTK_TEXT_DIR_RTL)
+ xoffset = ((gdouble) xoffset) * (1.0 - xalign);
+ else
+ xoffset *= ((gdouble) xoffset) * xalign;
+ yoffset = yalign * (cell_area->height - (2 * ypad + rect.height));
+ yoffset = MAX(yoffset, 0);
+ }
+
+ aligned_area->x = cell_area->x + xoffset;
+ aligned_area->y = cell_area->y + yoffset;
+ aligned_area->width = 2 * xpad + rect.width;
+ aligned_area->height = 2 * ypad + rect.height;
+
+ g_object_unref(layout);
+}
+
+// this is based on both what GtkCellRendererText does and what GtkCellRendererToggle does
+static void cellRendererButton_render(GtkCellRenderer *r, cairo_t *cr, GtkWidget *widget, const GdkRectangle *background_area, const GdkRectangle *cell_area, GtkCellRendererState flags)
+{
+ cellRendererButton *c = cellRendererButton(r);
+ gint xpad, ypad;
+ GdkRectangle alignedArea;
+ gint xoffset, yoffset;
+ GtkStyleContext *context;
+ PangoLayout *layout;
+ PangoRectangle rect;
+
+ gtk_cell_renderer_get_padding(GTK_CELL_RENDERER(c), &xpad, &ypad);
+ gtk_cell_renderer_get_aligned_area(GTK_CELL_RENDERER(c), widget, flags, cell_area, &alignedArea);
+ xoffset = alignedArea.x - cell_area->x;
+ yoffset = alignedArea.y - cell_area->y;
+
+ context = setButtonStyle(widget);
+ layout = gtk_widget_create_pango_layout(widget, c->text);
+
+ gtk_render_background(context, cr,
+ background_area->x + xoffset + xpad,
+ background_area->y + yoffset + ypad,
+ background_area->width - 2 * xpad,
+ background_area->height - 2 * ypad);
+ gtk_render_frame(context, cr,
+ background_area->x + xoffset + xpad,
+ background_area->y + yoffset + ypad,
+ background_area->width - 2 * xpad,
+ background_area->height - 2 * ypad);
+
+ pango_layout_set_width(layout, -1);
+ pango_layout_get_pixel_extents(layout, NULL, &rect);
+ xoffset -= rect.x;
+ gtk_render_layout(context, cr,
+ cell_area->x + xoffset + xpad,
+ cell_area->y + yoffset + ypad,
+ layout);
+
+ g_object_unref(layout);
+ unsetButtonStyle(context);
+}
+
+static guint clickedSignal;
+
+static gboolean cellRendererButton_activate(GtkCellRenderer *r, GdkEvent *e, GtkWidget *widget, const gchar *path, const GdkRectangle *background_area, const GdkRectangle *cell_area, GtkCellRendererState flags)
+{
+ g_signal_emit(r, clickedSignal, 0, path);
+ return TRUE;
+}
+
+static GParamSpec *props[2] = { NULL, NULL };
+
+static void cellRendererButton_set_property(GObject *object, guint prop, const GValue *value, GParamSpec *pspec)
+{
+ cellRendererButton *c = cellRendererButton(object);
+
+ if (prop != 1) {
+ G_OBJECT_WARN_INVALID_PROPERTY_ID(c, prop, pspec);
+ return;
+ }
+ if (c->text != NULL)
+ g_free(c->text);
+ c->text = g_value_dup_string(value);
+ // GtkCellRendererText doesn't queue a redraw; we won't either
+}
+
+static void cellRendererButton_get_property(GObject *object, guint prop, GValue *value, GParamSpec *pspec)
+{
+ cellRendererButton *c = cellRendererButton(object);
+
+ if (prop != 1) {
+ G_OBJECT_WARN_INVALID_PROPERTY_ID(c, prop, pspec);
+ return;
+ }
+ g_value_set_string(value, c->text);
+}
+
+static void cellRendererButton_class_init(cellRendererButtonClass *class)
+{
+ G_OBJECT_CLASS(class)->dispose = cellRendererButton_dispose;
+ G_OBJECT_CLASS(class)->finalize = cellRendererButton_finalize;
+ G_OBJECT_CLASS(class)->set_property = cellRendererButton_set_property;
+ G_OBJECT_CLASS(class)->get_property = cellRendererButton_get_property;
+ GTK_CELL_RENDERER_CLASS(class)->get_request_mode = cellRendererButton_get_request_mode;
+ GTK_CELL_RENDERER_CLASS(class)->get_preferred_width = cellRendererButton_get_preferred_width;
+ GTK_CELL_RENDERER_CLASS(class)->get_preferred_height_for_width = cellRendererButton_get_preferred_height_for_width;
+ GTK_CELL_RENDERER_CLASS(class)->get_preferred_height = cellRendererButton_get_preferred_height;
+ // don't provide a get_preferred_width_for_height()
+ GTK_CELL_RENDERER_CLASS(class)->get_aligned_area = cellRendererButton_get_aligned_area;
+ // don't provide a get_size()
+ GTK_CELL_RENDERER_CLASS(class)->render = cellRendererButton_render;
+ GTK_CELL_RENDERER_CLASS(class)->activate = cellRendererButton_activate;
+ // don't provide a start_editing()
+
+ props[1] = g_param_spec_string("text",
+ "Text",
+ "Button text",
+ "",
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS);
+ g_object_class_install_properties(G_OBJECT_CLASS(class), 2, props);
+
+ clickedSignal = g_signal_new("clicked",
+ G_TYPE_FROM_CLASS(class),
+ G_SIGNAL_RUN_LAST,
+ 0,
+ NULL, NULL, NULL,
+ G_TYPE_NONE,
+ 1, G_TYPE_STRING);
+}
+
+GtkCellRenderer *newCellRendererButton(void)
+{
+ return GTK_CELL_RENDERER(g_object_new(cellRendererButtonType, NULL));
+}
diff --git a/src/libui_sdl/libui/unix/checkbox.c b/src/libui_sdl/libui/unix/checkbox.c
new file mode 100644
index 0000000..47f8514
--- /dev/null
+++ b/src/libui_sdl/libui/unix/checkbox.c
@@ -0,0 +1,78 @@
+// 10 june 2015
+#include "uipriv_unix.h"
+
+struct uiCheckbox {
+ uiUnixControl c;
+ GtkWidget *widget;
+ GtkButton *button;
+ GtkToggleButton *toggleButton;
+ GtkCheckButton *checkButton;
+ void (*onToggled)(uiCheckbox *, void *);
+ void *onToggledData;
+ gulong onToggledSignal;
+};
+
+uiUnixControlAllDefaults(uiCheckbox)
+
+static void onToggled(GtkToggleButton *b, gpointer data)
+{
+ uiCheckbox *c = uiCheckbox(data);
+
+ (*(c->onToggled))(c, c->onToggledData);
+}
+
+static void defaultOnToggled(uiCheckbox *c, void *data)
+{
+ // do nothing
+}
+
+char *uiCheckboxText(uiCheckbox *c)
+{
+ return uiUnixStrdupText(gtk_button_get_label(c->button));
+}
+
+void uiCheckboxSetText(uiCheckbox *c, const char *text)
+{
+ gtk_button_set_label(GTK_BUTTON(c->button), text);
+}
+
+void uiCheckboxOnToggled(uiCheckbox *c, void (*f)(uiCheckbox *, void *), void *data)
+{
+ c->onToggled = f;
+ c->onToggledData = data;
+}
+
+int uiCheckboxChecked(uiCheckbox *c)
+{
+ return gtk_toggle_button_get_active(c->toggleButton) != FALSE;
+}
+
+void uiCheckboxSetChecked(uiCheckbox *c, int checked)
+{
+ gboolean active;
+
+ active = FALSE;
+ if (checked)
+ active = TRUE;
+ // we need to inhibit sending of ::toggled because this WILL send a ::toggled otherwise
+ g_signal_handler_block(c->toggleButton, c->onToggledSignal);
+ gtk_toggle_button_set_active(c->toggleButton, active);
+ g_signal_handler_unblock(c->toggleButton, c->onToggledSignal);
+}
+
+uiCheckbox *uiNewCheckbox(const char *text)
+{
+ uiCheckbox *c;
+
+ uiUnixNewControl(uiCheckbox, c);
+
+ c->widget = gtk_check_button_new_with_label(text);
+ c->button = GTK_BUTTON(c->widget);
+ c->toggleButton = GTK_TOGGLE_BUTTON(c->widget);
+ c->checkButton = GTK_CHECK_BUTTON(c->widget);
+
+ c->onToggledSignal = g_signal_connect(c->widget, "toggled", G_CALLBACK(onToggled), c);
+ uiCheckboxOnToggled(c, defaultOnToggled, NULL);
+
+ return c;
+}
diff --git a/src/libui_sdl/libui/unix/child.c b/src/libui_sdl/libui/unix/child.c
new file mode 100644
index 0000000..b4a0967
--- /dev/null
+++ b/src/libui_sdl/libui/unix/child.c
@@ -0,0 +1,120 @@
+// 28 august 2015
+#include "uipriv_unix.h"
+
+// This file contains helpers for managing child controls.
+
+struct child {
+ uiControl *c;
+ GtkWidget *widget;
+
+ gboolean oldhexpand;
+ GtkAlign oldhalign;
+ gboolean oldvexpand;
+ GtkAlign oldvalign;
+
+ // Some children can be boxed; that is, they can have an optionally-margined box around them.
+ // uiGroup, uiTab, and uiWindow all do this.
+ GtkWidget *box;
+
+ // If the child is not boxed, this is its parent.
+ // If the child is boxed, this is the box.
+ GtkContainer *parent;
+
+ // This flag is for users of these functions.
+ // For uiBox, this is "spaced".
+ // For uiTab, this is "margined". (uiGroup and uiWindow have to maintain their margined state themselves, since the margined state is independent of whether there is a child for those two.)
+ int flag;
+};
+
+struct child *newChild(uiControl *child, uiControl *parent, GtkContainer *parentContainer)
+{
+ struct child *c;
+
+ if (child == NULL)
+ return NULL;
+
+ c = uiNew(struct child);
+ c->c = child;
+ c->widget = GTK_WIDGET(uiControlHandle(c->c));
+
+ c->oldhexpand = gtk_widget_get_hexpand(c->widget);
+ c->oldhalign = gtk_widget_get_halign(c->widget);
+ c->oldvexpand = gtk_widget_get_vexpand(c->widget);
+ c->oldvalign = gtk_widget_get_valign(c->widget);
+
+ uiControlSetParent(c->c, parent);
+ uiUnixControlSetContainer(uiUnixControl(c->c), parentContainer, FALSE);
+ c->parent = parentContainer;
+
+ return c;
+}
+
+struct child *newChildWithBox(uiControl *child, uiControl *parent, GtkContainer *parentContainer, int margined)
+{
+ struct child *c;
+ GtkWidget *box;
+
+ if (child == NULL)
+ return NULL;
+ box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
+ gtk_widget_show(box);
+ c = newChild(child, parent, GTK_CONTAINER(box));
+ gtk_widget_set_hexpand(c->widget, TRUE);
+ gtk_widget_set_halign(c->widget, GTK_ALIGN_FILL);
+ gtk_widget_set_vexpand(c->widget, TRUE);
+ gtk_widget_set_valign(c->widget, GTK_ALIGN_FILL);
+ c->box = box;
+ gtk_container_add(parentContainer, c->box);
+ childSetMargined(c, margined);
+ return c;
+}
+
+void childRemove(struct child *c)
+{
+ uiControlSetParent(c->c, NULL);
+ uiUnixControlSetContainer(uiUnixControl(c->c), c->parent, TRUE);
+
+ gtk_widget_set_hexpand(c->widget, c->oldhexpand);
+ gtk_widget_set_halign(c->widget, c->oldhalign);
+ gtk_widget_set_vexpand(c->widget, c->oldvexpand);
+ gtk_widget_set_valign(c->widget, c->oldvalign);
+
+ if (c->box != NULL)
+ gtk_widget_destroy(c->box);
+
+ uiFree(c);
+}
+
+void childDestroy(struct child *c)
+{
+ uiControl *child;
+
+ child = c->c;
+ childRemove(c);
+ uiControlDestroy(child);
+}
+
+GtkWidget *childWidget(struct child *c)
+{
+ return c->widget;
+}
+
+int childFlag(struct child *c)
+{
+ return c->flag;
+}
+
+void childSetFlag(struct child *c, int flag)
+{
+ c->flag = flag;
+}
+
+GtkWidget *childBox(struct child *c)
+{
+ return c->box;
+}
+
+void childSetMargined(struct child *c, int margined)
+{
+ setMargined(GTK_CONTAINER(c->box), margined);
+}
diff --git a/src/libui_sdl/libui/unix/colorbutton.c b/src/libui_sdl/libui/unix/colorbutton.c
new file mode 100644
index 0000000..393b16f
--- /dev/null
+++ b/src/libui_sdl/libui/unix/colorbutton.c
@@ -0,0 +1,80 @@
+// 15 may 2016
+#include "uipriv_unix.h"
+
+struct uiColorButton {
+ uiUnixControl c;
+ GtkWidget *widget;
+ GtkButton *button;
+ GtkColorButton *cb;
+ GtkColorChooser *cc;
+ void (*onChanged)(uiColorButton *, void *);
+ void *onChangedData;
+};
+
+uiUnixControlAllDefaults(uiColorButton)
+
+static void onColorSet(GtkColorButton *button, gpointer data)
+{
+ uiColorButton *b = uiColorButton(data);
+
+ (*(b->onChanged))(b, b->onChangedData);
+}
+
+static void defaultOnChanged(uiColorButton *b, void *data)
+{
+ // do nothing
+}
+
+void uiColorButtonColor(uiColorButton *b, double *r, double *g, double *bl, double *a)
+{
+ GdkRGBA rgba;
+
+ gtk_color_chooser_get_rgba(b->cc, &rgba);
+ *r = rgba.red;
+ *g = rgba.green;
+ *bl = rgba.blue;
+ *a = rgba.alpha;
+}
+
+void uiColorButtonSetColor(uiColorButton *b, double r, double g, double bl, double a)
+{
+ GdkRGBA rgba;
+
+ rgba.red = r;
+ rgba.green = g;
+ rgba.blue = bl;
+ rgba.alpha = a;
+ // no need to inhibit the signal; color-set is documented as only being sent when the user changes the color
+ gtk_color_chooser_set_rgba(b->cc, &rgba);
+}
+
+void uiColorButtonOnChanged(uiColorButton *b, void (*f)(uiColorButton *, void *), void *data)
+{
+ b->onChanged = f;
+ b->onChangedData = data;
+}
+
+uiColorButton *uiNewColorButton(void)
+{
+ uiColorButton *b;
+ GdkRGBA black;
+
+ uiUnixNewControl(uiColorButton, b);
+
+ // I'm not sure what the initial color is; set up a real one
+ black.red = 0.0;
+ black.green = 0.0;
+ black.blue = 0.0;
+ black.alpha = 1.0;
+ b->widget = gtk_color_button_new_with_rgba(&black);
+ b->button = GTK_BUTTON(b->widget);
+ b->cb = GTK_COLOR_BUTTON(b->widget);
+ b->cc = GTK_COLOR_CHOOSER(b->widget);
+
+ gtk_color_chooser_set_use_alpha(b->cc, TRUE);
+
+ g_signal_connect(b->widget, "color-set", G_CALLBACK(onColorSet), b);
+ uiColorButtonOnChanged(b, defaultOnChanged, NULL);
+
+ return b;
+}
diff --git a/src/libui_sdl/libui/unix/combobox.c b/src/libui_sdl/libui/unix/combobox.c
new file mode 100644
index 0000000..6fed804
--- /dev/null
+++ b/src/libui_sdl/libui/unix/combobox.c
@@ -0,0 +1,66 @@
+// 11 june 2015
+#include "uipriv_unix.h"
+
+struct uiCombobox {
+ uiUnixControl c;
+ GtkWidget *widget;
+ GtkComboBox *combobox;
+ GtkComboBoxText *comboboxText;
+ void (*onSelected)(uiCombobox *, void *);
+ void *onSelectedData;
+ gulong onSelectedSignal;
+};
+
+uiUnixControlAllDefaults(uiCombobox)
+
+static void onChanged(GtkComboBox *cbox, gpointer data)
+{
+ uiCombobox *c = uiCombobox(data);
+
+ (*(c->onSelected))(c, c->onSelectedData);
+}
+
+static void defaultOnSelected(uiCombobox *c, void *data)
+{
+ // do nothing
+}
+
+void uiComboboxAppend(uiCombobox *c, const char *text)
+{
+ gtk_combo_box_text_append(c->comboboxText, NULL, text);
+}
+
+int uiComboboxSelected(uiCombobox *c)
+{
+ return gtk_combo_box_get_active(c->combobox);
+}
+
+void uiComboboxSetSelected(uiCombobox *c, int n)
+{
+ // we need to inhibit sending of ::changed because this WILL send a ::changed otherwise
+ g_signal_handler_block(c->combobox, c->onSelectedSignal);
+ gtk_combo_box_set_active(c->combobox, n);
+ g_signal_handler_unblock(c->combobox, c->onSelectedSignal);
+}
+
+void uiComboboxOnSelected(uiCombobox *c, void (*f)(uiCombobox *c, void *data), void *data)
+{
+ c->onSelected = f;
+ c->onSelectedData = data;
+}
+
+uiCombobox *uiNewCombobox(void)
+{
+ uiCombobox *c;
+
+ uiUnixNewControl(uiCombobox, c);
+
+ c->widget = gtk_combo_box_text_new();
+ c->combobox = GTK_COMBO_BOX(c->widget);
+ c->comboboxText = GTK_COMBO_BOX_TEXT(c->widget);
+
+ c->onSelectedSignal = g_signal_connect(c->widget, "changed", G_CALLBACK(onChanged), c);
+ uiComboboxOnSelected(c, defaultOnSelected, NULL);
+
+ return c;
+}
diff --git a/src/libui_sdl/libui/unix/control.c b/src/libui_sdl/libui/unix/control.c
new file mode 100644
index 0000000..f6fdcea
--- /dev/null
+++ b/src/libui_sdl/libui/unix/control.c
@@ -0,0 +1,14 @@
+// 16 august 2015
+#include "uipriv_unix.h"
+
+void uiUnixControlSetContainer(uiUnixControl *c, GtkContainer *container, gboolean remove)
+{
+ (*(c->SetContainer))(c, container, remove);
+}
+
+#define uiUnixControlSignature 0x556E6978
+
+uiUnixControl *uiUnixAllocControl(size_t n, uint32_t typesig, const char *typenamestr)
+{
+ return uiUnixControl(uiAllocControl(n, uiUnixControlSignature, typesig, typenamestr));
+}
diff --git a/src/libui_sdl/libui/unix/datetimepicker.c b/src/libui_sdl/libui/unix/datetimepicker.c
new file mode 100644
index 0000000..19689a2
--- /dev/null
+++ b/src/libui_sdl/libui/unix/datetimepicker.c
@@ -0,0 +1,599 @@
+// 4 september 2015
+#include "uipriv_unix.h"
+
+// LONGTERM imitate gnome-calendar's day/month/year entries above the calendar
+// LONGTERM allow entering a 24-hour hour in the hour spinbutton and adjust accordingly
+
+#define dateTimePickerWidgetType (dateTimePickerWidget_get_type())
+#define dateTimePickerWidget(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), dateTimePickerWidgetType, dateTimePickerWidget))
+#define isDateTimePickerWidget(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), dateTimePickerWidgetType))
+#define dateTimePickerWidgetClass(class) (G_TYPE_CHECK_CLASS_CAST((class), dateTimePickerWidgetType, dateTimePickerWidgetClass))
+#define isDateTimePickerWidgetClass(class) (G_TYPE_CHECK_CLASS_TYPE((class), dateTimePickerWidget))
+#define getDateTimePickerWidgetClass(obj) (G_TYPE_INSTANCE_GET_CLASS((obj), dateTimePickerWidgetType, dateTimePickerWidgetClass))
+
+typedef struct dateTimePickerWidget dateTimePickerWidget;
+typedef struct dateTimePickerWidgetClass dateTimePickerWidgetClass;
+
+struct dateTimePickerWidget {
+ GtkToggleButton parent_instance;
+
+ gulong toggledSignal;
+
+ gboolean hasTime;
+ gboolean hasDate;
+
+ GtkWidget *window;
+ GtkWidget *box;
+ GtkWidget *calendar;
+ GtkWidget *timebox;
+ GtkWidget *hours;
+ GtkWidget *minutes;
+ GtkWidget *seconds;
+ GtkWidget *ampm;
+
+ gulong hoursBlock;
+ gulong minutesBlock;
+ gulong secondsBlock;
+ gulong ampmBlock;
+
+ GdkDevice *keyboard;
+ GdkDevice *mouse;
+};
+
+struct dateTimePickerWidgetClass {
+ GtkToggleButtonClass parent_class;
+};
+
+G_DEFINE_TYPE(dateTimePickerWidget, dateTimePickerWidget, GTK_TYPE_TOGGLE_BUTTON)
+
+static int realSpinValue(GtkSpinButton *spinButton)
+{
+ GtkAdjustment *adj;
+
+ adj = gtk_spin_button_get_adjustment(spinButton);
+ return (int) gtk_adjustment_get_value(adj);
+}
+
+static void setRealSpinValue(GtkSpinButton *spinButton, int value, gulong block)
+{
+ GtkAdjustment *adj;
+
+ g_signal_handler_block(spinButton, block);
+ adj = gtk_spin_button_get_adjustment(spinButton);
+ gtk_adjustment_set_value(adj, value);
+ g_signal_handler_unblock(spinButton, block);
+}
+
+static GDateTime *selected(dateTimePickerWidget *d)
+{
+ // choose a day for which all times are likely to be valid for the default date in case we're only dealing with time
+ guint year = 1970, month = 1, day = 1;
+ guint hour = 0, minute = 0, second = 0;
+
+ if (d->hasDate) {
+ gtk_calendar_get_date(GTK_CALENDAR(d->calendar), &year, &month, &day);
+ month++; // GtkCalendar/GDateTime differences
+ }
+ if (d->hasTime) {
+ hour = realSpinValue(GTK_SPIN_BUTTON(d->hours));
+ if (realSpinValue(GTK_SPIN_BUTTON(d->ampm)) != 0)
+ hour += 12;
+ minute = realSpinValue(GTK_SPIN_BUTTON(d->minutes));
+ second = realSpinValue(GTK_SPIN_BUTTON(d->seconds));
+ }
+ return g_date_time_new_local(year, month, day, hour, minute, second);
+}
+
+static void setLabel(dateTimePickerWidget *d)
+{
+ GDateTime *dt;
+ char *fmt;
+ char *msg;
+ gboolean free;
+
+ dt = selected(d);
+ free = FALSE;
+ if (d->hasDate && d->hasTime) {
+ // don't use D_T_FMT; that's too verbose
+ fmt = g_strdup_printf("%s %s", nl_langinfo(D_FMT), nl_langinfo(T_FMT));
+ free = TRUE;
+ } else if (d->hasDate)
+ fmt = nl_langinfo(D_FMT);
+ else
+ fmt = nl_langinfo(T_FMT);
+ msg = g_date_time_format(dt, fmt);
+ gtk_button_set_label(GTK_BUTTON(d), msg);
+ g_free(msg);
+ if (free)
+ g_free(fmt);
+ g_date_time_unref(dt);
+}
+
+static void dateTimeChanged(dateTimePickerWidget *d)
+{
+ setLabel(d);
+ // TODO fire event here
+}
+
+// we don't want ::toggled to be sent again
+static void setActive(dateTimePickerWidget *d, gboolean active)
+{
+ g_signal_handler_block(d, d->toggledSignal);
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(d), active);
+ g_signal_handler_unblock(d, d->toggledSignal);
+}
+
+// like startGrab() below, a lot of this is in the order that GtkComboBox does it
+static void endGrab(dateTimePickerWidget *d)
+{
+ if (d->keyboard != NULL)
+ gdk_device_ungrab(d->keyboard, GDK_CURRENT_TIME);
+ gdk_device_ungrab(d->mouse, GDK_CURRENT_TIME);
+ gtk_device_grab_remove(d->window, d->mouse);
+ d->keyboard = NULL;
+ d->mouse = NULL;
+}
+
+static void hidePopup(dateTimePickerWidget *d)
+{
+ endGrab(d);
+ gtk_widget_hide(d->window);
+ setActive(d, FALSE);
+}
+
+// this consolidates a good chunk of what GtkComboBox does
+static gboolean startGrab(dateTimePickerWidget *d)
+{
+ GdkDevice *dev;
+ guint32 time;
+ GdkWindow *window;
+ GdkDevice *keyboard, *mouse;
+
+ dev = gtk_get_current_event_device();
+ if (dev == NULL) {
+ // this is what GtkComboBox does
+ // since no device was set, just use the first available "master device"
+ GdkDisplay *disp;
+ GdkDeviceManager *dm;
+ GList *list;
+
+ disp = gtk_widget_get_display(GTK_WIDGET(d));
+ dm = gdk_display_get_device_manager(disp);
+ list = gdk_device_manager_list_devices(dm, GDK_DEVICE_TYPE_MASTER);
+ dev = (GdkDevice *) (list->data);
+ g_list_free(list);
+ }
+
+ time = gtk_get_current_event_time();
+ keyboard = dev;
+ mouse = gdk_device_get_associated_device(dev);
+ if (gdk_device_get_source(dev) != GDK_SOURCE_KEYBOARD) {
+ dev = mouse;
+ mouse = keyboard;
+ keyboard = dev;
+ }
+
+ window = gtk_widget_get_window(d->window);
+ if (keyboard != NULL)
+ if (gdk_device_grab(keyboard, window,
+ GDK_OWNERSHIP_WINDOW, TRUE,
+ GDK_KEY_PRESS_MASK | GDK_KEY_RELEASE_MASK,
+ NULL, time) != GDK_GRAB_SUCCESS)
+ return FALSE;
+ if (mouse != NULL)
+ if (gdk_device_grab(mouse, window,
+ GDK_OWNERSHIP_WINDOW, TRUE,
+ GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | GDK_POINTER_MOTION_MASK,
+ NULL, time) != GDK_GRAB_SUCCESS) {
+ if (keyboard != NULL)
+ gdk_device_ungrab(keyboard, time);
+ return FALSE;
+ }
+
+ gtk_device_grab_add(d->window, mouse, TRUE);
+ d->keyboard = keyboard;
+ d->mouse = mouse;
+ return TRUE;
+}
+
+// based on gtk_combo_box_list_position() in the GTK+ source code
+static void allocationToScreen(dateTimePickerWidget *d, gint *x, gint *y)
+{
+ GdkWindow *window;
+ GtkAllocation a;
+ GtkRequisition aWin;
+ GdkScreen *screen;
+ GdkRectangle workarea;
+ int otherY;
+
+ gtk_widget_get_allocation(GTK_WIDGET(d), &a);
+ gtk_widget_get_preferred_size(d->window, &aWin, NULL);
+ *x = 0;
+ *y = 0;
+ if (!gtk_widget_get_has_window(GTK_WIDGET(d))) {
+ *x = a.x;
+ *y = a.y;
+ }
+ window = gtk_widget_get_window(GTK_WIDGET(d));
+ gdk_window_get_root_coords(window, *x, *y, x, y);
+ if (gtk_widget_get_direction(GTK_WIDGET(d)) == GTK_TEXT_DIR_RTL)
+ *x += a.width - aWin.width;
+
+ // now adjust to prevent the box from going offscreen
+ screen = gtk_widget_get_screen(GTK_WIDGET(d));
+ gdk_screen_get_monitor_workarea(screen,
+ gdk_screen_get_monitor_at_window(screen, window),
+ &workarea);
+ if (*x < workarea.x) // too far to the left?
+ *x = workarea.x;
+ else if (*x + aWin.width > (workarea.x + workarea.width)) // too far to the right?
+ *x = (workarea.x + workarea.width) - aWin.width;
+ // this isn't the same algorithm used by GtkComboBox
+ // first, get our two choices; *y for down and otherY for up
+ otherY = *y - aWin.height;
+ *y += a.height;
+ // and use otherY if we're too low
+ if (*y + aWin.height >= workarea.y + workarea.height)
+ *y = otherY;
+}
+
+static void showPopup(dateTimePickerWidget *d)
+{
+ GtkWidget *toplevel;
+ gint x, y;
+
+ // GtkComboBox does it
+ toplevel = gtk_widget_get_toplevel(GTK_WIDGET(d));
+ if (GTK_IS_WINDOW(toplevel))
+ gtk_window_group_add_window(gtk_window_get_group(GTK_WINDOW(toplevel)), GTK_WINDOW(d->window));
+
+ allocationToScreen(d, &x, &y);
+ gtk_window_move(GTK_WINDOW(d->window), x, y);
+
+ gtk_widget_show(d->window);
+ setActive(d, TRUE);
+
+ if (!startGrab(d))
+ hidePopup(d);
+}
+
+static void onToggled(GtkToggleButton *b, gpointer data)
+{
+ dateTimePickerWidget *d = dateTimePickerWidget(b);
+
+ if (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(d)))
+ showPopup(d);
+ else
+ hidePopup(d);
+}
+
+static gboolean grabBroken(GtkWidget *w, GdkEventGrabBroken *e, gpointer data)
+{
+ dateTimePickerWidget *d = dateTimePickerWidget(data);
+
+ hidePopup(d);
+ return TRUE; // this is what GtkComboBox does
+}
+
+static gboolean buttonReleased(GtkWidget *w, GdkEventButton *e, gpointer data)
+{
+ dateTimePickerWidget *d = dateTimePickerWidget(data);
+ int winx, winy;
+ GtkAllocation wina;
+ gboolean in;
+
+ gtk_widget_get_allocation(d->window, &wina);
+ winx = 0;
+ winy = 0;
+ if (!gtk_widget_get_has_window(d->window)) {
+ winx = wina.x;
+ winy = wina.y;
+ }
+ gdk_window_get_root_coords(gtk_widget_get_window(d->window), winx, winy, &winx, &winy);
+ in = TRUE;
+ if (e->x_root < winx)
+ in = FALSE;
+ if (e->x_root >= (winx + wina.width))
+ in = FALSE;
+ if (e->y_root < winy)
+ in = FALSE;
+ if (e->y_root >= (winy + wina.height))
+ in = FALSE;
+ if (!in)
+ hidePopup(d);
+ return TRUE; // this is what GtkComboBox does
+}
+
+static gint hoursSpinboxInput(GtkSpinButton *sb, gpointer ptr, gpointer data)
+{
+ double *out = (double *) ptr;
+ const gchar *text;
+ int value;
+
+ text = gtk_entry_get_text(GTK_ENTRY(sb));
+ value = (int) g_strtod(text, NULL);
+ if (value < 0 || value > 12)
+ return GTK_INPUT_ERROR;
+ if (value == 12) // 12 to the user is 0 internally
+ value = 0;
+ *out = (double) value;
+ return TRUE;
+}
+
+static gboolean hoursSpinboxOutput(GtkSpinButton *sb, gpointer data)
+{
+ gchar *text;
+ int value;
+
+ value = realSpinValue(sb);
+ if (value == 0) // 0 internally is 12 to the user
+ value = 12;
+ text = g_strdup_printf("%d", value);
+ gtk_entry_set_text(GTK_ENTRY(sb), text);
+ g_free(text);
+ return TRUE;
+}
+
+static gboolean zeroPadSpinbox(GtkSpinButton *sb, gpointer data)
+{
+ gchar *text;
+ int value;
+
+ value = realSpinValue(sb);
+ text = g_strdup_printf("%02d", value);
+ gtk_entry_set_text(GTK_ENTRY(sb), text);
+ g_free(text);
+ return TRUE;
+}
+
+// this is really hacky but we can't use GtkCombobox here :(
+static gint ampmSpinboxInput(GtkSpinButton *sb, gpointer ptr, gpointer data)
+{
+ double *out = (double *) ptr;
+ const gchar *text;
+ char firstAM, firstPM;
+
+ text = gtk_entry_get_text(GTK_ENTRY(sb));
+ // LONGTERM don't use ASCII here for case insensitivity
+ firstAM = g_ascii_tolower(nl_langinfo(AM_STR)[0]);
+ firstPM = g_ascii_tolower(nl_langinfo(PM_STR)[0]);
+ for (; *text != '\0'; text++)
+ if (g_ascii_tolower(*text) == firstAM) {
+ *out = 0;
+ return TRUE;
+ } else if (g_ascii_tolower(*text) == firstPM) {
+ *out = 1;
+ return TRUE;
+ }
+ return GTK_INPUT_ERROR;
+}
+
+static gboolean ampmSpinboxOutput(GtkSpinButton *sb, gpointer data)
+{
+ int value;
+
+ value = gtk_spin_button_get_value_as_int(sb);
+ if (value == 0)
+ gtk_entry_set_text(GTK_ENTRY(sb), nl_langinfo(AM_STR));
+ else
+ gtk_entry_set_text(GTK_ENTRY(sb), nl_langinfo(PM_STR));
+ return TRUE;
+}
+
+static void spinboxChanged(GtkSpinButton *sb, gpointer data)
+{
+ dateTimePickerWidget *d = dateTimePickerWidget(data);
+
+ dateTimeChanged(d);
+}
+
+static GtkWidget *newSpinbox(dateTimePickerWidget *d, int min, int max, gint (*input)(GtkSpinButton *, gpointer, gpointer), gboolean (*output)(GtkSpinButton *, gpointer), gulong *block)
+{
+ GtkWidget *sb;
+
+ sb = gtk_spin_button_new_with_range(min, max, 1);
+ gtk_spin_button_set_digits(GTK_SPIN_BUTTON(sb), 0);
+ gtk_spin_button_set_wrap(GTK_SPIN_BUTTON(sb), TRUE);
+ gtk_orientable_set_orientation(GTK_ORIENTABLE(sb), GTK_ORIENTATION_VERTICAL);
+ *block = g_signal_connect(sb, "value-changed", G_CALLBACK(spinboxChanged), d);
+ if (input != NULL)
+ g_signal_connect(sb, "input", G_CALLBACK(input), NULL);
+ if (output != NULL)
+ g_signal_connect(sb, "output", G_CALLBACK(output), NULL);
+ return sb;
+}
+
+static void dateChanged(GtkCalendar *c, gpointer data)
+{
+ dateTimePickerWidget *d = dateTimePickerWidget(data);
+
+ dateTimeChanged(d);
+}
+
+static void setDateOnly(dateTimePickerWidget *d)
+{
+ d->hasTime = FALSE;
+ gtk_container_remove(GTK_CONTAINER(d->box), d->timebox);
+}
+
+static void setTimeOnly(dateTimePickerWidget *d)
+{
+ d->hasDate = FALSE;
+ gtk_container_remove(GTK_CONTAINER(d->box), d->calendar);
+}
+
+static void dateTimePickerWidget_init(dateTimePickerWidget *d)
+{
+ GDateTime *dt;
+ gint year, month, day;
+ gint hour;
+ gulong calendarBlock;
+
+ d->window = gtk_window_new(GTK_WINDOW_POPUP);
+ gtk_window_set_resizable(GTK_WINDOW(d->window), FALSE);
+ gtk_window_set_attached_to(GTK_WINDOW(d->window), GTK_WIDGET(d));
+ gtk_window_set_decorated(GTK_WINDOW(d->window), FALSE);
+ gtk_window_set_deletable(GTK_WINDOW(d->window), FALSE);
+ gtk_window_set_type_hint(GTK_WINDOW(d->window), GDK_WINDOW_TYPE_HINT_COMBO);
+ gtk_window_set_skip_taskbar_hint(GTK_WINDOW(d->window), TRUE);
+ gtk_window_set_skip_pager_hint(GTK_WINDOW(d->window), TRUE);
+ gtk_window_set_has_resize_grip(GTK_WINDOW(d->window), FALSE);
+ gtk_container_set_border_width(GTK_CONTAINER(d->window), 12);
+ // and make it stand out a bit
+ gtk_style_context_add_class(gtk_widget_get_style_context(d->window), "frame");
+
+ d->box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 6);
+ gtk_container_add(GTK_CONTAINER(d->window), d->box);
+
+ d->calendar = gtk_calendar_new();
+ calendarBlock = g_signal_connect(d->calendar, "day-selected", G_CALLBACK(dateChanged), d);
+ gtk_container_add(GTK_CONTAINER(d->box), d->calendar);
+
+ d->timebox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
+ gtk_widget_set_valign(d->timebox, GTK_ALIGN_CENTER);
+ gtk_container_add(GTK_CONTAINER(d->box), d->timebox);
+
+ d->hours = newSpinbox(d, 0, 11, hoursSpinboxInput, hoursSpinboxOutput, &(d->hoursBlock));
+ gtk_container_add(GTK_CONTAINER(d->timebox), d->hours);
+
+ gtk_container_add(GTK_CONTAINER(d->timebox),
+ gtk_label_new(":"));
+
+ d->minutes = newSpinbox(d, 0, 59, NULL, zeroPadSpinbox, &(d->minutesBlock));
+ gtk_container_add(GTK_CONTAINER(d->timebox), d->minutes);
+
+ gtk_container_add(GTK_CONTAINER(d->timebox),
+ gtk_label_new(":"));
+
+ d->seconds = newSpinbox(d, 0, 59, NULL, zeroPadSpinbox, &(d->secondsBlock));
+ gtk_container_add(GTK_CONTAINER(d->timebox), d->seconds);
+
+ // LONGTERM this should be the case, but that interferes with grabs
+ // switch to it when we can drop GTK+ 3.10 and use popovers
+#if 0
+ d->ampm = gtk_combo_box_text_new();
+ gtk_combo_box_text_append(GTK_COMBO_BOX_TEXT(d->ampm), NULL, "AM");
+ gtk_combo_box_text_append(GTK_COMBO_BOX_TEXT(d->ampm), NULL, "PM");
+#endif
+ d->ampm = newSpinbox(d, 0, 1, ampmSpinboxInput, ampmSpinboxOutput, &(d->ampmBlock));
+ gtk_spin_button_set_numeric(GTK_SPIN_BUTTON(d->ampm), FALSE);
+ gtk_widget_set_valign(d->ampm, GTK_ALIGN_CENTER);
+ gtk_container_add(GTK_CONTAINER(d->timebox), d->ampm);
+
+ gtk_widget_show_all(d->box);
+
+ g_signal_connect(d->window, "grab-broken-event", G_CALLBACK(grabBroken), d);
+ g_signal_connect(d->window, "button-release-event", G_CALLBACK(buttonReleased), d);
+
+ d->toggledSignal = g_signal_connect(d, "toggled", G_CALLBACK(onToggled), NULL);
+ d->keyboard = NULL;
+ d->mouse = NULL;
+
+ d->hasTime = TRUE;
+ d->hasDate = TRUE;
+
+ // set the current date/time
+ // notice how we block signals from firing
+ dt = g_date_time_new_now_local();
+ g_date_time_get_ymd(dt, &year, &month, &day);
+ month--; // GDateTime/GtkCalendar differences
+ g_signal_handler_block(d->calendar, calendarBlock);
+ gtk_calendar_select_month(GTK_CALENDAR(d->calendar), month, year);
+ gtk_calendar_select_day(GTK_CALENDAR(d->calendar), day);
+ g_signal_handler_unblock(d->calendar, calendarBlock);
+ hour = g_date_time_get_hour(dt);
+ if (hour >= 12) {
+ hour -= 12;
+ setRealSpinValue(GTK_SPIN_BUTTON(d->ampm), 1, d->ampmBlock);
+ }
+ setRealSpinValue(GTK_SPIN_BUTTON(d->hours), hour, d->hoursBlock);
+ setRealSpinValue(GTK_SPIN_BUTTON(d->minutes), g_date_time_get_minute(dt), d->minutesBlock);
+ setRealSpinValue(GTK_SPIN_BUTTON(d->seconds), g_date_time_get_seconds(dt), d->secondsBlock);
+ g_date_time_unref(dt);
+}
+
+static void dateTimePickerWidget_dispose(GObject *obj)
+{
+ dateTimePickerWidget *d = dateTimePickerWidget(obj);
+
+ if (d->window != NULL) {
+ gtk_widget_destroy(d->window);
+ d->window = NULL;
+ }
+ G_OBJECT_CLASS(dateTimePickerWidget_parent_class)->dispose(obj);
+}
+
+static void dateTimePickerWidget_finalize(GObject *obj)
+{
+ G_OBJECT_CLASS(dateTimePickerWidget_parent_class)->finalize(obj);
+}
+
+static void dateTimePickerWidget_class_init(dateTimePickerWidgetClass *class)
+{
+ G_OBJECT_CLASS(class)->dispose = dateTimePickerWidget_dispose;
+ G_OBJECT_CLASS(class)->finalize = dateTimePickerWidget_finalize;
+}
+
+static GtkWidget *newDTP(void)
+{
+ GtkWidget *w;
+
+ w = GTK_WIDGET(g_object_new(dateTimePickerWidgetType, "label", "", NULL));
+ setLabel(dateTimePickerWidget(w));
+ return w;
+}
+
+static GtkWidget *newDP(void)
+{
+ GtkWidget *w;
+
+ w = GTK_WIDGET(g_object_new(dateTimePickerWidgetType, "label", "", NULL));
+ setDateOnly(dateTimePickerWidget(w));
+ setLabel(dateTimePickerWidget(w));
+ return w;
+}
+
+static GtkWidget *newTP(void)
+{
+ GtkWidget *w;
+
+ w = GTK_WIDGET(g_object_new(dateTimePickerWidgetType, "label", "", NULL));
+ setTimeOnly(dateTimePickerWidget(w));
+ setLabel(dateTimePickerWidget(w));
+ return w;
+}
+
+struct uiDateTimePicker {
+ uiUnixControl c;
+ GtkWidget *widget;
+ dateTimePickerWidget *d;
+};
+
+uiUnixControlAllDefaults(uiDateTimePicker)
+
+uiDateTimePicker *finishNewDateTimePicker(GtkWidget *(*fn)(void))
+{
+ uiDateTimePicker *d;
+
+ uiUnixNewControl(uiDateTimePicker, d);
+
+ d->widget = (*fn)();
+ d->d = dateTimePickerWidget(d->widget);
+
+ return d;
+}
+
+uiDateTimePicker *uiNewDateTimePicker(void)
+{
+ return finishNewDateTimePicker(newDTP);
+}
+
+uiDateTimePicker *uiNewDatePicker(void)
+{
+ return finishNewDateTimePicker(newDP);
+}
+
+uiDateTimePicker *uiNewTimePicker(void)
+{
+ return finishNewDateTimePicker(newTP);
+}
diff --git a/src/libui_sdl/libui/unix/debug.c b/src/libui_sdl/libui/unix/debug.c
new file mode 100644
index 0000000..c948db6
--- /dev/null
+++ b/src/libui_sdl/libui/unix/debug.c
@@ -0,0 +1,14 @@
+// 13 may 2016
+#include "uipriv_unix.h"
+
+// LONGTERM don't halt on release builds
+
+void realbug(const char *file, const char *line, const char *func, const char *prefix, const char *format, va_list ap)
+{
+ char *a, *b;
+
+ a = g_strdup_printf("[libui] %s:%s:%s() %s", file, line, func, prefix);
+ b = g_strdup_vprintf(format, ap);
+ g_critical("%s%s", a, b);
+ G_BREAKPOINT();
+}
diff --git a/src/libui_sdl/libui/unix/draw.c b/src/libui_sdl/libui/unix/draw.c
new file mode 100644
index 0000000..2d7a636
--- /dev/null
+++ b/src/libui_sdl/libui/unix/draw.c
@@ -0,0 +1,141 @@
+// 6 september 2015
+#include "uipriv_unix.h"
+#include "draw.h"
+
+uiDrawContext *newContext(cairo_t *cr)
+{
+ uiDrawContext *c;
+
+ c = uiNew(uiDrawContext);
+ c->cr = cr;
+ return c;
+}
+
+void freeContext(uiDrawContext *c)
+{
+ uiFree(c);
+}
+
+static cairo_pattern_t *mkbrush(uiDrawBrush *b)
+{
+ cairo_pattern_t *pat;
+ size_t i;
+
+ switch (b->Type) {
+ case uiDrawBrushTypeSolid:
+ pat = cairo_pattern_create_rgba(b->R, b->G, b->B, b->A);
+ break;
+ case uiDrawBrushTypeLinearGradient:
+ pat = cairo_pattern_create_linear(b->X0, b->Y0, b->X1, b->Y1);
+ break;
+ case uiDrawBrushTypeRadialGradient:
+ // make the start circle radius 0 to make it a point
+ pat = cairo_pattern_create_radial(
+ b->X0, b->Y0, 0,
+ b->X1, b->Y1, b->OuterRadius);
+ break;
+// case uiDrawBrushTypeImage:
+ }
+ if (cairo_pattern_status(pat) != CAIRO_STATUS_SUCCESS)
+ implbug("error creating pattern in mkbrush(): %s",
+ cairo_status_to_string(cairo_pattern_status(pat)));
+ switch (b->Type) {
+ case uiDrawBrushTypeLinearGradient:
+ case uiDrawBrushTypeRadialGradient:
+ for (i = 0; i < b->NumStops; i++)
+ cairo_pattern_add_color_stop_rgba(pat,
+ b->Stops[i].Pos,
+ b->Stops[i].R,
+ b->Stops[i].G,
+ b->Stops[i].B,
+ b->Stops[i].A);
+ }
+ return pat;
+}
+
+void uiDrawStroke(uiDrawContext *c, uiDrawPath *path, uiDrawBrush *b, uiDrawStrokeParams *p)
+{
+ cairo_pattern_t *pat;
+
+ runPath(path, c->cr);
+ pat = mkbrush(b);
+ cairo_set_source(c->cr, pat);
+ switch (p->Cap) {
+ case uiDrawLineCapFlat:
+ cairo_set_line_cap(c->cr, CAIRO_LINE_CAP_BUTT);
+ break;
+ case uiDrawLineCapRound:
+ cairo_set_line_cap(c->cr, CAIRO_LINE_CAP_ROUND);
+ break;
+ case uiDrawLineCapSquare:
+ cairo_set_line_cap(c->cr, CAIRO_LINE_CAP_SQUARE);
+ break;
+ }
+ switch (p->Join) {
+ case uiDrawLineJoinMiter:
+ cairo_set_line_join(c->cr, CAIRO_LINE_JOIN_MITER);
+ cairo_set_miter_limit(c->cr, p->MiterLimit);
+ break;
+ case uiDrawLineJoinRound:
+ cairo_set_line_join(c->cr, CAIRO_LINE_JOIN_ROUND);
+ break;
+ case uiDrawLineJoinBevel:
+ cairo_set_line_join(c->cr, CAIRO_LINE_JOIN_BEVEL);
+ break;
+ }
+ cairo_set_line_width(c->cr, p->Thickness);
+ cairo_set_dash(c->cr, p->Dashes, p->NumDashes, p->DashPhase);
+ cairo_stroke(c->cr);
+ cairo_pattern_destroy(pat);
+}
+
+void uiDrawFill(uiDrawContext *c, uiDrawPath *path, uiDrawBrush *b)
+{
+ cairo_pattern_t *pat;
+
+ runPath(path, c->cr);
+ pat = mkbrush(b);
+ cairo_set_source(c->cr, pat);
+ switch (pathFillMode(path)) {
+ case uiDrawFillModeWinding:
+ cairo_set_fill_rule(c->cr, CAIRO_FILL_RULE_WINDING);
+ break;
+ case uiDrawFillModeAlternate:
+ cairo_set_fill_rule(c->cr, CAIRO_FILL_RULE_EVEN_ODD);
+ break;
+ }
+ cairo_fill(c->cr);
+ cairo_pattern_destroy(pat);
+}
+
+void uiDrawTransform(uiDrawContext *c, uiDrawMatrix *m)
+{
+ cairo_matrix_t cm;
+
+ m2c(m, &cm);
+ cairo_transform(c->cr, &cm);
+}
+
+void uiDrawClip(uiDrawContext *c, uiDrawPath *path)
+{
+ runPath(path, c->cr);
+ switch (pathFillMode(path)) {
+ case uiDrawFillModeWinding:
+ cairo_set_fill_rule(c->cr, CAIRO_FILL_RULE_WINDING);
+ break;
+ case uiDrawFillModeAlternate:
+ cairo_set_fill_rule(c->cr, CAIRO_FILL_RULE_EVEN_ODD);
+ break;
+ }
+ cairo_clip(c->cr);
+}
+
+void uiDrawSave(uiDrawContext *c)
+{
+ cairo_save(c->cr);
+}
+
+void uiDrawRestore(uiDrawContext *c)
+{
+ cairo_restore(c->cr);
+}
diff --git a/src/libui_sdl/libui/unix/draw.h b/src/libui_sdl/libui/unix/draw.h
new file mode 100644
index 0000000..dbd82ff
--- /dev/null
+++ b/src/libui_sdl/libui/unix/draw.h
@@ -0,0 +1,13 @@
+// 5 may 2016
+
+// draw.c
+struct uiDrawContext {
+ cairo_t *cr;
+};
+
+// drawpath.c
+extern void runPath(uiDrawPath *p, cairo_t *cr);
+extern uiDrawFillMode pathFillMode(uiDrawPath *path);
+
+// drawmatrix.c
+extern void m2c(uiDrawMatrix *m, cairo_matrix_t *c);
diff --git a/src/libui_sdl/libui/unix/drawmatrix.c b/src/libui_sdl/libui/unix/drawmatrix.c
new file mode 100644
index 0000000..ac7ac57
--- /dev/null
+++ b/src/libui_sdl/libui/unix/drawmatrix.c
@@ -0,0 +1,109 @@
+// 6 september 2015
+#include "uipriv_unix.h"
+#include "draw.h"
+
+void m2c(uiDrawMatrix *m, cairo_matrix_t *c)
+{
+ c->xx = m->M11;
+ c->yx = m->M12;
+ c->xy = m->M21;
+ c->yy = m->M22;
+ c->x0 = m->M31;
+ c->y0 = m->M32;
+}
+
+static void c2m(cairo_matrix_t *c, uiDrawMatrix *m)
+{
+ m->M11 = c->xx;
+ m->M12 = c->yx;
+ m->M21 = c->xy;
+ m->M22 = c->yy;
+ m->M31 = c->x0;
+ m->M32 = c->y0;
+}
+
+void uiDrawMatrixTranslate(uiDrawMatrix *m, double x, double y)
+{
+ cairo_matrix_t c;
+
+ m2c(m, &c);
+ cairo_matrix_translate(&c, x, y);
+ c2m(&c, m);
+}
+
+void uiDrawMatrixScale(uiDrawMatrix *m, double xCenter, double yCenter, double x, double y)
+{
+ cairo_matrix_t c;
+ double xt, yt;
+
+ m2c(m, &c);
+ xt = x;
+ yt = y;
+ scaleCenter(xCenter, yCenter, &xt, &yt);
+ cairo_matrix_translate(&c, xt, yt);
+ cairo_matrix_scale(&c, x, y);
+ cairo_matrix_translate(&c, -xt, -yt);
+ c2m(&c, m);
+}
+
+void uiDrawMatrixRotate(uiDrawMatrix *m, double x, double y, double amount)
+{
+ cairo_matrix_t c;
+
+ m2c(m, &c);
+ cairo_matrix_translate(&c, x, y);
+ cairo_matrix_rotate(&c, amount);
+ cairo_matrix_translate(&c, -x, -y);
+ c2m(&c, m);
+}
+
+void uiDrawMatrixSkew(uiDrawMatrix *m, double x, double y, double xamount, double yamount)
+{
+ fallbackSkew(m, x, y, xamount, yamount);
+}
+
+void uiDrawMatrixMultiply(uiDrawMatrix *dest, uiDrawMatrix *src)
+{
+ cairo_matrix_t c;
+ cairo_matrix_t d;
+
+ m2c(dest, &c);
+ m2c(src, &d);
+ cairo_matrix_multiply(&c, &c, &d);
+ c2m(&c, dest);
+}
+
+int uiDrawMatrixInvertible(uiDrawMatrix *m)
+{
+ cairo_matrix_t c;
+
+ m2c(m, &c);
+ return cairo_matrix_invert(&c) == CAIRO_STATUS_SUCCESS;
+}
+
+int uiDrawMatrixInvert(uiDrawMatrix *m)
+{
+ cairo_matrix_t c;
+
+ m2c(m, &c);
+ if (cairo_matrix_invert(&c) != CAIRO_STATUS_SUCCESS)
+ return 0;
+ c2m(&c, m);
+ return 1;
+}
+
+void uiDrawMatrixTransformPoint(uiDrawMatrix *m, double *x, double *y)
+{
+ cairo_matrix_t c;
+
+ m2c(m, &c);
+ cairo_matrix_transform_point(&c, x, y);
+}
+
+void uiDrawMatrixTransformSize(uiDrawMatrix *m, double *x, double *y)
+{
+ cairo_matrix_t c;
+
+ m2c(m, &c);
+ cairo_matrix_transform_distance(&c, x, y);
+}
diff --git a/src/libui_sdl/libui/unix/drawpath.c b/src/libui_sdl/libui/unix/drawpath.c
new file mode 100644
index 0000000..a0165fb
--- /dev/null
+++ b/src/libui_sdl/libui/unix/drawpath.c
@@ -0,0 +1,199 @@
+// 6 september 2015
+#include "uipriv_unix.h"
+#include "draw.h"
+
+struct uiDrawPath {
+ GArray *pieces;
+ uiDrawFillMode fillMode;
+ gboolean ended;
+};
+
+struct piece {
+ int type;
+ double d[8];
+ int b;
+};
+
+enum {
+ newFigure,
+ newFigureArc,
+ lineTo,
+ arcTo,
+ bezierTo,
+ closeFigure,
+ addRect,
+};
+
+uiDrawPath *uiDrawNewPath(uiDrawFillMode mode)
+{
+ uiDrawPath *p;
+
+ p = uiNew(uiDrawPath);
+ p->pieces = g_array_new(FALSE, TRUE, sizeof (struct piece));
+ p->fillMode = mode;
+ return p;
+}
+
+void uiDrawFreePath(uiDrawPath *p)
+{
+ g_array_free(p->pieces, TRUE);
+ uiFree(p);
+}
+
+static void add(uiDrawPath *p, struct piece *piece)
+{
+ if (p->ended)
+ userbug("You cannot modify a uiDrawPath that has been ended. (path: %p)", p);
+ g_array_append_vals(p->pieces, piece, 1);
+}
+
+void uiDrawPathNewFigure(uiDrawPath *p, double x, double y)
+{
+ struct piece piece;
+
+ piece.type = newFigure;
+ piece.d[0] = x;
+ piece.d[1] = y;
+ add(p, &piece);
+}
+
+void uiDrawPathNewFigureWithArc(uiDrawPath *p, double xCenter, double yCenter, double radius, double startAngle, double sweep, int negative)
+{
+ struct piece piece;
+
+ if (sweep > 2 * uiPi)
+ sweep = 2 * uiPi;
+ piece.type = newFigureArc;
+ piece.d[0] = xCenter;
+ piece.d[1] = yCenter;
+ piece.d[2] = radius;
+ piece.d[3] = startAngle;
+ piece.d[4] = sweep;
+ piece.b = negative;
+ add(p, &piece);
+}
+
+void uiDrawPathLineTo(uiDrawPath *p, double x, double y)
+{
+ struct piece piece;
+
+ piece.type = lineTo;
+ piece.d[0] = x;
+ piece.d[1] = y;
+ add(p, &piece);
+}
+
+void uiDrawPathArcTo(uiDrawPath *p, double xCenter, double yCenter, double radius, double startAngle, double sweep, int negative)
+{
+ struct piece piece;
+
+ if (sweep > 2 * uiPi)
+ sweep = 2 * uiPi;
+ piece.type = arcTo;
+ piece.d[0] = xCenter;
+ piece.d[1] = yCenter;
+ piece.d[2] = radius;
+ piece.d[3] = startAngle;
+ piece.d[4] = sweep;
+ piece.b = negative;
+ add(p, &piece);
+}
+
+void uiDrawPathBezierTo(uiDrawPath *p, double c1x, double c1y, double c2x, double c2y, double endX, double endY)
+{
+ struct piece piece;
+
+ piece.type = bezierTo;
+ piece.d[0] = c1x;
+ piece.d[1] = c1y;
+ piece.d[2] = c2x;
+ piece.d[3] = c2y;
+ piece.d[4] = endX;
+ piece.d[5] = endY;
+ add(p, &piece);
+}
+
+void uiDrawPathCloseFigure(uiDrawPath *p)
+{
+ struct piece piece;
+
+ piece.type = closeFigure;
+ add(p, &piece);
+}
+
+void uiDrawPathAddRectangle(uiDrawPath *p, double x, double y, double width, double height)
+{
+ struct piece piece;
+
+ piece.type = addRect;
+ piece.d[0] = x;
+ piece.d[1] = y;
+ piece.d[2] = width;
+ piece.d[3] = height;
+ add(p, &piece);
+}
+
+void uiDrawPathEnd(uiDrawPath *p)
+{
+ p->ended = TRUE;
+}
+
+void runPath(uiDrawPath *p, cairo_t *cr)
+{
+ guint i;
+ struct piece *piece;
+ void (*arc)(cairo_t *, double, double, double, double, double);
+
+ if (!p->ended)
+ userbug("You cannot draw with a uiDrawPath that has not been ended. (path: %p)", p);
+ cairo_new_path(cr);
+ for (i = 0; i < p->pieces->len; i++) {
+ piece = &g_array_index(p->pieces, struct piece, i);
+ switch (piece->type) {
+ case newFigure:
+ cairo_move_to(cr, piece->d[0], piece->d[1]);
+ break;
+ case newFigureArc:
+ cairo_new_sub_path(cr);
+ // fall through
+ case arcTo:
+ arc = cairo_arc;
+ if (piece->b)
+ arc = cairo_arc_negative;
+ (*arc)(cr,
+ piece->d[0],
+ piece->d[1],
+ piece->d[2],
+ piece->d[3],
+ piece->d[3] + piece->d[4]);
+ break;
+ case lineTo:
+ cairo_line_to(cr, piece->d[0], piece->d[1]);
+ break;
+ case bezierTo:
+ cairo_curve_to(cr,
+ piece->d[0],
+ piece->d[1],
+ piece->d[2],
+ piece->d[3],
+ piece->d[4],
+ piece->d[5]);
+ break;
+ case closeFigure:
+ cairo_close_path(cr);
+ break;
+ case addRect:
+ cairo_rectangle(cr,
+ piece->d[0],
+ piece->d[1],
+ piece->d[2],
+ piece->d[3]);
+ break;
+ }
+ }
+}
+
+uiDrawFillMode pathFillMode(uiDrawPath *path)
+{
+ return path->fillMode;
+}
diff --git a/src/libui_sdl/libui/unix/drawtext.c b/src/libui_sdl/libui/unix/drawtext.c
new file mode 100644
index 0000000..7078e1a
--- /dev/null
+++ b/src/libui_sdl/libui/unix/drawtext.c
@@ -0,0 +1,293 @@
+// 6 september 2015
+#include "uipriv_unix.h"
+#include "draw.h"
+
+struct uiDrawFontFamilies {
+ PangoFontFamily **f;
+ int n;
+};
+
+uiDrawFontFamilies *uiDrawListFontFamilies(void)
+{
+ uiDrawFontFamilies *ff;
+ PangoFontMap *map;
+
+ ff = uiNew(uiDrawFontFamilies);
+ map = pango_cairo_font_map_get_default();
+ pango_font_map_list_families(map, &(ff->f), &(ff->n));
+ // do not free map; it's a shared resource
+ return ff;
+}
+
+int uiDrawFontFamiliesNumFamilies(uiDrawFontFamilies *ff)
+{
+ return ff->n;
+}
+
+char *uiDrawFontFamiliesFamily(uiDrawFontFamilies *ff, int n)
+{
+ PangoFontFamily *f;
+
+ f = ff->f[n];
+ return uiUnixStrdupText(pango_font_family_get_name(f));
+}
+
+void uiDrawFreeFontFamilies(uiDrawFontFamilies *ff)
+{
+ g_free(ff->f);
+ uiFree(ff);
+}
+
+struct uiDrawTextFont {
+ PangoFont *f;
+};
+
+uiDrawTextFont *mkTextFont(PangoFont *f, gboolean ref)
+{
+ uiDrawTextFont *font;
+
+ font = uiNew(uiDrawTextFont);
+ font->f = f;
+ if (ref)
+ g_object_ref(font->f);
+ return font;
+}
+
+static const PangoWeight pangoWeights[] = {
+ [uiDrawTextWeightThin] = PANGO_WEIGHT_THIN,
+ [uiDrawTextWeightUltraLight] = PANGO_WEIGHT_ULTRALIGHT,
+ [uiDrawTextWeightLight] = PANGO_WEIGHT_LIGHT,
+ [uiDrawTextWeightBook] = PANGO_WEIGHT_BOOK,
+ [uiDrawTextWeightNormal] = PANGO_WEIGHT_NORMAL,
+ [uiDrawTextWeightMedium] = PANGO_WEIGHT_MEDIUM,
+ [uiDrawTextWeightSemiBold] = PANGO_WEIGHT_SEMIBOLD,
+ [uiDrawTextWeightBold] = PANGO_WEIGHT_BOLD,
+ [uiDrawTextWeightUltraBold] = PANGO_WEIGHT_ULTRABOLD,
+ [uiDrawTextWeightHeavy] = PANGO_WEIGHT_HEAVY,
+ [uiDrawTextWeightUltraHeavy] = PANGO_WEIGHT_ULTRAHEAVY,
+};
+
+static const PangoStyle pangoItalics[] = {
+ [uiDrawTextItalicNormal] = PANGO_STYLE_NORMAL,
+ [uiDrawTextItalicOblique] = PANGO_STYLE_OBLIQUE,
+ [uiDrawTextItalicItalic] = PANGO_STYLE_ITALIC,
+};
+
+static const PangoStretch pangoStretches[] = {
+ [uiDrawTextStretchUltraCondensed] = PANGO_STRETCH_ULTRA_CONDENSED,
+ [uiDrawTextStretchExtraCondensed] = PANGO_STRETCH_EXTRA_CONDENSED,
+ [uiDrawTextStretchCondensed] = PANGO_STRETCH_CONDENSED,
+ [uiDrawTextStretchSemiCondensed] = PANGO_STRETCH_SEMI_CONDENSED,
+ [uiDrawTextStretchNormal] = PANGO_STRETCH_NORMAL,
+ [uiDrawTextStretchSemiExpanded] = PANGO_STRETCH_SEMI_EXPANDED,
+ [uiDrawTextStretchExpanded] = PANGO_STRETCH_EXPANDED,
+ [uiDrawTextStretchExtraExpanded] = PANGO_STRETCH_EXTRA_EXPANDED,
+ [uiDrawTextStretchUltraExpanded] = PANGO_STRETCH_ULTRA_EXPANDED,
+};
+
+// we need a context for a few things
+// the documentation suggests creating cairo_t-specific, GdkScreen-specific, or even GtkWidget-specific contexts, but we can't really do that because we want our uiDrawTextFonts and uiDrawTextLayouts to be context-independent
+// we could use pango_font_map_create_context(pango_cairo_font_map_get_default()) but that will ignore GDK-specific settings
+// so let's use gdk_pango_context_get() instead; even though it's for the default screen only, it's good enough for us
+#define mkGenericPangoCairoContext() (gdk_pango_context_get())
+
+PangoFont *pangoDescToPangoFont(PangoFontDescription *pdesc)
+{
+ PangoFont *f;
+ PangoContext *context;
+
+ // in this case, the context is necessary for the metrics to be correct
+ context = mkGenericPangoCairoContext();
+ f = pango_font_map_load_font(pango_cairo_font_map_get_default(), context, pdesc);
+ if (f == NULL) {
+ // LONGTERM
+ g_error("[libui] no match in pangoDescToPangoFont(); report to andlabs");
+ }
+ g_object_unref(context);
+ return f;
+}
+
+uiDrawTextFont *uiDrawLoadClosestFont(const uiDrawTextFontDescriptor *desc)
+{
+ PangoFont *f;
+ PangoFontDescription *pdesc;
+
+ pdesc = pango_font_description_new();
+ pango_font_description_set_family(pdesc,
+ desc->Family);
+ pango_font_description_set_size(pdesc,
+ (gint) (desc->Size * PANGO_SCALE));
+ pango_font_description_set_weight(pdesc,
+ pangoWeights[desc->Weight]);
+ pango_font_description_set_style(pdesc,
+ pangoItalics[desc->Italic]);
+ pango_font_description_set_stretch(pdesc,
+ pangoStretches[desc->Stretch]);
+ f = pangoDescToPangoFont(pdesc);
+ pango_font_description_free(pdesc);
+ return mkTextFont(f, FALSE); // we hold the initial reference; no need to ref
+}
+
+void uiDrawFreeTextFont(uiDrawTextFont *font)
+{
+ g_object_unref(font->f);
+ uiFree(font);
+}
+
+uintptr_t uiDrawTextFontHandle(uiDrawTextFont *font)
+{
+ return (uintptr_t) (font->f);
+}
+
+void uiDrawTextFontDescribe(uiDrawTextFont *font, uiDrawTextFontDescriptor *desc)
+{
+ PangoFontDescription *pdesc;
+
+ // this creates a copy; we free it later
+ pdesc = pango_font_describe(font->f);
+
+ // TODO
+
+ pango_font_description_free(pdesc);
+}
+
+// See https://developer.gnome.org/pango/1.30/pango-Cairo-Rendering.html#pango-Cairo-Rendering.description
+// Note that we convert to double before dividing to make sure the floating-point stuff is right
+#define pangoToCairo(pango) (((double) (pango)) / PANGO_SCALE)
+#define cairoToPango(cairo) ((gint) ((cairo) * PANGO_SCALE))
+
+void uiDrawTextFontGetMetrics(uiDrawTextFont *font, uiDrawTextFontMetrics *metrics)
+{
+ PangoFontMetrics *pm;
+
+ pm = pango_font_get_metrics(font->f, NULL);
+ metrics->Ascent = pangoToCairo(pango_font_metrics_get_ascent(pm));
+ metrics->Descent = pangoToCairo(pango_font_metrics_get_descent(pm));
+ // Pango doesn't seem to expose this :( Use 0 and hope for the best.
+ metrics->Leading = 0;
+ metrics->UnderlinePos = pangoToCairo(pango_font_metrics_get_underline_position(pm));
+ metrics->UnderlineThickness = pangoToCairo(pango_font_metrics_get_underline_thickness(pm));
+ pango_font_metrics_unref(pm);
+}
+
+// note: PangoCairoLayouts are tied to a given cairo_t, so we can't store one in this device-independent structure
+struct uiDrawTextLayout {
+ char *s;
+ ptrdiff_t *graphemes;
+ PangoFont *defaultFont;
+ double width;
+ PangoAttrList *attrs;
+};
+
+uiDrawTextLayout *uiDrawNewTextLayout(const char *text, uiDrawTextFont *defaultFont, double width)
+{
+ uiDrawTextLayout *layout;
+ PangoContext *context;
+
+ layout = uiNew(uiDrawTextLayout);
+ layout->s = g_strdup(text);
+ context = mkGenericPangoCairoContext();
+ layout->graphemes = graphemes(layout->s, context);
+ g_object_unref(context);
+ layout->defaultFont = defaultFont->f;
+ g_object_ref(layout->defaultFont); // retain a copy
+ uiDrawTextLayoutSetWidth(layout, width);
+ layout->attrs = pango_attr_list_new();
+ return layout;
+}
+
+void uiDrawFreeTextLayout(uiDrawTextLayout *layout)
+{
+ pango_attr_list_unref(layout->attrs);
+ g_object_unref(layout->defaultFont);
+ uiFree(layout->graphemes);
+ g_free(layout->s);
+ uiFree(layout);
+}
+
+void uiDrawTextLayoutSetWidth(uiDrawTextLayout *layout, double width)
+{
+ layout->width = width;
+}
+
+static void prepareLayout(uiDrawTextLayout *layout, PangoLayout *pl)
+{
+ PangoFontDescription *desc;
+ int width;
+
+ pango_layout_set_text(pl, layout->s, -1);
+
+ // again, this makes a copy
+ desc = pango_font_describe(layout->defaultFont);
+ // this is safe; the description is copied
+ pango_layout_set_font_description(pl, desc);
+ pango_font_description_free(desc);
+
+ width = cairoToPango(layout->width);
+ if (layout->width < 0)
+ width = -1;
+ pango_layout_set_width(pl, width);
+
+ pango_layout_set_attributes(pl, layout->attrs);
+}
+
+void uiDrawTextLayoutExtents(uiDrawTextLayout *layout, double *width, double *height)
+{
+ PangoContext *context;
+ PangoLayout *pl;
+ PangoRectangle logical;
+
+ // in this case, the context is necessary to create the layout
+ // the layout takes a ref on the context so we can unref it afterward
+ context = mkGenericPangoCairoContext();
+ pl = pango_layout_new(context);
+ g_object_unref(context);
+ prepareLayout(layout, pl);
+
+ pango_layout_get_extents(pl, NULL, &logical);
+
+ g_object_unref(pl);
+
+ *width = pangoToCairo(logical.width);
+ *height = pangoToCairo(logical.height);
+}
+
+void uiDrawText(uiDrawContext *c, double x, double y, uiDrawTextLayout *layout)
+{
+ PangoLayout *pl;
+
+ pl = pango_cairo_create_layout(c->cr);
+ prepareLayout(layout, pl);
+
+ cairo_move_to(c->cr, x, y);
+ pango_cairo_show_layout(c->cr, pl);
+
+ g_object_unref(pl);
+}
+
+static void addAttr(uiDrawTextLayout *layout, PangoAttribute *attr, int startChar, int endChar)
+{
+ attr->start_index = layout->graphemes[startChar];
+ attr->end_index = layout->graphemes[endChar];
+ pango_attr_list_insert(layout->attrs, attr);
+ // pango_attr_list_insert() takes attr; we don't free it
+}
+
+void uiDrawTextLayoutSetColor(uiDrawTextLayout *layout, int startChar, int endChar, double r, double g, double b, double a)
+{
+ PangoAttribute *attr;
+ guint16 rr, gg, bb, aa;
+
+ rr = (guint16) (r * 65535);
+ gg = (guint16) (g * 65535);
+ bb = (guint16) (b * 65535);
+ aa = (guint16) (a * 65535);
+
+ attr = pango_attr_foreground_new(rr, gg, bb);
+ addAttr(layout, attr, startChar, endChar);
+
+ // TODO what if aa == 0?
+ attr = FUTURE_pango_attr_foreground_alpha_new(aa);
+ if (attr != NULL)
+ addAttr(layout, attr, startChar, endChar);
+}
diff --git a/src/libui_sdl/libui/unix/editablecombo.c b/src/libui_sdl/libui/unix/editablecombo.c
new file mode 100644
index 0000000..7ee3829
--- /dev/null
+++ b/src/libui_sdl/libui/unix/editablecombo.c
@@ -0,0 +1,79 @@
+// 11 june 2015
+#include "uipriv_unix.h"
+
+struct uiEditableCombobox {
+ uiUnixControl c;
+ GtkWidget *widget;
+ GtkBin *bin;
+ GtkComboBox *combobox;
+ GtkComboBoxText *comboboxText;
+ void (*onChanged)(uiEditableCombobox *, void *);
+ void *onChangedData;
+ gulong onChangedSignal;
+};
+
+uiUnixControlAllDefaults(uiEditableCombobox)
+
+static void onChanged(GtkComboBox *cbox, gpointer data)
+{
+ uiEditableCombobox *c = uiEditableCombobox(data);
+
+ (*(c->onChanged))(c, c->onChangedData);
+}
+
+static void defaultOnChanged(uiEditableCombobox *c, void *data)
+{
+ // do nothing
+}
+
+void uiEditableComboboxAppend(uiEditableCombobox *c, const char *text)
+{
+ gtk_combo_box_text_append(c->comboboxText, NULL, text);
+}
+
+char *uiEditableComboboxText(uiEditableCombobox *c)
+{
+ char *s;
+ char *out;
+
+ s = gtk_combo_box_text_get_active_text(c->comboboxText);
+ // s will always be non-NULL in the case of a combobox with an entry (according to the source code)
+ out = uiUnixStrdupText(s);
+ g_free(s);
+ return out;
+}
+
+void uiEditableComboboxSetText(uiEditableCombobox *c, const char *text)
+{
+ GtkEntry *e;
+
+ // we need to inhibit sending of ::changed because this WILL send a ::changed otherwise
+ g_signal_handler_block(c->combobox, c->onChangedSignal);
+ // since there isn't a gtk_combo_box_text_set_active_text()...
+ e = GTK_ENTRY(gtk_bin_get_child(c->bin));
+ gtk_entry_set_text(e, text);
+ g_signal_handler_unblock(c->combobox, c->onChangedSignal);
+}
+
+void uiEditableComboboxOnChanged(uiEditableCombobox *c, void (*f)(uiEditableCombobox *c, void *data), void *data)
+{
+ c->onChanged = f;
+ c->onChangedData = data;
+}
+
+uiEditableCombobox *uiNewEditableCombobox(void)
+{
+ uiEditableCombobox *c;
+
+ uiUnixNewControl(uiEditableCombobox, c);
+
+ c->widget = gtk_combo_box_text_new_with_entry();
+ c->bin = GTK_BIN(c->widget);
+ c->combobox = GTK_COMBO_BOX(c->widget);
+ c->comboboxText = GTK_COMBO_BOX_TEXT(c->widget);
+
+ c->onChangedSignal = g_signal_connect(c->widget, "changed", G_CALLBACK(onChanged), c);
+ uiEditableComboboxOnChanged(c, defaultOnChanged, NULL);
+
+ return c;
+}
diff --git a/src/libui_sdl/libui/unix/entry.c b/src/libui_sdl/libui/unix/entry.c
new file mode 100644
index 0000000..4a9a1d0
--- /dev/null
+++ b/src/libui_sdl/libui/unix/entry.c
@@ -0,0 +1,97 @@
+// 11 june 2015
+#include "uipriv_unix.h"
+
+struct uiEntry {
+ uiUnixControl c;
+ GtkWidget *widget;
+ GtkEntry *entry;
+ GtkEditable *editable;
+ void (*onChanged)(uiEntry *, void *);
+ void *onChangedData;
+ gulong onChangedSignal;
+};
+
+uiUnixControlAllDefaults(uiEntry)
+
+static void onChanged(GtkEditable *editable, gpointer data)
+{
+ uiEntry *e = uiEntry(data);
+
+ (*(e->onChanged))(e, e->onChangedData);
+}
+
+static void defaultOnChanged(uiEntry *e, void *data)
+{
+ // do nothing
+}
+
+char *uiEntryText(uiEntry *e)
+{
+ return uiUnixStrdupText(gtk_entry_get_text(e->entry));
+}
+
+void uiEntrySetText(uiEntry *e, const char *text)
+{
+ // we need to inhibit sending of ::changed because this WILL send a ::changed otherwise
+ g_signal_handler_block(e->editable, e->onChangedSignal);
+ gtk_entry_set_text(e->entry, text);
+ g_signal_handler_unblock(e->editable, e->onChangedSignal);
+ // don't queue the control for resize; entry sizes are independent of their contents
+}
+
+void uiEntryOnChanged(uiEntry *e, void (*f)(uiEntry *, void *), void *data)
+{
+ e->onChanged = f;
+ e->onChangedData = data;
+}
+
+int uiEntryReadOnly(uiEntry *e)
+{
+ return gtk_editable_get_editable(e->editable) == FALSE;
+}
+
+void uiEntrySetReadOnly(uiEntry *e, int readonly)
+{
+ gboolean editable;
+
+ editable = TRUE;
+ if (readonly)
+ editable = FALSE;
+ gtk_editable_set_editable(e->editable, editable);
+}
+
+static uiEntry *finishNewEntry(GtkWidget *w, const gchar *signal)
+{
+ uiEntry *e;
+
+ uiUnixNewControl(uiEntry, e);
+
+ e->widget = w;
+ e->entry = GTK_ENTRY(e->widget);
+ e->editable = GTK_EDITABLE(e->widget);
+
+ e->onChangedSignal = g_signal_connect(e->widget, signal, G_CALLBACK(onChanged), e);
+ uiEntryOnChanged(e, defaultOnChanged, NULL);
+
+ return e;
+}
+
+uiEntry *uiNewEntry(void)
+{
+ return finishNewEntry(gtk_entry_new(), "changed");
+}
+
+uiEntry *uiNewPasswordEntry(void)
+{
+ GtkWidget *e;
+
+ e = gtk_entry_new();
+ gtk_entry_set_visibility(GTK_ENTRY(e), FALSE);
+ return finishNewEntry(e, "changed");
+}
+
+// TODO make it use a separate function to be type-safe
+uiEntry *uiNewSearchEntry(void)
+{
+ return finishNewEntry(gtk_search_entry_new(), "search-changed");
+}
diff --git a/src/libui_sdl/libui/unix/fontbutton.c b/src/libui_sdl/libui/unix/fontbutton.c
new file mode 100644
index 0000000..f8047e0
--- /dev/null
+++ b/src/libui_sdl/libui/unix/fontbutton.c
@@ -0,0 +1,70 @@
+// 14 april 2016
+#include "uipriv_unix.h"
+
+struct uiFontButton {
+ uiUnixControl c;
+ GtkWidget *widget;
+ GtkButton *button;
+ GtkFontButton *fb;
+ GtkFontChooser *fc;
+ void (*onChanged)(uiFontButton *, void *);
+ void *onChangedData;
+};
+
+uiUnixControlAllDefaults(uiFontButton)
+
+// TODO NOTE no need to inhibit the signal; font-set is documented as only being sent when the user changes the font
+static void onFontSet(GtkFontButton *button, gpointer data)
+{
+ uiFontButton *b = uiFontButton(data);
+
+ (*(b->onChanged))(b, b->onChangedData);
+}
+
+static void defaultOnChanged(uiFontButton *b, void *data)
+{
+ // do nothing
+}
+
+uiDrawTextFont *uiFontButtonFont(uiFontButton *b)
+{
+ PangoFont *f;
+ PangoFontDescription *desc;
+
+ desc = gtk_font_chooser_get_font_desc(b->fc);
+ f = pangoDescToPangoFont(desc);
+ // desc is transfer-full and thus is a copy
+ pango_font_description_free(desc);
+ return mkTextFont(f, FALSE); // we hold the initial reference; no need to ref
+}
+
+void uiFontButtonOnChanged(uiFontButton *b, void (*f)(uiFontButton *, void *), void *data)
+{
+ b->onChanged = f;
+ b->onChangedData = data;
+}
+
+uiFontButton *uiNewFontButton(void)
+{
+ uiFontButton *b;
+
+ uiUnixNewControl(uiFontButton, b);
+
+ b->widget = gtk_font_button_new();
+ b->button = GTK_BUTTON(b->widget);
+ b->fb = GTK_FONT_BUTTON(b->widget);
+ b->fc = GTK_FONT_CHOOSER(b->widget);
+
+ // match behavior on other platforms
+ gtk_font_button_set_show_style(b->fb, TRUE);
+ gtk_font_button_set_show_size(b->fb, TRUE);
+ gtk_font_button_set_use_font(b->fb, FALSE);
+ gtk_font_button_set_use_size(b->fb, FALSE);
+ // other customizations
+ gtk_font_chooser_set_show_preview_entry(b->fc, TRUE);
+
+ g_signal_connect(b->widget, "font-set", G_CALLBACK(onFontSet), b);
+ uiFontButtonOnChanged(b, defaultOnChanged, NULL);
+
+ return b;
+}
diff --git a/src/libui_sdl/libui/unix/form.c b/src/libui_sdl/libui/unix/form.c
new file mode 100644
index 0000000..54422b3
--- /dev/null
+++ b/src/libui_sdl/libui/unix/form.c
@@ -0,0 +1,159 @@
+// 8 june 2016
+#include "uipriv_unix.h"
+
+struct formChild {
+ uiControl *c;
+ int stretchy;
+ GtkWidget *label;
+ gboolean oldhexpand;
+ GtkAlign oldhalign;
+ gboolean oldvexpand;
+ GtkAlign oldvalign;
+ GBinding *labelBinding;
+};
+
+struct uiForm {
+ uiUnixControl c;
+ GtkWidget *widget;
+ GtkContainer *container;
+ GtkGrid *grid;
+ GArray *children;
+ int padded;
+ GtkSizeGroup *stretchygroup; // ensures all stretchy controls have the same size
+};
+
+uiUnixControlAllDefaultsExceptDestroy(uiForm)
+
+#define ctrl(f, i) &g_array_index(f->children, struct formChild, i)
+
+static void uiFormDestroy(uiControl *c)
+{
+ uiForm *f = uiForm(c);
+ struct formChild *fc;
+ guint i;
+
+ // kill the size group
+ g_object_unref(f->stretchygroup);
+ // free all controls
+ for (i = 0; i < f->children->len; i++) {
+ fc = ctrl(f, i);
+ uiControlSetParent(fc->c, NULL);
+ uiUnixControlSetContainer(uiUnixControl(fc->c), f->container, TRUE);
+ uiControlDestroy(fc->c);
+ gtk_widget_destroy(fc->label);
+ }
+ g_array_free(f->children, TRUE);
+ // and then ourselves
+ g_object_unref(f->widget);
+ uiFreeControl(uiControl(f));
+}
+
+void uiFormAppend(uiForm *f, const char *label, uiControl *c, int stretchy)
+{
+ struct formChild fc;
+ GtkWidget *widget;
+ guint row;
+
+ fc.c = c;
+ widget = GTK_WIDGET(uiControlHandle(fc.c));
+ fc.stretchy = stretchy;
+ fc.oldhexpand = gtk_widget_get_hexpand(widget);
+ fc.oldhalign = gtk_widget_get_halign(widget);
+ fc.oldvexpand = gtk_widget_get_vexpand(widget);
+ fc.oldvalign = gtk_widget_get_valign(widget);
+
+ if (stretchy) {
+ gtk_widget_set_vexpand(widget, TRUE);
+ gtk_widget_set_valign(widget, GTK_ALIGN_FILL);
+ gtk_size_group_add_widget(f->stretchygroup, widget);
+ } else
+ gtk_widget_set_vexpand(widget, FALSE);
+ // and make them fill horizontally
+ gtk_widget_set_hexpand(widget, TRUE);
+ gtk_widget_set_halign(widget, GTK_ALIGN_FILL);
+
+ fc.label = gtk_label_new(label);
+ gtk_widget_set_hexpand(fc.label, FALSE);
+ gtk_widget_set_halign(fc.label, GTK_ALIGN_END);
+ gtk_widget_set_vexpand(fc.label, FALSE);
+ if (GTK_IS_SCROLLED_WINDOW(widget))
+ gtk_widget_set_valign(fc.label, GTK_ALIGN_START);
+ else
+ gtk_widget_set_valign(fc.label, GTK_ALIGN_CENTER);
+ gtk_style_context_add_class(gtk_widget_get_style_context(fc.label), "dim-label");
+ row = f->children->len;
+ gtk_grid_attach(f->grid, fc.label,
+ 0, row,
+ 1, 1);
+ // and make them share visibility so if the control is hidden, so is its label
+ fc.labelBinding = g_object_bind_property(GTK_WIDGET(uiControlHandle(fc.c)), "visible",
+ fc.label, "visible",
+ G_BINDING_SYNC_CREATE);
+
+ uiControlSetParent(fc.c, uiControl(f));
+ uiUnixControlSetContainer(uiUnixControl(fc.c), f->container, FALSE);
+ g_array_append_val(f->children, fc);
+
+ // move the widget to the correct place
+ gtk_container_child_set(f->container, widget,
+ "left-attach", 1,
+ "top-attach", row,
+ NULL);
+}
+
+void uiFormDelete(uiForm *f, int index)
+{
+ struct formChild *fc;
+ GtkWidget *widget;
+
+ fc = ctrl(f, index);
+ widget = GTK_WIDGET(uiControlHandle(fc->c));
+
+ gtk_widget_destroy(fc->label);
+
+ uiControlSetParent(fc->c, NULL);
+ uiUnixControlSetContainer(uiUnixControl(fc->c), f->container, TRUE);
+
+ if (fc->stretchy)
+ gtk_size_group_remove_widget(f->stretchygroup, widget);
+ gtk_widget_set_hexpand(widget, fc->oldhexpand);
+ gtk_widget_set_halign(widget, fc->oldhalign);
+ gtk_widget_set_vexpand(widget, fc->oldvexpand);
+ gtk_widget_set_valign(widget, fc->oldvalign);
+
+ g_array_remove_index(f->children, index);
+}
+
+int uiFormPadded(uiForm *f)
+{
+ return f->padded;
+}
+
+void uiFormSetPadded(uiForm *f, int padded)
+{
+ f->padded = padded;
+ if (f->padded) {
+ gtk_grid_set_row_spacing(f->grid, gtkYPadding);
+ gtk_grid_set_column_spacing(f->grid, gtkXPadding);
+ } else {
+ gtk_grid_set_row_spacing(f->grid, 0);
+ gtk_grid_set_column_spacing(f->grid, 0);
+ }
+}
+
+uiForm *uiNewForm(void)
+{
+ uiForm *f;
+
+ uiUnixNewControl(uiForm, f);
+
+ f->widget = gtk_grid_new();
+ f->container = GTK_CONTAINER(f->widget);
+ f->grid = GTK_GRID(f->widget);
+
+ f->stretchygroup = gtk_size_group_new(GTK_SIZE_GROUP_VERTICAL);
+
+ f->children = g_array_new(FALSE, TRUE, sizeof (struct formChild));
+
+ return f;
+}
diff --git a/src/libui_sdl/libui/unix/future.c b/src/libui_sdl/libui/unix/future.c
new file mode 100644
index 0000000..1f9f532
--- /dev/null
+++ b/src/libui_sdl/libui/unix/future.c
@@ -0,0 +1,42 @@
+// 29 june 2016
+#include "uipriv_unix.h"
+
+// functions FROM THE FUTURE!
+// in some cases, because being held back by LTS releases sucks :/
+// in others, because parts of GTK+ being unstable until recently also sucks :/
+
+// added in pango 1.38; we need 1.36
+static PangoAttribute *(*newFGAlphaAttr)(guint16 alpha) = NULL;
+
+// added in GTK+ 3.20; we need 3.10
+static void (*gwpIterSetObjectName)(GtkWidgetPath *path, gint pos, const char *name) = NULL;
+
+// note that we treat any error as "the symbols aren't there" (and don't care if dlclose() failed)
+void loadFutures(void)
+{
+ void *handle;
+
+ // dlsym() walks the dependency chain, so opening the current process should be sufficient
+ handle = dlopen(NULL, RTLD_LAZY);
+ if (handle == NULL)
+ return;
+#define GET(var, fn) *((void **) (&var)) = dlsym(handle, #fn)
+ GET(newFGAlphaAttr, pango_attr_foreground_alpha_new);
+ GET(gwpIterSetObjectName, gtk_widget_path_iter_set_object_name);
+ dlclose(handle);
+}
+
+PangoAttribute *FUTURE_pango_attr_foreground_alpha_new(guint16 alpha)
+{
+ if (newFGAlphaAttr == NULL)
+ return NULL;
+ return (*newFGAlphaAttr)(alpha);
+}
+
+gboolean FUTURE_gtk_widget_path_iter_set_object_name(GtkWidgetPath *path, gint pos, const char *name)
+{
+ if (gwpIterSetObjectName == NULL)
+ return FALSE;
+ (*gwpIterSetObjectName)(path, pos, name);
+ return TRUE;
+}
diff --git a/src/libui_sdl/libui/unix/graphemes.c b/src/libui_sdl/libui/unix/graphemes.c
new file mode 100644
index 0000000..a2c47b7
--- /dev/null
+++ b/src/libui_sdl/libui/unix/graphemes.c
@@ -0,0 +1,31 @@
+// 25 may 2016
+#include "uipriv_unix.h"
+
+ptrdiff_t *graphemes(const char *text, PangoContext *context)
+{
+ size_t len, lenchars;
+ PangoLogAttr *logattrs;
+ ptrdiff_t *out;
+ ptrdiff_t *op;
+ size_t i;
+
+ len = strlen(text);
+ lenchars = g_utf8_strlen(text, -1);
+ logattrs = (PangoLogAttr *) uiAlloc((lenchars + 1) * sizeof (PangoLogAttr), "PangoLogAttr[]");
+ pango_get_log_attrs(text, len,
+ -1, NULL,
+ logattrs, lenchars + 1);
+
+ // should be more than enough
+ out = (ptrdiff_t *) uiAlloc((lenchars + 2) * sizeof (ptrdiff_t), "ptrdiff_t[]");
+ op = out;
+ for (i = 0; i < lenchars; i++)
+ if (logattrs[i].is_cursor_position != 0)
+ // TODO optimize this
+ *op++ = g_utf8_offset_to_pointer(text, i) - text;
+ // and do the last one
+ *op++ = len;
+
+ uiFree(logattrs);
+ return out;
+}
diff --git a/src/libui_sdl/libui/unix/grid.c b/src/libui_sdl/libui/unix/grid.c
new file mode 100644
index 0000000..6d9813b
--- /dev/null
+++ b/src/libui_sdl/libui/unix/grid.c
@@ -0,0 +1,141 @@
+// 9 june 2016
+#include "uipriv_unix.h"
+
+struct gridChild {
+ uiControl *c;
+ GtkWidget *label;
+ gboolean oldhexpand;
+ GtkAlign oldhalign;
+ gboolean oldvexpand;
+ GtkAlign oldvalign;
+};
+
+struct uiGrid {
+ uiUnixControl c;
+ GtkWidget *widget;
+ GtkContainer *container;
+ GtkGrid *grid;
+ GArray *children;
+ int padded;
+};
+
+uiUnixControlAllDefaultsExceptDestroy(uiGrid)
+
+#define ctrl(g, i) &g_array_index(g->children, struct gridChild, i)
+
+static void uiGridDestroy(uiControl *c)
+{
+ uiGrid *g = uiGrid(c);
+ struct gridChild *gc;
+ guint i;
+
+ // free all controls
+ for (i = 0; i < g->children->len; i++) {
+ gc = ctrl(g, i);
+ uiControlSetParent(gc->c, NULL);
+ uiUnixControlSetContainer(uiUnixControl(gc->c), g->container, TRUE);
+ uiControlDestroy(gc->c);
+ }
+ g_array_free(g->children, TRUE);
+ // and then ourselves
+ g_object_unref(g->widget);
+ uiFreeControl(uiControl(g));
+}
+
+#define TODO_MASSIVE_HACK(c) \
+ if (!uiUnixControl(c)->addedBefore) { \
+ g_object_ref_sink(GTK_WIDGET(uiControlHandle(uiControl(c)))); \
+ gtk_widget_show(GTK_WIDGET(uiControlHandle(uiControl(c)))); \
+ uiUnixControl(c)->addedBefore = TRUE; \
+ }
+
+static const GtkAlign gtkAligns[] = {
+ [uiAlignFill] = GTK_ALIGN_FILL,
+ [uiAlignStart] = GTK_ALIGN_START,
+ [uiAlignCenter] = GTK_ALIGN_CENTER,
+ [uiAlignEnd] = GTK_ALIGN_END,
+};
+
+static const GtkPositionType gtkPositions[] = {
+ [uiAtLeading] = GTK_POS_LEFT,
+ [uiAtTop] = GTK_POS_TOP,
+ [uiAtTrailing] = GTK_POS_RIGHT,
+ [uiAtBottom] = GTK_POS_BOTTOM,
+};
+
+static GtkWidget *prepare(struct gridChild *gc, uiControl *c, int hexpand, uiAlign halign, int vexpand, uiAlign valign)
+{
+ GtkWidget *widget;
+
+ gc->c = c;
+ widget = GTK_WIDGET(uiControlHandle(gc->c));
+ gc->oldhexpand = gtk_widget_get_hexpand(widget);
+ gc->oldhalign = gtk_widget_get_halign(widget);
+ gc->oldvexpand = gtk_widget_get_vexpand(widget);
+ gc->oldvalign = gtk_widget_get_valign(widget);
+ gtk_widget_set_hexpand(widget, hexpand != 0);
+ gtk_widget_set_halign(widget, gtkAligns[halign]);
+ gtk_widget_set_vexpand(widget, vexpand != 0);
+ gtk_widget_set_valign(widget, gtkAligns[valign]);
+ return widget;
+}
+
+void uiGridAppend(uiGrid *g, uiControl *c, int left, int top, int xspan, int yspan, int hexpand, uiAlign halign, int vexpand, uiAlign valign)
+{
+ struct gridChild gc;
+ GtkWidget *widget;
+
+ widget = prepare(&gc, c, hexpand, halign, vexpand, valign);
+ uiControlSetParent(gc.c, uiControl(g));
+ TODO_MASSIVE_HACK(uiUnixControl(gc.c));
+ gtk_grid_attach(g->grid, widget,
+ left, top,
+ xspan, yspan);
+ g_array_append_val(g->children, gc);
+}
+
+void uiGridInsertAt(uiGrid *g, uiControl *c, uiControl *existing, uiAt at, int xspan, int yspan, int hexpand, uiAlign halign, int vexpand, uiAlign valign)
+{
+ struct gridChild gc;
+ GtkWidget *widget;
+
+ widget = prepare(&gc, c, hexpand, halign, vexpand, valign);
+ uiControlSetParent(gc.c, uiControl(g));
+ TODO_MASSIVE_HACK(uiUnixControl(gc.c));
+ gtk_grid_attach_next_to(g->grid, widget,
+ GTK_WIDGET(uiControlHandle(existing)), gtkPositions[at],
+ xspan, yspan);
+ g_array_append_val(g->children, gc);
+}
+
+int uiGridPadded(uiGrid *g)
+{
+ return g->padded;
+}
+
+void uiGridSetPadded(uiGrid *g, int padded)
+{
+ g->padded = padded;
+ if (g->padded) {
+ gtk_grid_set_row_spacing(g->grid, gtkYPadding);
+ gtk_grid_set_column_spacing(g->grid, gtkXPadding);
+ } else {
+ gtk_grid_set_row_spacing(g->grid, 0);
+ gtk_grid_set_column_spacing(g->grid, 0);
+ }
+}
+
+uiGrid *uiNewGrid(void)
+{
+ uiGrid *g;
+
+ uiUnixNewControl(uiGrid, g);
+
+ g->widget = gtk_grid_new();
+ g->container = GTK_CONTAINER(g->widget);
+ g->grid = GTK_GRID(g->widget);
+
+ g->children = g_array_new(FALSE, TRUE, sizeof (struct gridChild));
+
+ return g;
+}
diff --git a/src/libui_sdl/libui/unix/group.c b/src/libui_sdl/libui/unix/group.c
new file mode 100644
index 0000000..6238a1b
--- /dev/null
+++ b/src/libui_sdl/libui/unix/group.c
@@ -0,0 +1,89 @@
+// 11 june 2015
+#include "uipriv_unix.h"
+
+struct uiGroup {
+ uiUnixControl c;
+ GtkWidget *widget;
+ GtkContainer *container;
+ GtkBin *bin;
+ GtkFrame *frame;
+
+ // unfortunately, even though a GtkFrame is a GtkBin, calling gtk_container_set_border_width() on it /includes/ the GtkFrame's label; we don't want tht
+ struct child *child;
+
+ int margined;
+};
+
+uiUnixControlAllDefaultsExceptDestroy(uiGroup)
+
+static void uiGroupDestroy(uiControl *c)
+{
+ uiGroup *g = uiGroup(c);
+
+ if (g->child != NULL)
+ childDestroy(g->child);
+ g_object_unref(g->widget);
+ uiFreeControl(uiControl(g));
+}
+
+char *uiGroupTitle(uiGroup *g)
+{
+ return uiUnixStrdupText(gtk_frame_get_label(g->frame));
+}
+
+void uiGroupSetTitle(uiGroup *g, const char *text)
+{
+ gtk_frame_set_label(g->frame, text);
+}
+
+void uiGroupSetChild(uiGroup *g, uiControl *child)
+{
+ if (g->child != NULL)
+ childRemove(g->child);
+ g->child = newChildWithBox(child, uiControl(g), g->container, g->margined);
+}
+
+int uiGroupMargined(uiGroup *g)
+{
+ return g->margined;
+}
+
+void uiGroupSetMargined(uiGroup *g, int margined)
+{
+ g->margined = margined;
+ if (g->child != NULL)
+ childSetMargined(g->child, g->margined);
+}
+
+uiGroup *uiNewGroup(const char *text)
+{
+ uiGroup *g;
+ gfloat yalign;
+ GtkLabel *label;
+ PangoAttribute *bold;
+ PangoAttrList *boldlist;
+
+ uiUnixNewControl(uiGroup, g);
+
+ g->widget = gtk_frame_new(text);
+ g->container = GTK_CONTAINER(g->widget);
+ g->bin = GTK_BIN(g->widget);
+ g->frame = GTK_FRAME(g->widget);
+
+ // with GTK+, groupboxes by default have frames and slightly x-offset regular text
+ // they should have no frame and fully left-justified, bold text
+ // preserve default y-alignment
+ gtk_frame_get_label_align(g->frame, NULL, &yalign);
+ gtk_frame_set_label_align(g->frame, 0, yalign);
+ gtk_frame_set_shadow_type(g->frame, GTK_SHADOW_NONE);
+ label = GTK_LABEL(gtk_frame_get_label_widget(g->frame));
+ // this is the boldness level used by GtkPrintUnixDialog
+ // (it technically uses "bold" but see pango's pango-enum-types.c for the name conversion; GType is weird)
+ bold = pango_attr_weight_new(PANGO_WEIGHT_BOLD);
+ boldlist = pango_attr_list_new();
+ pango_attr_list_insert(boldlist, bold);
+ gtk_label_set_attributes(label, boldlist);
+ pango_attr_list_unref(boldlist); // thanks baedert in irc.gimp.net/#gtk+
+
+ return g;
+}
diff --git a/src/libui_sdl/libui/unix/image.c b/src/libui_sdl/libui/unix/image.c
new file mode 100644
index 0000000..a79e550
--- /dev/null
+++ b/src/libui_sdl/libui/unix/image.c
@@ -0,0 +1,120 @@
+// 27 june 2016
+#include "uipriv_unix.h"
+
+struct uiImage {
+ double width;
+ double height;
+ GPtrArray *images;
+};
+
+static void freeImageRep(gpointer item)
+{
+ cairo_surface_t *cs = (cairo_surface_t *) item;
+ unsigned char *buf;
+
+ buf = cairo_image_surface_get_data(cs);
+ cairo_surface_destroy(cs);
+ uiFree(buf);
+}
+
+uiImage *uiNewImage(double width, double height)
+{
+ uiImage *i;
+
+ i = uiNew(uiImage);
+ i->width = width;
+ i->height = height;
+ i->images = g_ptr_array_new_with_free_func(freeImageRep);
+ return i;
+}
+
+void uiFreeImage(uiImage *i)
+{
+ g_ptr_array_free(i->images, TRUE);
+ uiFree(i);
+}
+
+void uiImageAppend(uiImage *i, void *pixels, int pixelWidth, int pixelHeight, int pixelStride)
+{
+ cairo_surface_t *cs;
+ unsigned char *buf, *p;
+ uint8_t *src = (uint8_t *) pixels;
+ int cstride;
+ int y;
+
+ cstride = cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, pixelWidth);
+ buf = (unsigned char *) uiAlloc((cstride * pixelHeight * 4) * sizeof (unsigned char), "unsigned char[]");
+ p = buf;
+ for (y = 0; y < pixelStride * pixelHeight; y += pixelStride) {
+ memmove(p, src + y, cstride);
+ p += cstride;
+ }
+ cs = cairo_image_surface_create_for_data(buf, CAIRO_FORMAT_ARGB32,
+ pixelWidth, pixelHeight,
+ cstride);
+ if (cairo_surface_status(cs) != CAIRO_STATUS_SUCCESS)
+ /* TODO */;
+ cairo_surface_flush(cs);
+ g_ptr_array_add(i->images, cs);
+}
+
+struct matcher {
+ cairo_surface_t *best;
+ int distX;
+ int distY;
+ int targetX;
+ int targetY;
+ gboolean foundLarger;
+};
+
+// TODO is this the right algorithm?
+static void match(gpointer surface, gpointer data)
+{
+ cairo_surface_t *cs = (cairo_surface_t *) surface;
+ struct matcher *m = (struct matcher *) data;
+ int x, y;
+ int x2, y2;
+
+ x = cairo_image_surface_get_width(cs);
+ y = cairo_image_surface_get_height(cs);
+ if (m->best == NULL)
+ goto writeMatch;
+
+ if (x < m->targetX && y < m->targetY)
+ if (m->foundLarger)
+ // always prefer larger ones
+ return;
+ if (x >= m->targetX && y >= m->targetY && !m->foundLarger)
+ // we set foundLarger below
+ goto writeMatch;
+
+ x2 = abs(m->targetX - x);
+ y2 = abs(m->targetY - y);
+ if (x2 < m->distX && y2 < m->distY)
+ goto writeMatch;
+
+ // TODO weight one dimension? threshhold?
+ return;
+
+writeMatch:
+ // must set this here too; otherwise the first image will never have ths set
+ if (x >= m->targetX && y >= m->targetY && !m->foundLarger)
+ m->foundLarger = TRUE;
+ m->best = cs;
+ m->distX = abs(m->targetX - x);
+ m->distY = abs(m->targetY - y);
+}
+
+cairo_surface_t *imageAppropriateSurface(uiImage *i, GtkWidget *w)
+{
+ struct matcher m;
+
+ m.best = NULL;
+ m.distX = G_MAXINT;
+ m.distY = G_MAXINT;
+ m.targetX = i->width * gtk_widget_get_scale_factor(w);
+ m.targetY = i->height * gtk_widget_get_scale_factor(w);
+ m.foundLarger = FALSE;
+ g_ptr_array_foreach(i->images, match, &m);
+ return m.best;
+}
diff --git a/src/libui_sdl/libui/unix/label.c b/src/libui_sdl/libui/unix/label.c
new file mode 100644
index 0000000..b39fc7c
--- /dev/null
+++ b/src/libui_sdl/libui/unix/label.c
@@ -0,0 +1,36 @@
+// 11 june 2015
+#include "uipriv_unix.h"
+
+struct uiLabel {
+ uiUnixControl c;
+ GtkWidget *widget;
+ GtkMisc *misc;
+ GtkLabel *label;
+};
+
+uiUnixControlAllDefaults(uiLabel)
+
+char *uiLabelText(uiLabel *l)
+{
+ return uiUnixStrdupText(gtk_label_get_text(l->label));
+}
+
+void uiLabelSetText(uiLabel *l, const char *text)
+{
+ gtk_label_set_text(l->label, text);
+}
+
+uiLabel *uiNewLabel(const char *text)
+{
+ uiLabel *l;
+
+ uiUnixNewControl(uiLabel, l);
+
+ l->widget = gtk_label_new(text);
+ l->misc = GTK_MISC(l->widget);
+ l->label = GTK_LABEL(l->widget);
+
+ gtk_misc_set_alignment(l->misc, 0, 0);
+
+ return l;
+}
diff --git a/src/libui_sdl/libui/unix/main.c b/src/libui_sdl/libui/unix/main.c
new file mode 100644
index 0000000..2998bf3
--- /dev/null
+++ b/src/libui_sdl/libui/unix/main.c
@@ -0,0 +1,108 @@
+// 6 april 2015
+#include "uipriv_unix.h"
+
+uiInitOptions options;
+
+const char *uiInit(uiInitOptions *o)
+{
+ GError *err = NULL;
+ const char *msg;
+
+ options = *o;
+ if (gtk_init_with_args(NULL, NULL, NULL, NULL, NULL, &err) == FALSE) {
+ msg = g_strdup(err->message);
+ g_error_free(err);
+ return msg;
+ }
+ initAlloc();
+ loadFutures();
+ return NULL;
+}
+
+void uiUninit(void)
+{
+ uninitMenus();
+ uninitAlloc();
+}
+
+void uiFreeInitError(const char *err)
+{
+ g_free((gpointer) err);
+}
+
+static gboolean (*iteration)(gboolean) = NULL;
+
+void uiMain(void)
+{
+ iteration = gtk_main_iteration_do;
+ gtk_main();
+}
+
+static gboolean stepsQuit = FALSE;
+
+// the only difference is we ignore the return value from gtk_main_iteration_do(), since it will always be TRUE if gtk_main() was never called
+// gtk_main_iteration_do() will still run the main loop regardless
+static gboolean stepsIteration(gboolean block)
+{
+ gtk_main_iteration_do(block);
+ return stepsQuit;
+}
+
+void uiMainSteps(void)
+{
+ iteration = stepsIteration;
+}
+
+int uiMainStep(int wait)
+{
+ gboolean block;
+
+ block = FALSE;
+ if (wait)
+ block = TRUE;
+ return (*iteration)(block) == FALSE;
+}
+
+// gtk_main_quit() may run immediately, or it may wait for other pending events; "it depends" (thanks mclasen in irc.gimp.net/#gtk+)
+// PostQuitMessage() on Windows always waits, so we must do so too
+// we'll do it by using an idle callback
+static gboolean quit(gpointer data)
+{
+ if (iteration == stepsIteration)
+ stepsQuit = TRUE;
+ // TODO run a gtk_main() here just to do the cleanup steps of syncing the clipboard and other stuff gtk_main() does before it returns
+ else
+ gtk_main_quit();
+ return FALSE;
+}
+
+void uiQuit(void)
+{
+ gdk_threads_add_idle(quit, NULL);
+}
+
+struct queued {
+ void (*f)(void *);
+ void *data;
+};
+
+static gboolean doqueued(gpointer data)
+{
+ struct queued *q = (struct queued *) data;
+
+ (*(q->f))(q->data);
+ g_free(q);
+ return FALSE;
+}
+
+void uiQueueMain(void (*f)(void *data), void *data)
+{
+ struct queued *q;
+
+ // we have to use g_new0()/g_free() because uiAlloc() is only safe to call on the main thread
+ // for some reason it didn't affect me, but it did affect krakjoe
+ q = g_new0(struct queued, 1);
+ q->f = f;
+ q->data = data;
+ gdk_threads_add_idle(doqueued, q);
+}
diff --git a/src/libui_sdl/libui/unix/menu.c b/src/libui_sdl/libui/unix/menu.c
new file mode 100644
index 0000000..5ccb4a5
--- /dev/null
+++ b/src/libui_sdl/libui/unix/menu.c
@@ -0,0 +1,366 @@
+// 23 april 2015
+#include "uipriv_unix.h"
+
+static GArray *menus = NULL;
+static gboolean menusFinalized = FALSE;
+static gboolean hasQuit = FALSE;
+static gboolean hasPreferences = FALSE;
+static gboolean hasAbout = FALSE;
+
+struct uiMenu {
+ char *name;
+ GArray *items; // []*uiMenuItem
+};
+
+struct uiMenuItem {
+ char *name;
+ int type;
+ void (*onClicked)(uiMenuItem *, uiWindow *, void *);
+ void *onClickedData;
+ GType gtype; // template for new instances; kept in sync with everything else
+ gboolean disabled;
+ gboolean checked;
+ GHashTable *windows; // map[GtkMenuItem]*menuItemWindow
+};
+
+struct menuItemWindow {
+ uiWindow *w;
+ gulong signal;
+};
+
+enum {
+ typeRegular,
+ typeCheckbox,
+ typeQuit,
+ typePreferences,
+ typeAbout,
+ typeSeparator,
+};
+
+// we do NOT want programmatic updates to raise an ::activated signal
+static void singleSetChecked(GtkCheckMenuItem *menuitem, gboolean checked, gulong signal)
+{
+ g_signal_handler_block(menuitem, signal);
+ gtk_check_menu_item_set_active(menuitem, checked);
+ g_signal_handler_unblock(menuitem, signal);
+}
+
+static void setChecked(uiMenuItem *item, gboolean checked)
+{
+ GHashTableIter iter;
+ gpointer widget;
+ gpointer ww;
+ struct menuItemWindow *w;
+
+ item->checked = checked;
+ g_hash_table_iter_init(&iter, item->windows);
+ while (g_hash_table_iter_next(&iter, &widget, &ww)) {
+ w = (struct menuItemWindow *) ww;
+ singleSetChecked(GTK_CHECK_MENU_ITEM(widget), item->checked, w->signal);
+ }
+}
+
+static void onClicked(GtkMenuItem *menuitem, gpointer data)
+{
+ uiMenuItem *item = uiMenuItem(data);
+ struct menuItemWindow *w;
+
+ // we need to manually update the checked states of all menu items if one changes
+ // notice that this is getting the checked state of the menu item that this signal is sent from
+ if (item->type == typeCheckbox)
+ setChecked(item, gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(menuitem)));
+
+ w = (struct menuItemWindow *) g_hash_table_lookup(item->windows, menuitem);
+ (*(item->onClicked))(item, w->w, item->onClickedData);
+}
+
+static void defaultOnClicked(uiMenuItem *item, uiWindow *w, void *data)
+{
+ // do nothing
+}
+
+static void onQuitClicked(uiMenuItem *item, uiWindow *w, void *data)
+{
+ if (shouldQuit())
+ uiQuit();
+}
+
+static void menuItemEnableDisable(uiMenuItem *item, gboolean enabled)
+{
+ GHashTableIter iter;
+ gpointer widget;
+
+ item->disabled = !enabled;
+ g_hash_table_iter_init(&iter, item->windows);
+ while (g_hash_table_iter_next(&iter, &widget, NULL))
+ gtk_widget_set_sensitive(GTK_WIDGET(widget), enabled);
+}
+
+void uiMenuItemEnable(uiMenuItem *item)
+{
+ menuItemEnableDisable(item, TRUE);
+}
+
+void uiMenuItemDisable(uiMenuItem *item)
+{
+ menuItemEnableDisable(item, FALSE);
+}
+
+void uiMenuItemOnClicked(uiMenuItem *item, void (*f)(uiMenuItem *, uiWindow *, void *), void *data)
+{
+ if (item->type == typeQuit)
+ userbug("You cannot call uiMenuItemOnClicked() on a Quit item; use uiOnShouldQuit() instead.");
+ item->onClicked = f;
+ item->onClickedData = data;
+}
+
+int uiMenuItemChecked(uiMenuItem *item)
+{
+ return item->checked != FALSE;
+}
+
+void uiMenuItemSetChecked(uiMenuItem *item, int checked)
+{
+ gboolean c;
+
+ // use explicit values
+ c = FALSE;
+ if (checked)
+ c = TRUE;
+ setChecked(item, c);
+}
+
+static uiMenuItem *newItem(uiMenu *m, int type, const char *name)
+{
+ uiMenuItem *item;
+
+ if (menusFinalized)
+ userbug("You cannot create a new menu item after menus have been finalized.");
+
+ item = uiNew(uiMenuItem);
+
+ g_array_append_val(m->items, item);
+
+ item->type = type;
+ switch (item->type) {
+ case typeQuit:
+ item->name = g_strdup("Quit");
+ break;
+ case typePreferences:
+ item->name = g_strdup("Preferences...");
+ break;
+ case typeAbout:
+ item->name = g_strdup("About");
+ break;
+ case typeSeparator:
+ break;
+ default:
+ item->name = g_strdup(name);
+ break;
+ }
+
+ if (item->type == typeQuit) {
+ // can't call uiMenuItemOnClicked() here
+ item->onClicked = onQuitClicked;
+ item->onClickedData = NULL;
+ } else
+ uiMenuItemOnClicked(item, defaultOnClicked, NULL);
+
+ switch (item->type) {
+ case typeCheckbox:
+ item->gtype = GTK_TYPE_CHECK_MENU_ITEM;
+ break;
+ case typeSeparator:
+ item->gtype = GTK_TYPE_SEPARATOR_MENU_ITEM;
+ break;
+ default:
+ item->gtype = GTK_TYPE_MENU_ITEM;
+ break;
+ }
+
+ item->windows = g_hash_table_new(g_direct_hash, g_direct_equal);
+
+ return item;
+}
+
+uiMenuItem *uiMenuAppendItem(uiMenu *m, const char *name)
+{
+ return newItem(m, typeRegular, name);
+}
+
+uiMenuItem *uiMenuAppendCheckItem(uiMenu *m, const char *name)
+{
+ return newItem(m, typeCheckbox, name);
+}
+
+uiMenuItem *uiMenuAppendQuitItem(uiMenu *m)
+{
+ if (hasQuit)
+ userbug("You cannot have multiple Quit menu items in the same program.");
+ hasQuit = TRUE;
+ newItem(m, typeSeparator, NULL);
+ return newItem(m, typeQuit, NULL);
+}
+
+uiMenuItem *uiMenuAppendPreferencesItem(uiMenu *m)
+{
+ if (hasPreferences)
+ userbug("You cannot have multiple Preferences menu items in the same program.");
+ hasPreferences = TRUE;
+ newItem(m, typeSeparator, NULL);
+ return newItem(m, typePreferences, NULL);
+}
+
+uiMenuItem *uiMenuAppendAboutItem(uiMenu *m)
+{
+ if (hasAbout)
+ userbug("You cannot have multiple About menu items in the same program.");
+ hasAbout = TRUE;
+ newItem(m, typeSeparator, NULL);
+ return newItem(m, typeAbout, NULL);
+}
+
+void uiMenuAppendSeparator(uiMenu *m)
+{
+ newItem(m, typeSeparator, NULL);
+}
+
+uiMenu *uiNewMenu(const char *name)
+{
+ uiMenu *m;
+
+ if (menusFinalized)
+ userbug("You cannot create a new menu after menus have been finalized.");
+ if (menus == NULL)
+ menus = g_array_new(FALSE, TRUE, sizeof (uiMenu *));
+
+ m = uiNew(uiMenu);
+
+ g_array_append_val(menus, m);
+
+ m->name = g_strdup(name);
+ m->items = g_array_new(FALSE, TRUE, sizeof (uiMenuItem *));
+
+ return m;
+}
+
+static void appendMenuItem(GtkMenuShell *submenu, uiMenuItem *item, uiWindow *w)
+{
+ GtkWidget *menuitem;
+ gulong signal;
+ struct menuItemWindow *ww;
+
+ menuitem = g_object_new(item->gtype, NULL);
+ if (item->name != NULL)
+ gtk_menu_item_set_label(GTK_MENU_ITEM(menuitem), item->name);
+ if (item->type != typeSeparator) {
+ signal = g_signal_connect(menuitem, "activate", G_CALLBACK(onClicked), item);
+ gtk_widget_set_sensitive(menuitem, !item->disabled);
+ if (item->type == typeCheckbox)
+ singleSetChecked(GTK_CHECK_MENU_ITEM(menuitem), item->checked, signal);
+ }
+ gtk_menu_shell_append(submenu, menuitem);
+ ww = uiNew(struct menuItemWindow);
+ ww->w = w;
+ ww->signal = signal;
+ g_hash_table_insert(item->windows, menuitem, ww);
+}
+
+GtkWidget *makeMenubar(uiWindow *w)
+{
+ GtkWidget *menubar;
+ guint i, j;
+ uiMenu *m;
+ GtkWidget *menuitem;
+ GtkWidget *submenu;
+
+ menusFinalized = TRUE;
+
+ menubar = gtk_menu_bar_new();
+
+ if (menus != NULL)
+ for (i = 0; i < menus->len; i++) {
+ m = g_array_index(menus, uiMenu *, i);
+ menuitem = gtk_menu_item_new_with_label(m->name);
+ submenu = gtk_menu_new();
+ gtk_menu_item_set_submenu(GTK_MENU_ITEM(menuitem), submenu);
+ for (j = 0; j < m->items->len; j++)
+ appendMenuItem(GTK_MENU_SHELL(submenu), g_array_index(m->items, uiMenuItem *, j), w);
+ gtk_menu_shell_append(GTK_MENU_SHELL(menubar), menuitem);
+ }
+
+ gtk_widget_set_hexpand(menubar, TRUE);
+ gtk_widget_set_halign(menubar, GTK_ALIGN_FILL);
+ return menubar;
+}
+
+struct freeMenuItemData {
+ GArray *items;
+ guint i;
+};
+
+static void freeMenuItem(GtkWidget *widget, gpointer data)
+{
+ struct freeMenuItemData *fmi = (struct freeMenuItemData *) data;
+ uiMenuItem *item;
+ struct menuItemWindow *w;
+
+ item = g_array_index(fmi->items, uiMenuItem *, fmi->i);
+ w = (struct menuItemWindow *) g_hash_table_lookup(item->windows, widget);
+ if (g_hash_table_remove(item->windows, widget) == FALSE)
+ implbug("GtkMenuItem %p not in menu item's item/window map", widget);
+ uiFree(w);
+ fmi->i++;
+}
+
+static void freeMenu(GtkWidget *widget, gpointer data)
+{
+ guint *i = (guint *) data;
+ uiMenu *m;
+ GtkMenuItem *item;
+ GtkWidget *submenu;
+ struct freeMenuItemData fmi;
+
+ m = g_array_index(menus, uiMenu *, *i);
+ item = GTK_MENU_ITEM(widget);
+ submenu = gtk_menu_item_get_submenu(item);
+ fmi.items = m->items;
+ fmi.i = 0;
+ gtk_container_foreach(GTK_CONTAINER(submenu), freeMenuItem, &fmi);
+ (*i)++;
+}
+
+void freeMenubar(GtkWidget *mb)
+{
+ guint i;
+
+ i = 0;
+ gtk_container_foreach(GTK_CONTAINER(mb), freeMenu, &i);
+ // no need to worry about destroying any widgets; destruction of the window they're in will do it for us
+}
+
+void uninitMenus(void)
+{
+ uiMenu *m;
+ uiMenuItem *item;
+ guint i, j;
+
+ if (menus == NULL)
+ return;
+ for (i = 0; i < menus->len; i++) {
+ m = g_array_index(menus, uiMenu *, i);
+ g_free(m->name);
+ for (j = 0; j < m->items->len; j++) {
+ item = g_array_index(m->items, uiMenuItem *, j);
+ if (g_hash_table_size(item->windows) != 0)
+ // TODO is this really a userbug()?
+ implbug("menu item %p (%s) still has uiWindows attached; did you forget to destroy some windows?", item, item->name);
+ g_free(item->name);
+ g_hash_table_destroy(item->windows);
+ uiFree(item);
+ }
+ g_array_free(m->items, TRUE);
+ uiFree(m);
+ }
+ g_array_free(menus, TRUE);
+}
diff --git a/src/libui_sdl/libui/unix/multilineentry.c b/src/libui_sdl/libui/unix/multilineentry.c
new file mode 100644
index 0000000..09ffd46
--- /dev/null
+++ b/src/libui_sdl/libui/unix/multilineentry.c
@@ -0,0 +1,124 @@
+// 6 december 2015
+#include "uipriv_unix.h"
+
+struct uiMultilineEntry {
+ uiUnixControl c;
+ GtkWidget *widget;
+ GtkContainer *scontainer;
+ GtkScrolledWindow *sw;
+ GtkWidget *textviewWidget;
+ GtkTextView *textview;
+ GtkTextBuffer *textbuf;
+ void (*onChanged)(uiMultilineEntry *, void *);
+ void *onChangedData;
+ gulong onChangedSignal;
+};
+
+uiUnixControlAllDefaults(uiMultilineEntry)
+
+static void onChanged(GtkTextBuffer *textbuf, gpointer data)
+{
+ uiMultilineEntry *e = uiMultilineEntry(data);
+
+ (*(e->onChanged))(e, e->onChangedData);
+}
+
+static void defaultOnChanged(uiMultilineEntry *e, void *data)
+{
+ // do nothing
+}
+
+char *uiMultilineEntryText(uiMultilineEntry *e)
+{
+ GtkTextIter start, end;
+ char *tret, *out;
+
+ gtk_text_buffer_get_start_iter(e->textbuf, &start);
+ gtk_text_buffer_get_end_iter(e->textbuf, &end);
+ tret = gtk_text_buffer_get_text(e->textbuf, &start, &end, TRUE);
+ // theoretically we could just return tret because uiUnixStrdupText() is just g_strdup(), but if that ever changes we can't, so let's do it this way to be safe
+ out = uiUnixStrdupText(tret);
+ g_free(tret);
+ return out;
+}
+
+void uiMultilineEntrySetText(uiMultilineEntry *e, const char *text)
+{
+ // we need to inhibit sending of ::changed because this WILL send a ::changed otherwise
+ g_signal_handler_block(e->textbuf, e->onChangedSignal);
+ gtk_text_buffer_set_text(e->textbuf, text, -1);
+ g_signal_handler_unblock(e->textbuf, e->onChangedSignal);
+}
+
+// TODO scroll to end?
+void uiMultilineEntryAppend(uiMultilineEntry *e, const char *text)
+{
+ GtkTextIter end;
+
+ gtk_text_buffer_get_end_iter(e->textbuf, &end);
+ // we need to inhibit sending of ::changed because this WILL send a ::changed otherwise
+ g_signal_handler_block(e->textbuf, e->onChangedSignal);
+ gtk_text_buffer_insert(e->textbuf, &end, text, -1);
+ g_signal_handler_unblock(e->textbuf, e->onChangedSignal);
+}
+
+void uiMultilineEntryOnChanged(uiMultilineEntry *e, void (*f)(uiMultilineEntry *e, void *data), void *data)
+{
+ e->onChanged = f;
+ e->onChangedData = data;
+}
+
+int uiMultilineEntryReadOnly(uiMultilineEntry *e)
+{
+ return gtk_text_view_get_editable(e->textview) == FALSE;
+}
+
+void uiMultilineEntrySetReadOnly(uiMultilineEntry *e, int readonly)
+{
+ gboolean editable;
+
+ editable = TRUE;
+ if (readonly)
+ editable = FALSE;
+ gtk_text_view_set_editable(e->textview, editable);
+}
+
+static uiMultilineEntry *finishMultilineEntry(GtkPolicyType hpolicy, GtkWrapMode wrapMode)
+{
+ uiMultilineEntry *e;
+
+ uiUnixNewControl(uiMultilineEntry, e);
+
+ e->widget = gtk_scrolled_window_new(NULL, NULL);
+ e->scontainer = GTK_CONTAINER(e->widget);
+ e->sw = GTK_SCROLLED_WINDOW(e->widget);
+ gtk_scrolled_window_set_policy(e->sw,
+ hpolicy,
+ GTK_POLICY_AUTOMATIC);
+ gtk_scrolled_window_set_shadow_type(e->sw, GTK_SHADOW_IN);
+
+ e->textviewWidget = gtk_text_view_new();
+ e->textview = GTK_TEXT_VIEW(e->textviewWidget);
+ gtk_text_view_set_wrap_mode(e->textview, wrapMode);
+
+ gtk_container_add(e->scontainer, e->textviewWidget);
+ // and make the text view visible; only the scrolled window's visibility is controlled by libui
+ gtk_widget_show(e->textviewWidget);
+
+ e->textbuf = gtk_text_view_get_buffer(e->textview);
+
+ e->onChangedSignal = g_signal_connect(e->textbuf, "changed", G_CALLBACK(onChanged), e);
+ uiMultilineEntryOnChanged(e, defaultOnChanged, NULL);
+
+ return e;
+}
+
+uiMultilineEntry *uiNewMultilineEntry(void)
+{
+ return finishMultilineEntry(GTK_POLICY_NEVER, GTK_WRAP_WORD);
+}
+
+uiMultilineEntry *uiNewNonWrappingMultilineEntry(void)
+{
+ return finishMultilineEntry(GTK_POLICY_AUTOMATIC, GTK_WRAP_NONE);
+}
diff --git a/src/libui_sdl/libui/unix/progressbar.c b/src/libui_sdl/libui/unix/progressbar.c
new file mode 100644
index 0000000..9b543b0
--- /dev/null
+++ b/src/libui_sdl/libui/unix/progressbar.c
@@ -0,0 +1,71 @@
+// 11 june 2015
+#include "uipriv_unix.h"
+
+struct uiProgressBar {
+ uiUnixControl c;
+ GtkWidget *widget;
+ GtkProgressBar *pbar;
+ gboolean indeterminate;
+ guint pulser;
+};
+
+uiUnixControlAllDefaultsExceptDestroy(uiProgressBar)
+
+static void uiProgressBarDestroy(uiControl *c)
+{
+ uiProgressBar *p = uiProgressBar(c);
+
+ // be sure to stop the timeout now
+ if (p->indeterminate)
+ g_source_remove(p->pulser);
+ g_object_unref(p->widget);
+ uiFreeControl(uiControl(p));
+}
+
+int uiProgressBarValue(uiProgressBar *p)
+{
+ if (p->indeterminate)
+ return -1;
+ return (int) (gtk_progress_bar_get_fraction(p->pbar) * 100);
+}
+
+static gboolean pulse(void* data)
+{
+ uiProgressBar *p = uiProgressBar(data);
+
+ gtk_progress_bar_pulse(p->pbar);
+ return TRUE;
+}
+
+void uiProgressBarSetValue(uiProgressBar *p, int value)
+{
+ if (value == -1) {
+ if (!p->indeterminate) {
+ p->indeterminate = TRUE;
+ // TODO verify the timeout
+ p->pulser = g_timeout_add(100, pulse, p);
+ }
+ return;
+ }
+ if (p->indeterminate) {
+ p->indeterminate = FALSE;
+ g_source_remove(p->pulser);
+ }
+
+ if (value < 0 || value > 100)
+ userbug("Value %d is out of range for a uiProgressBar.", value);
+
+ gtk_progress_bar_set_fraction(p->pbar, ((gdouble) value) / 100);
+}
+
+uiProgressBar *uiNewProgressBar(void)
+{
+ uiProgressBar *p;
+
+ uiUnixNewControl(uiProgressBar, p);
+
+ p->widget = gtk_progress_bar_new();
+ p->pbar = GTK_PROGRESS_BAR(p->widget);
+
+ return p;
+}
diff --git a/src/libui_sdl/libui/unix/radiobuttons.c b/src/libui_sdl/libui/unix/radiobuttons.c
new file mode 100644
index 0000000..da41107
--- /dev/null
+++ b/src/libui_sdl/libui/unix/radiobuttons.c
@@ -0,0 +1,121 @@
+// 11 june 2015
+#include "uipriv_unix.h"
+
+// on GTK+ a uiRadioButtons is a GtkBox with each of the GtkRadioButtons as children
+
+struct uiRadioButtons {
+ uiUnixControl c;
+ GtkWidget *widget;
+ GtkContainer *container;
+ GtkBox *box;
+ GPtrArray *buttons;
+ void (*onSelected)(uiRadioButtons *, void *);
+ void *onSelectedData;
+ gboolean changing;
+};
+
+uiUnixControlAllDefaultsExceptDestroy(uiRadioButtons)
+
+static void defaultOnSelected(uiRadioButtons *r, void *data)
+{
+ // do nothing
+}
+
+static void onToggled(GtkToggleButton *tb, gpointer data)
+{
+ uiRadioButtons *r = uiRadioButtons(data);
+
+ // only care if a button is selected
+ if (!gtk_toggle_button_get_active(tb))
+ return;
+ // ignore programmatic changes
+ if (r->changing)
+ return;
+ (*(r->onSelected))(r, r->onSelectedData);
+}
+
+static void uiRadioButtonsDestroy(uiControl *c)
+{
+ uiRadioButtons *r = uiRadioButtons(c);
+ GtkWidget *b;
+
+ while (r->buttons->len != 0) {
+ b = GTK_WIDGET(g_ptr_array_remove_index(r->buttons, 0));
+ gtk_widget_destroy(b);
+ }
+ g_ptr_array_free(r->buttons, TRUE);
+ // and free ourselves
+ g_object_unref(r->widget);
+ uiFreeControl(uiControl(r));
+}
+
+void uiRadioButtonsAppend(uiRadioButtons *r, const char *text)
+{
+ GtkWidget *rb;
+ GtkRadioButton *previous;
+
+ previous = NULL;
+ if (r->buttons->len > 0)
+ previous = GTK_RADIO_BUTTON(g_ptr_array_index(r->buttons, 0));
+ rb = gtk_radio_button_new_with_label_from_widget(previous, text);
+ g_signal_connect(rb, "toggled", G_CALLBACK(onToggled), r);
+ gtk_container_add(r->container, rb);
+ g_ptr_array_add(r->buttons, rb);
+ gtk_widget_show(rb);
+}
+
+int uiRadioButtonsSelected(uiRadioButtons *r)
+{
+ GtkToggleButton *tb;
+ guint i;
+
+ for (i = 0; i < r->buttons->len; i++) {
+ tb = GTK_TOGGLE_BUTTON(g_ptr_array_index(r->buttons, i));
+ if (gtk_toggle_button_get_active(tb))
+ return i;
+ }
+ return -1;
+}
+
+void uiRadioButtonsSetSelected(uiRadioButtons *r, int n)
+{
+ GtkToggleButton *tb;
+ gboolean active;
+
+ active = TRUE;
+ // TODO this doesn't work
+ if (n == -1) {
+ n = uiRadioButtonsSelected(r);
+ if (n == -1) // no selection; keep it that way
+ return;
+ active = FALSE;
+ }
+ tb = GTK_TOGGLE_BUTTON(g_ptr_array_index(r->buttons, n));
+ // this is easier than remembering all the signals
+ r->changing = TRUE;
+ gtk_toggle_button_set_active(tb, active);
+ r->changing = FALSE;
+}
+
+void uiRadioButtonsOnSelected(uiRadioButtons *r, void (*f)(uiRadioButtons *, void *), void *data)
+{
+ r->onSelected = f;
+ r->onSelectedData = data;
+}
+
+uiRadioButtons *uiNewRadioButtons(void)
+{
+ uiRadioButtons *r;
+
+ uiUnixNewControl(uiRadioButtons, r);
+
+ r->widget = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
+ r->container = GTK_CONTAINER(r->widget);
+ r->box = GTK_BOX(r->widget);
+
+ r->buttons = g_ptr_array_new();
+
+ uiRadioButtonsOnSelected(r, defaultOnSelected, NULL);
+
+ return r;
+}
diff --git a/src/libui_sdl/libui/unix/separator.c b/src/libui_sdl/libui/unix/separator.c
new file mode 100644
index 0000000..02c75da
--- /dev/null
+++ b/src/libui_sdl/libui/unix/separator.c
@@ -0,0 +1,34 @@
+// 11 june 2015
+#include "uipriv_unix.h"
+
+struct uiSeparator {
+ uiUnixControl c;
+ GtkWidget *widget;
+ GtkSeparator *separator;
+};
+
+uiUnixControlAllDefaults(uiSeparator)
+
+uiSeparator *uiNewHorizontalSeparator(void)
+{
+ uiSeparator *s;
+
+ uiUnixNewControl(uiSeparator, s);
+
+ s->widget = gtk_separator_new(GTK_ORIENTATION_HORIZONTAL);
+ s->separator = GTK_SEPARATOR(s->widget);
+
+ return s;
+}
+
+uiSeparator *uiNewVerticalSeparator(void)
+{
+ uiSeparator *s;
+
+ uiUnixNewControl(uiSeparator, s);
+
+ s->widget = gtk_separator_new(GTK_ORIENTATION_VERTICAL);
+ s->separator = GTK_SEPARATOR(s->widget);
+
+ return s;
+}
diff --git a/src/libui_sdl/libui/unix/slider.c b/src/libui_sdl/libui/unix/slider.c
new file mode 100644
index 0000000..7f0cc24
--- /dev/null
+++ b/src/libui_sdl/libui/unix/slider.c
@@ -0,0 +1,71 @@
+// 11 june 2015
+#include "uipriv_unix.h"
+
+struct uiSlider {
+ uiUnixControl c;
+ GtkWidget *widget;
+ GtkRange *range;
+ GtkScale *scale;
+ void (*onChanged)(uiSlider *, void *);
+ void *onChangedData;
+ gulong onChangedSignal;
+};
+
+uiUnixControlAllDefaults(uiSlider)
+
+static void onChanged(GtkRange *range, gpointer data)
+{
+ uiSlider *s = uiSlider(data);
+
+ (*(s->onChanged))(s, s->onChangedData);
+}
+
+static void defaultOnChanged(uiSlider *s, void *data)
+{
+ // do nothing
+}
+
+int uiSliderValue(uiSlider *s)
+{
+ return gtk_range_get_value(s->range);
+}
+
+void uiSliderSetValue(uiSlider *s, int value)
+{
+ // we need to inhibit sending of ::value-changed because this WILL send a ::value-changed otherwise
+ g_signal_handler_block(s->range, s->onChangedSignal);
+ gtk_range_set_value(s->range, value);
+ g_signal_handler_unblock(s->range, s->onChangedSignal);
+}
+
+void uiSliderOnChanged(uiSlider *s, void (*f)(uiSlider *, void *), void *data)
+{
+ s->onChanged = f;
+ s->onChangedData = data;
+}
+
+uiSlider *uiNewSlider(int min, int max)
+{
+ uiSlider *s;
+ int temp;
+
+ if (min >= max) {
+ temp = min;
+ min = max;
+ max = temp;
+ }
+
+ uiUnixNewControl(uiSlider, s);
+
+ s->widget = gtk_scale_new_with_range(GTK_ORIENTATION_HORIZONTAL, min, max, 1);
+ s->range = GTK_RANGE(s->widget);
+ s->scale = GTK_SCALE(s->widget);
+
+ // ensure integers, just to be safe
+ gtk_scale_set_digits(s->scale, 0);
+
+ s->onChangedSignal = g_signal_connect(s->scale, "value-changed", G_CALLBACK(onChanged), s);
+ uiSliderOnChanged(s, defaultOnChanged, NULL);
+
+ return s;
+}
diff --git a/src/libui_sdl/libui/unix/spinbox.c b/src/libui_sdl/libui/unix/spinbox.c
new file mode 100644
index 0000000..90a5d3c
--- /dev/null
+++ b/src/libui_sdl/libui/unix/spinbox.c
@@ -0,0 +1,72 @@
+// 11 june 2015
+#include "uipriv_unix.h"
+
+struct uiSpinbox {
+ uiUnixControl c;
+ GtkWidget *widget;
+ GtkEntry *entry;
+ GtkSpinButton *spinButton;
+ void (*onChanged)(uiSpinbox *, void *);
+ void *onChangedData;
+ gulong onChangedSignal;
+};
+
+uiUnixControlAllDefaults(uiSpinbox)
+
+static void onChanged(GtkSpinButton *sb, gpointer data)
+{
+ uiSpinbox *s = uiSpinbox(data);
+
+ (*(s->onChanged))(s, s->onChangedData);
+}
+
+static void defaultOnChanged(uiSpinbox *s, void *data)
+{
+ // do nothing
+}
+
+int uiSpinboxValue(uiSpinbox *s)
+{
+ return gtk_spin_button_get_value(s->spinButton);
+}
+
+void uiSpinboxSetValue(uiSpinbox *s, int value)
+{
+ // we need to inhibit sending of ::value-changed because this WILL send a ::value-changed otherwise
+ g_signal_handler_block(s->spinButton, s->onChangedSignal);
+ // this clamps for us
+ gtk_spin_button_set_value(s->spinButton, (gdouble) value);
+ g_signal_handler_unblock(s->spinButton, s->onChangedSignal);
+}
+
+void uiSpinboxOnChanged(uiSpinbox *s, void (*f)(uiSpinbox *, void *), void *data)
+{
+ s->onChanged = f;
+ s->onChangedData = data;
+}
+
+uiSpinbox *uiNewSpinbox(int min, int max)
+{
+ uiSpinbox *s;
+ int temp;
+
+ if (min >= max) {
+ temp = min;
+ min = max;
+ max = temp;
+ }
+
+ uiUnixNewControl(uiSpinbox, s);
+
+ s->widget = gtk_spin_button_new_with_range(min, max, 1);
+ s->entry = GTK_ENTRY(s->widget);
+ s->spinButton = GTK_SPIN_BUTTON(s->widget);
+
+ // ensure integers, just to be safe
+ gtk_spin_button_set_digits(s->spinButton, 0);
+
+ s->onChangedSignal = g_signal_connect(s->spinButton, "value-changed", G_CALLBACK(onChanged), s);
+ uiSpinboxOnChanged(s, defaultOnChanged, NULL);
+
+ return s;
+}
diff --git a/src/libui_sdl/libui/unix/stddialogs.c b/src/libui_sdl/libui/unix/stddialogs.c
new file mode 100644
index 0000000..93302f7
--- /dev/null
+++ b/src/libui_sdl/libui/unix/stddialogs.c
@@ -0,0 +1,66 @@
+// 26 june 2015
+#include "uipriv_unix.h"
+
+// LONGTERM figure out why, and describe, that this is the desired behavior
+// LONGTERM also point out that font and color buttons also work like this
+
+#define windowWindow(w) (GTK_WINDOW(uiControlHandle(uiControl(w))))
+
+static char *filedialog(GtkWindow *parent, GtkFileChooserAction mode, const gchar *confirm)
+{
+ GtkWidget *fcd;
+ GtkFileChooser *fc;
+ gint response;
+ char *filename;
+
+ fcd = gtk_file_chooser_dialog_new(NULL, parent, mode,
+ "_Cancel", GTK_RESPONSE_CANCEL,
+ confirm, GTK_RESPONSE_ACCEPT,
+ NULL);
+ fc = GTK_FILE_CHOOSER(fcd);
+ gtk_file_chooser_set_local_only(fc, FALSE);
+ gtk_file_chooser_set_select_multiple(fc, FALSE);
+ gtk_file_chooser_set_show_hidden(fc, TRUE);
+ gtk_file_chooser_set_do_overwrite_confirmation(fc, TRUE);
+ gtk_file_chooser_set_create_folders(fc, TRUE);
+ response = gtk_dialog_run(GTK_DIALOG(fcd));
+ if (response != GTK_RESPONSE_ACCEPT) {
+ gtk_widget_destroy(fcd);
+ return NULL;
+ }
+ filename = uiUnixStrdupText(gtk_file_chooser_get_filename(fc));
+ gtk_widget_destroy(fcd);
+ return filename;
+}
+
+char *uiOpenFile(uiWindow *parent)
+{
+ return filedialog(windowWindow(parent), GTK_FILE_CHOOSER_ACTION_OPEN, "_Open");
+}
+
+char *uiSaveFile(uiWindow *parent)
+{
+ return filedialog(windowWindow(parent), GTK_FILE_CHOOSER_ACTION_SAVE, "_Save");
+}
+
+static void msgbox(GtkWindow *parent, const char *title, const char *description, GtkMessageType type, GtkButtonsType buttons)
+{
+ GtkWidget *md;
+
+ md = gtk_message_dialog_new(parent, GTK_DIALOG_MODAL,
+ type, buttons,
+ "%s", title);
+ gtk_message_dialog_format_secondary_text(GTK_MESSAGE_DIALOG(md), "%s", description);
+ gtk_dialog_run(GTK_DIALOG(md));
+ gtk_widget_destroy(md);
+}
+
+void uiMsgBox(uiWindow *parent, const char *title, const char *description)
+{
+ msgbox(windowWindow(parent), title, description, GTK_MESSAGE_OTHER, GTK_BUTTONS_OK);
+}
+
+void uiMsgBoxError(uiWindow *parent, const char *title, const char *description)
+{
+ msgbox(windowWindow(parent), title, description, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK);
+}
diff --git a/src/libui_sdl/libui/unix/tab.c b/src/libui_sdl/libui/unix/tab.c
new file mode 100644
index 0000000..552e0e3
--- /dev/null
+++ b/src/libui_sdl/libui/unix/tab.c
@@ -0,0 +1,97 @@
+// 11 june 2015
+#include "uipriv_unix.h"
+
+struct uiTab {
+ uiUnixControl c;
+
+ GtkWidget *widget;
+ GtkContainer *container;
+ GtkNotebook *notebook;
+
+ GArray *pages; // []*struct child
+};
+
+uiUnixControlAllDefaultsExceptDestroy(uiTab)
+
+static void uiTabDestroy(uiControl *c)
+{
+ uiTab *t = uiTab(c);
+ guint i;
+ struct child *page;
+
+ for (i = 0; i < t->pages->len; i++) {
+ page = g_array_index(t->pages, struct child *, i);
+ childDestroy(page);
+ }
+ g_array_free(t->pages, TRUE);
+ // and free ourselves
+ g_object_unref(t->widget);
+ uiFreeControl(uiControl(t));
+}
+
+void uiTabAppend(uiTab *t, const char *name, uiControl *child)
+{
+ uiTabInsertAt(t, name, t->pages->len, child);
+}
+
+void uiTabInsertAt(uiTab *t, const char *name, int n, uiControl *child)
+{
+ struct child *page;
+
+ // this will create a tab, because of gtk_container_add()
+ page = newChildWithBox(child, uiControl(t), t->container, 0);
+
+ gtk_notebook_set_tab_label_text(t->notebook, childBox(page), name);
+ gtk_notebook_reorder_child(t->notebook, childBox(page), n);
+
+ g_array_insert_val(t->pages, n, page);
+}
+
+void uiTabDelete(uiTab *t, int n)
+{
+ struct child *page;
+
+ page = g_array_index(t->pages, struct child *, n);
+ // this will remove the tab, because gtk_widget_destroy() calls gtk_container_remove()
+ childRemove(page);
+ g_array_remove_index(t->pages, n);
+}
+
+int uiTabNumPages(uiTab *t)
+{
+ return t->pages->len;
+}
+
+int uiTabMargined(uiTab *t, int n)
+{
+ struct child *page;
+
+ page = g_array_index(t->pages, struct child *, n);
+ return childFlag(page);
+}
+
+void uiTabSetMargined(uiTab *t, int n, int margined)
+{
+ struct child *page;
+
+ page = g_array_index(t->pages, struct child *, n);
+ childSetFlag(page, margined);
+ childSetMargined(page, childFlag(page));
+}
+
+uiTab *uiNewTab(void)
+{
+ uiTab *t;
+
+ uiUnixNewControl(uiTab, t);
+
+ t->widget = gtk_notebook_new();
+ t->container = GTK_CONTAINER(t->widget);
+ t->notebook = GTK_NOTEBOOK(t->widget);
+
+ gtk_notebook_set_scrollable(t->notebook, TRUE);
+
+ t->pages = g_array_new(FALSE, TRUE, sizeof (struct child *));
+
+ return t;
+}
diff --git a/src/libui_sdl/libui/unix/text.c b/src/libui_sdl/libui/unix/text.c
new file mode 100644
index 0000000..ad92738
--- /dev/null
+++ b/src/libui_sdl/libui/unix/text.c
@@ -0,0 +1,12 @@
+// 9 april 2015
+#include "uipriv_unix.h"
+
+char *uiUnixStrdupText(const char *t)
+{
+ return g_strdup(t);
+}
+
+void uiFreeText(char *t)
+{
+ g_free(t);
+}
diff --git a/src/libui_sdl/libui/unix/uipriv_unix.h b/src/libui_sdl/libui/unix/uipriv_unix.h
new file mode 100644
index 0000000..33ff1e3
--- /dev/null
+++ b/src/libui_sdl/libui/unix/uipriv_unix.h
@@ -0,0 +1,65 @@
+// 22 april 2015
+#define GLIB_VERSION_MIN_REQUIRED GLIB_VERSION_2_40
+#define GLIB_VERSION_MAX_ALLOWED GLIB_VERSION_2_40
+#define GDK_VERSION_MIN_REQUIRED GDK_VERSION_3_10
+#define GDK_VERSION_MAX_ALLOWED GDK_VERSION_3_10
+#include <gtk/gtk.h>
+#include <math.h>
+#include <dlfcn.h> // see drawtext.c
+#include <langinfo.h>
+#include <string.h>
+#include <stdlib.h>
+#include "../ui.h"
+#include "../ui_unix.h"
+#include "../common/uipriv.h"
+
+#define gtkXMargin 12
+#define gtkYMargin 12
+#define gtkXPadding 12
+#define gtkYPadding 6
+
+// menu.c
+extern GtkWidget *makeMenubar(uiWindow *);
+extern void freeMenubar(GtkWidget *);
+extern void uninitMenus(void);
+
+// alloc.c
+extern void initAlloc(void);
+extern void uninitAlloc(void);
+
+// util.c
+extern void setMargined(GtkContainer *, int);
+
+// child.c
+extern struct child *newChild(uiControl *child, uiControl *parent, GtkContainer *parentContainer);
+extern struct child *newChildWithBox(uiControl *child, uiControl *parent, GtkContainer *parentContainer, int margined);
+extern void childRemove(struct child *c);
+extern void childDestroy(struct child *c);
+extern GtkWidget *childWidget(struct child *c);
+extern int childFlag(struct child *c);
+extern void childSetFlag(struct child *c, int flag);
+extern GtkWidget *childBox(struct child *c);
+extern void childSetMargined(struct child *c, int margined);
+
+// draw.c
+extern uiDrawContext *newContext(cairo_t *);
+extern void freeContext(uiDrawContext *);
+
+// drawtext.c
+extern uiDrawTextFont *mkTextFont(PangoFont *f, gboolean add);
+extern PangoFont *pangoDescToPangoFont(PangoFontDescription *pdesc);
+
+// graphemes.c
+extern ptrdiff_t *graphemes(const char *text, PangoContext *context);
+
+// image.c
+/*TODO remove this*/typedef struct uiImage uiImage;
+extern cairo_surface_t *imageAppropriateSurface(uiImage *i, GtkWidget *w);
+
+// cellrendererbutton.c
+extern GtkCellRenderer *newCellRendererButton(void);
+
+// future.c
+extern void loadFutures(void);
+extern PangoAttribute *FUTURE_pango_attr_foreground_alpha_new(guint16 alpha);
+extern gboolean FUTURE_gtk_widget_path_iter_set_object_name(GtkWidgetPath *path, gint pos, const char *name);
diff --git a/src/libui_sdl/libui/unix/util.c b/src/libui_sdl/libui/unix/util.c
new file mode 100644
index 0000000..7f4f43f
--- /dev/null
+++ b/src/libui_sdl/libui/unix/util.c
@@ -0,0 +1,10 @@
+// 18 april 2015
+#include "uipriv_unix.h"
+
+void setMargined(GtkContainer *c, int margined)
+{
+ if (margined)
+ gtk_container_set_border_width(c, gtkXMargin);
+ else
+ gtk_container_set_border_width(c, 0);
+}
diff --git a/src/libui_sdl/libui/unix/window.c b/src/libui_sdl/libui/unix/window.c
new file mode 100644
index 0000000..ea9ba37
--- /dev/null
+++ b/src/libui_sdl/libui/unix/window.c
@@ -0,0 +1,279 @@
+// 11 june 2015
+#include "uipriv_unix.h"
+
+struct uiWindow {
+ uiUnixControl c;
+
+ GtkWidget *widget;
+ GtkContainer *container;
+ GtkWindow *window;
+
+ GtkWidget *vboxWidget;
+ GtkContainer *vboxContainer;
+ GtkBox *vbox;
+
+ GtkWidget *childHolderWidget;
+ GtkContainer *childHolderContainer;
+
+ GtkWidget *menubar;
+
+ uiControl *child;
+ int margined;
+
+ int (*onClosing)(uiWindow *, void *);
+ void *onClosingData;
+ void (*onContentSizeChanged)(uiWindow *, void *);
+ void *onContentSizeChangedData;
+ gboolean fullscreen;
+};
+
+static gboolean onClosing(GtkWidget *win, GdkEvent *e, gpointer data)
+{
+ uiWindow *w = uiWindow(data);
+
+ // manually destroy the window ourselves; don't let the delete-event handler do it
+ if ((*(w->onClosing))(w, w->onClosingData))
+ uiControlDestroy(uiControl(w));
+ // don't continue to the default delete-event handler; we destroyed the window by now
+ return TRUE;
+}
+
+static void onSizeAllocate(GtkWidget *widget, GdkRectangle *allocation, gpointer data)
+{
+ uiWindow *w = uiWindow(data);
+
+ // TODO deal with spurious size-allocates
+ (*(w->onContentSizeChanged))(w, w->onContentSizeChangedData);
+}
+
+static int defaultOnClosing(uiWindow *w, void *data)
+{
+ return 0;
+}
+
+static void defaultOnPositionContentSizeChanged(uiWindow *w, void *data)
+{
+ // do nothing
+}
+
+static void uiWindowDestroy(uiControl *c)
+{
+ uiWindow *w = uiWindow(c);
+
+ // first hide ourselves
+ gtk_widget_hide(w->widget);
+ // now destroy the child
+ if (w->child != NULL) {
+ uiControlSetParent(w->child, NULL);
+ uiUnixControlSetContainer(uiUnixControl(w->child), w->childHolderContainer, TRUE);
+ uiControlDestroy(w->child);
+ }
+ // now destroy the menus, if any
+ if (w->menubar != NULL)
+ freeMenubar(w->menubar);
+ gtk_widget_destroy(w->childHolderWidget);
+ gtk_widget_destroy(w->vboxWidget);
+ // and finally free ourselves
+ // use gtk_widget_destroy() instead of g_object_unref() because GTK+ has internal references (see #165)
+ gtk_widget_destroy(w->widget);
+ uiFreeControl(uiControl(w));
+}
+
+uiUnixControlDefaultHandle(uiWindow)
+
+uiControl *uiWindowParent(uiControl *c)
+{
+ return NULL;
+}
+
+void uiWindowSetParent(uiControl *c, uiControl *parent)
+{
+ uiUserBugCannotSetParentOnToplevel("uiWindow");
+}
+
+static int uiWindowToplevel(uiControl *c)
+{
+ return 1;
+}
+
+uiUnixControlDefaultVisible(uiWindow)
+
+static void uiWindowShow(uiControl *c)
+{
+ uiWindow *w = uiWindow(c);
+
+ // don't use gtk_widget_show_all() as that will show all children, regardless of user settings
+ // don't use gtk_widget_show(); that doesn't bring to front or give keyboard focus
+ // (gtk_window_present() does call gtk_widget_show() though)
+ gtk_window_present(w->window);
+}
+
+uiUnixControlDefaultHide(uiWindow)
+uiUnixControlDefaultEnabled(uiWindow)
+uiUnixControlDefaultEnable(uiWindow)
+uiUnixControlDefaultDisable(uiWindow)
+// TODO?
+uiUnixControlDefaultSetContainer(uiWindow)
+
+char *uiWindowTitle(uiWindow *w)
+{
+ return uiUnixStrdupText(gtk_window_get_title(w->window));
+}
+
+void uiWindowSetTitle(uiWindow *w, const char *title)
+{
+ gtk_window_set_title(w->window, title);
+}
+
+void uiWindowContentSize(uiWindow *w, int *width, int *height)
+{
+ GtkAllocation allocation;
+
+ gtk_widget_get_allocation(w->childHolderWidget, &allocation);
+ *width = allocation.width;
+ *height = allocation.height;
+}
+
+void uiWindowSetContentSize(uiWindow *w, int width, int height)
+{
+ GtkAllocation childAlloc;
+ gint winWidth, winHeight;
+
+ // we need to resize the child holder widget to the given size
+ // we can't resize that without running the event loop
+ // but we can do gtk_window_set_size()
+ // so how do we deal with the differences in sizes?
+ // simple arithmetic, of course!
+
+ // from what I can tell, the return from gtk_widget_get_allocation(w->window) and gtk_window_get_size(w->window) will be the same
+ // this is not affected by Wayland and not affected by GTK+ builtin CSD
+ // so we can safely juse use them to get the real window size!
+ // since we're using gtk_window_resize(), use the latter
+ gtk_window_get_size(w->window, &winWidth, &winHeight);
+
+ // now get the child holder widget's current allocation
+ gtk_widget_get_allocation(w->childHolderWidget, &childAlloc);
+ // and punch that out of the window size
+ winWidth -= childAlloc.width;
+ winHeight -= childAlloc.height;
+
+ // now we just need to add the new size back in
+ winWidth += width;
+ winHeight += height;
+ // and set it
+ // this will not move the window in my tests, so we're good
+ gtk_window_resize(w->window, winWidth, winHeight);
+}
+
+int uiWindowFullscreen(uiWindow *w)
+{
+ return w->fullscreen;
+}
+
+// TODO use window-state-event to track
+// TODO does this send an extra size changed?
+// TODO what behavior do we want?
+void uiWindowSetFullscreen(uiWindow *w, int fullscreen)
+{
+ w->fullscreen = fullscreen;
+ if (w->fullscreen)
+ gtk_window_fullscreen(w->window);
+ else
+ gtk_window_unfullscreen(w->window);
+}
+
+void uiWindowOnContentSizeChanged(uiWindow *w, void (*f)(uiWindow *, void *), void *data)
+{
+ w->onContentSizeChanged = f;
+ w->onContentSizeChangedData = data;
+}
+
+void uiWindowOnClosing(uiWindow *w, int (*f)(uiWindow *, void *), void *data)
+{
+ w->onClosing = f;
+ w->onClosingData = data;
+}
+
+int uiWindowBorderless(uiWindow *w)
+{
+ return gtk_window_get_decorated(w->window) == FALSE;
+}
+
+void uiWindowSetBorderless(uiWindow *w, int borderless)
+{
+ gtk_window_set_decorated(w->window, borderless == 0);
+}
+
+// TODO save and restore expands and aligns
+void uiWindowSetChild(uiWindow *w, uiControl *child)
+{
+ if (w->child != NULL) {
+ uiControlSetParent(w->child, NULL);
+ uiUnixControlSetContainer(uiUnixControl(w->child), w->childHolderContainer, TRUE);
+ }
+ w->child = child;
+ if (w->child != NULL) {
+ uiControlSetParent(w->child, uiControl(w));
+ uiUnixControlSetContainer(uiUnixControl(w->child), w->childHolderContainer, FALSE);
+ }
+}
+
+int uiWindowMargined(uiWindow *w)
+{
+ return w->margined;
+}
+
+void uiWindowSetMargined(uiWindow *w, int margined)
+{
+ w->margined = margined;
+ setMargined(w->childHolderContainer, w->margined);
+}
+
+uiWindow *uiNewWindow(const char *title, int width, int height, int hasMenubar)
+{
+ uiWindow *w;
+
+ uiUnixNewControl(uiWindow, w);
+
+ w->widget = gtk_window_new(GTK_WINDOW_TOPLEVEL);
+ w->container = GTK_CONTAINER(w->widget);
+ w->window = GTK_WINDOW(w->widget);
+
+ gtk_window_set_title(w->window, title);
+ gtk_window_resize(w->window, width, height);
+
+ w->vboxWidget = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
+ w->vboxContainer = GTK_CONTAINER(w->vboxWidget);
+ w->vbox = GTK_BOX(w->vboxWidget);
+
+ // set the vbox as the GtkWindow child
+ gtk_container_add(w->container, w->vboxWidget);
+
+ if (hasMenubar) {
+ w->menubar = makeMenubar(uiWindow(w));
+ gtk_container_add(w->vboxContainer, w->menubar);
+ }
+
+ w->childHolderWidget = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
+ w->childHolderContainer = GTK_CONTAINER(w->childHolderWidget);
+ gtk_widget_set_hexpand(w->childHolderWidget, TRUE);
+ gtk_widget_set_halign(w->childHolderWidget, GTK_ALIGN_FILL);
+ gtk_widget_set_vexpand(w->childHolderWidget, TRUE);
+ gtk_widget_set_valign(w->childHolderWidget, GTK_ALIGN_FILL);
+ gtk_container_add(w->vboxContainer, w->childHolderWidget);
+
+ // show everything in the vbox, but not the GtkWindow itself
+ gtk_widget_show_all(w->vboxWidget);
+
+ // and connect our events
+ g_signal_connect(w->widget, "delete-event", G_CALLBACK(onClosing), w);
+ g_signal_connect(w->childHolderWidget, "size-allocate", G_CALLBACK(onSizeAllocate), w);
+ uiWindowOnClosing(w, defaultOnClosing, NULL);
+ uiWindowOnContentSizeChanged(w, defaultOnPositionContentSizeChanged, NULL);
+
+ // normally it's SetParent() that does this, but we can't call SetParent() on a uiWindow
+ // TODO we really need to clean this up, especially since see uiWindowDestroy() above
+ g_object_ref(w->widget);
+
+ return w;
+}