Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
217 changes: 183 additions & 34 deletions .github/workflows/ci.yml

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ out-test/
*.d
*.so
__pycache__/
externals/violawww/
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ include mk/libxpm.mk
include mk/xcompat-libs.mk
include mk/pkgconfig.mk
include mk/motif.mk
include mk/violawww.mk
include mk/tests.mk
include mk/examples.mk
include mk/upstream-headers.mk
Expand Down
37 changes: 36 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ Clients link against it the same way they would link against the system `libX11.

## Examples

![ViolaWWW running through libx11-compat on macOS](assets/violawww.png)

`examples/` bundles real Xlib clients built against the local `libX11-compat.so`:

```sh
Expand All @@ -38,10 +40,43 @@ build/examples/2048

The bundle covers a 2048 game, a paint demo, Conway's Game of Life, an analog clock, an interactive Mandelbrot viewer, a single-runner Processing-style showcase, an SDL-backed clipboard `TARGETS` probe, and the upstream X.Org `x11perf` benchmark.
See [`docs/EXAMPLES.md`](docs/EXAMPLES.md) for the API each example exercises.
The screenshot above is from the larger ViolaWWW port described in [Larger Workloads Under Investigation](#larger-workloads-under-investigation).

## Larger Workloads Under Investigation

Two ports beyond the bundled demos are actively exercising `libx11-compat`.
Both are work-in-progress rather than ready for daily use,
but each surfaces gaps in coverage and edge cases that small examples never reach.

- [Motif](https://en.wikipedia.org/wiki/Motif_(software)): upstream Motif and its demo suite build against the compatibility libraries.
Menu posting, pointer grabs, focus changes, and text rendering all work in the cases tested so far,
and the screenshot-based differential harness flags regressions against native X11/Motif.
Some widget paths still trigger fallback or layout artifacts that require further work.
- [ViolaWWW](https://en.wikipedia.org/wiki/ViolaWWW): the 1992-era Motif web browser builds and runs out of the consolidated `build/` tree,
loads HTTP pages over the network,
and renders inline XPM images through `libXpm-compat`.
HTTPS, complex modern HTML, and several interactive flows are known limitations of the application itself rather than the compatibility layer.

```sh
make violawww # build ViolaWWW (depends on motif)
build/violawww/source/src/vw/vw # launch the browser
make check-smoke-violawww # replay-driven smoke checks (scroll + Help menu)
make check-differential-violawww # screenshot diff vs system libX11 (needs remote host)
```

The `check-*-smoke` targets need an X display, or `UI_REPLAY_XVFB=--xvfb` to drive an in-process Xvfb;
on macOS the browser launches against the host SDL backend without extra setup.

The value of these targets is not that they replace a real X11/Motif install today,
but that they keep legacy Xlib/Motif code building and running on platforms where no X server is available while migration is in progress.

Both ports rely on community input.
Concrete reproducers, screenshot diffs, and small fixes posted to [GitHub Issues](https://github.com/sysprog21/libx11-compat/issues) are the most effective way to push these workloads forward.
Specific gaps worth opening issues for include widget paths that misrender, Motif demos that crash or differ visibly from the native baseline, and ViolaWWW interactions (navigation, dialogs, image formats) that do not behave as the historical browser did.

## Coverage and Compatibility

The library exports 615 public Xlib symbols listed in [`tests/api-symbols.txt`](tests/api-symbols.txt),
The library exports 631 public Xlib symbols listed in [`tests/api-symbols.txt`](tests/api-symbols.txt),
covering window, drawable, GC, pixmap, image, event, input, atom, property, color, font, cursor, and region subsystems for the cases that real Xlib clients exercise.
Selection, property, and resource-manager support is partial;
MIT-SHM is a thin wrapper over the regular image path;
Expand Down
Binary file added assets/violawww.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
44 changes: 44 additions & 0 deletions compat/motif-patches/asan-safe-string-and-printer-lists.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
diff --git a/demos/lib/Xmd/Print.c b/demos/lib/Xmd/Print.c
--- a/demos/lib/Xmd/Print.c
+++ b/demos/lib/Xmd/Print.c
@@ -604,11 +604,11 @@ process_printer_list(Widget w)
/* Terminate with 0 */
buf[count++] = 0;
/* Make sure printer lists are big enough */
pw -> print.printers = (char**) XtRealloc((char*) pw -> print.printers,
- sizeof(char*) *
- pw -> print.num_printers + 1);
+ sizeof(char*) *
+ (pw -> print.num_printers + 1));
pw -> print.is_printer =
(Boolean*) XtRealloc((char*) pw -> print.is_printer,
- sizeof(Boolean) * pw -> print.num_printers + 1);
+ sizeof(Boolean) * (pw -> print.num_printers + 1));
pw -> print.printers[pw -> print.num_printers] = XtNewString(buf);
pw -> print.is_printer[pw -> print.num_printers] = ! is_path;
pw -> print.num_printers++;
diff --git a/lib/Xm/XmExtUtil.c b/lib/Xm/XmExtUtil.c
--- a/lib/Xm/XmExtUtil.c
+++ b/lib/Xm/XmExtUtil.c
@@ -349,7 +349,7 @@ _XmGetMBStringFromXmString(XmString xmstr)
{
case XmSTRING_COMPONENT_TEXT:
case XmSTRING_COMPONENT_LOCALE_TEXT:
- length += strlen( newText );
+ length += u_length;
break;
case XmSTRING_COMPONENT_SEPARATOR:
length += 1;
@@ -396,7 +396,11 @@ _XmGetMBStringFromXmString(XmString xmstr)
{
case XmSTRING_COMPONENT_TEXT:
case XmSTRING_COMPONENT_LOCALE_TEXT:
- strcat(text, newText);
+ {
+ size_t text_len = strlen(text);
+ memcpy(text + text_len, newText, u_length);
+ text[text_len + u_length] = '\0';
+ }
break;
case XmSTRING_COMPONENT_SEPARATOR:
strcat(text, "\n");
117 changes: 117 additions & 0 deletions compat/motif-patches/silence-iconv-cascade-warnings.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
diff --git a/lib/Xm/XmString.c b/lib/Xm/XmString.c
index aed38e37..5a03e807 100644
--- a/lib/Xm/XmString.c
+++ b/lib/Xm/XmString.c
@@ -312,6 +312,14 @@ static int _get_generate_parse_table(XmParseTable *gen_table);
static const char *WCHAR_T = "WCHAR_T";
static const XmStringTag TO_UTF8 = "UTF-8";

+/* Quiet variant of _Xmcsconv: returns NULL on iconv_open failure without
+ * emitting XmeWarning. Used by the XmStringParse cascade (tag → locale.tag
+ * → XmFALLBACK_CHARSET) where each non-final step is expected to fail and
+ * only the final aggregated failure warrants a user-visible warning. */
+static char *_Xmcsconv_silent(const char *from, const char *to,
+ char *text, size_t bytes, size_t *len_out);
+
+
static struct __Xmlocale locale;
static XmStringTag *_tag_cache = NULL;
static size_t _cache_count = 0;
@@ -404,6 +412,86 @@ again:
return result;
}

+/* See declaration comment near the top of this file. */
+static char *_Xmcsconv_silent(const char *from, const char *to,
+ char *text, size_t bytes, size_t *len_out)
+{
+ iconv_t ic;
+ char *inbuf, *outbuf, *result;
+ size_t insz, outsz, used, conv;
+ char msg[256];
+#if defined(_LIBICONV_VERSION)
+ char *_to, *_from;
+#endif
+
+ if (from == XmFONTLIST_DEFAULT_TAG || (from && !strcmp(from, XmFONTLIST_DEFAULT_TAG)))
+ from = locale.tag;
+
+#if defined(_LIBICONV_VERSION)
+ _to = XtMalloc(strlen(to) + 9);
+ _from = XtMalloc(strlen(from) + 9);
+ sprintf(_to, "%s//IGNORE", to);
+ sprintf(_from, "%s//IGNORE", from);
+ to = _to;
+ from = _from;
+#endif
+
+ if ((ic = iconv_open(to, from)) == (iconv_t)-1) {
+ /* Silent: caller will cascade or warn aggregate. */
+ *len_out = 0;
+#if defined(_LIBICONV_VERSION)
+ XtFree(_to);
+ XtFree(_from);
+#endif
+ return NULL;
+ }
+
+ insz = bytes;
+ inbuf = text;
+ outsz = 1 + (bytes << 2);
+ outbuf = XtMalloc(outsz);
+ result = outbuf;
+
+silent_again:
+ if ((conv = iconv(ic, &inbuf, &insz, &outbuf, &outsz)) == (size_t)-1) {
+ switch (errno) {
+ case EINVAL:
+ break;
+ case E2BIG:
+ insz = bytes;
+ inbuf = text;
+ used = (size_t)(outbuf - result);
+ outsz += 2 + (outsz >> 2);
+ result = XtRealloc(result, outsz);
+ outbuf = result + used;
+ goto silent_again;
+ case EILSEQ:
+ XmeWarning(NULL, "Invalid byte sequence in character set conversion input");
+ break;
+ default:
+ snprintf(msg, sizeof msg, "Error during charset conversion: %s", strerror(errno));
+ XmeWarning(NULL, msg);
+ }
+
+ iconv_close(ic);
+ XtFree(result);
+#if defined(_LIBICONV_VERSION)
+ XtFree(_to);
+ XtFree(_from);
+#endif
+ if (len_out) *len_out = 0;
+ return NULL;
+ }
+
+ iconv_close(ic);
+#if defined(_LIBICONV_VERSION)
+ XtFree(_to);
+ XtFree(_from);
+#endif
+ if (len_out) *len_out = (size_t)(outbuf - result);
+ return result;
+}
+
/**
* Create a new XmString
*/
@@ -5876,8 +5964,8 @@ static void parse_unmatched(XmString *result, char **ptr,
* So, if we can't convert based on the tag, fallback to the locale
* charset and further back to the fallback charset (Latin 1).
*/
- if (!(out = _Xmcsconv(tag, TO_UTF8, *ptr, length, &convsz))) {
- if (!(out = _Xmcsconv(locale.tag, TO_UTF8, *ptr, length, &convsz)))
+ if (!(out = _Xmcsconv_silent(tag, TO_UTF8, *ptr, length, &convsz))) {
+ if (!(out = _Xmcsconv_silent(locale.tag, TO_UTF8, *ptr, length, &convsz)))
out = _Xmcsconv(XmFALLBACK_CHARSET, TO_UTF8, *ptr, length, &convsz);
}

16 changes: 16 additions & 0 deletions compat/violawww-patches/clear-before-shown-position-redraw.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
--- a/src/viola/cl_txtDisp.c
+++ b/src/viola/cl_txtDisp.c
@@ -1090,6 +1090,13 @@ int help_txtDisp_shownPositionV(VObj* self, int newPosition)
return newPosition;
/* textFieldJumpToLine(self, destLine);*/
GLPrepareObjColor(self);
+ /*
+ * Wheel scrolling and Motif scrollbar dragging both enter through
+ * shownPositionV. Clear the backing window before jump/redraw so stale
+ * glyphs from the old viewport are not left under the new text frame.
+ */
+ if (GET_window(self))
+ GLClearWindow(GET_window(self));
tfed_jumpToOffsetLine(self, destLine);

SET_shownPositionV(self, newPosition);
59 changes: 59 additions & 0 deletions compat/violawww-patches/clear-canvas-before-scroll-position.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
--- a/src/vw/callbacks.c
+++ b/src/vw/callbacks.c
@@ -816,6 +816,17 @@
(positionPercent * maximum / SBAR_MAGNITUDE), NULL);
}

+static void clearDocViewBeforeScroll(DocViewInfo* dvi, VObj* violaObj) {
+ if (dvi && dvi->canvas && XtIsRealized(dvi->canvas)) {
+ XClearArea(XtDisplay(dvi->canvas), XtWindow(dvi->canvas), 0, 0, 0, 0, False);
+ }
+ if (violaObj && GET_window(violaObj)) {
+ XClearArea(display, GET_window(violaObj), 0, 0, 0, 0, False);
+ } else if (dvi && dvi->violaDocViewWindow) {
+ XClearArea(display, dvi->violaDocViewWindow, 0, 0, 0, 0, False);
+ }
+}
+
void scrollBarDrag(Widget sbar, XtPointer clientData, XtPointer callData) {
XmScrollBarCallbackStruct* sbData = (XmScrollBarCallbackStruct*)callData;
VObj* violaObj = (VObj*)clientData;
@@ -826,8 +837,11 @@
XtVaGetValues(sbar, XmNsliderSize, &sliderSize, XmNmaximum, &maximum, NULL);
percent = SBAR_MAGNITUDE * sbData->value / (maximum - sliderSize);

- if (violaObj)
+ if (violaObj) {
+ clearDocViewBeforeScroll(mainViewInfo(), violaObj);
sendTokenMessageN1int(violaObj, STR_shownPositionV, percent);
+ sendMessage1(violaObj, "render");
+ }

calledDrag++;
}
@@ -879,8 +893,11 @@

XtVaGetValues(sbar, XmNsliderSize, &sliderSize, XmNmaximum, &maximum, NULL);
percent = SBAR_MAGNITUDE * sbData->value / (maximum - sliderSize);
- if (violaObj)
+ if (violaObj) {
+ clearDocViewBeforeScroll(mainViewInfo(), violaObj);
sendTokenMessageN1int(violaObj, STR_shownPositionV, percent);
+ sendMessage1(violaObj, "render");
+ }
}
}

@@ -940,10 +957,12 @@
}

/* Notify Viola about the position change */
+ clearDocViewBeforeScroll(dvi, dvi->violaDocViewObj);
sendTokenMessageN1int(dvi->violaDocViewObj, STR_shownPositionV, percent);

/* Update scrollbar position */
XtVaSetValues(dvi->scrollBar, XmNvalue, newValue, NULL);
+ sendMessage1(dvi->violaDocViewObj, "render");
}

/*
25 changes: 25 additions & 0 deletions compat/violawww-patches/clear-text-row-before-draw.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
diff --git a/src/viola/tfed.c b/src/viola/tfed.c
--- a/src/viola/tfed.c
+++ b/src/viola/tfed.c
@@ -4984,15 +4984,20 @@
short prevFontID, prevVideo;
char* buffp;
int flags;
GC usegc = gc_fg;
int localYOffset = *yoffset;

+ if (tf->isRenderAble) {
+ XClearArea(display, w, tf->xUL, localYOffset, tf->width,
+ currentp->maxFontHeight * currentp->breakc, False);
+ }
+
if (tf->align == PANE_CONFIG_CENTER) {
segpx = (tf->xLR - tf->xUL - lineSegWidth(tf, currentp)) / 2;
} else if (tf->align == PANE_CONFIG_E2W) {
segpx = tf->xLR - tf->xUL - lineSegWidth(tf, currentp);
} else {
segpx = tf->xUL;
}

item.delta = 0;
item.chars = buff;
24 changes: 24 additions & 0 deletions compat/violawww-patches/clear-viola-target-after-resize.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
--- a/src/vw/callbacks.c
+++ b/src/vw/callbacks.c
@@ -1077,7 +1077,20 @@
if (event->type == ConfigureNotify) {
XConfigureEvent* xcep = (XConfigureEvent*)event;

- XResizeWindow(XtDisplay(widget), (Window)clientData, (unsigned int)xcep->width, (unsigned int)xcep->height);
+ Display* dpy = XtDisplay(widget);
+ Window target = (Window)clientData;
+
+ XResizeWindow(dpy, target, (unsigned int)xcep->width, (unsigned int)xcep->height);
+ XClearArea(dpy, target, 0, 0, 0, 0, True);
+ DocViewInfo* dvi = mainViewInfo();
+ if (dvi && dvi->violaDocViewObj &&
+ (GET_window(dvi->violaDocViewObj) == target ||
+ dvi->violaDocViewWindow == target)) {
+ clearDocViewBeforeScroll(dvi, dvi->violaDocViewObj);
+ sendMessage1N4int(dvi->violaDocViewObj, "config", -1, -1,
+ xcep->width, xcep->height);
+ sendMessage1(dvi->violaDocViewObj, "render");
+ }
}
}

10 changes: 10 additions & 0 deletions compat/violawww-patches/fill-text-window-before-render.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
--- a/src/viola/tfed.c
+++ b/src/viola/tfed.c
@@ -5522,6 +5522,7 @@ int renderTF(TFStruct* tf)
GLPrepareObjColor(tf->self);

XClearWindow(display, TFWINDOW);
+ XFillRectangle(display, TFWINDOW, gc_bg, 0, 0, tf->width, tf->height);

if (BDPixel == BGPixel) {
XSetForeground(display, gc_mesh, FGPixel);
18 changes: 18 additions & 0 deletions compat/violawww-patches/full-redraw-on-scroll-delta.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
--- a/src/viola/tfed.c
+++ b/src/viola/tfed.c
@@ -6019,8 +6019,15 @@

if (offsetdir == 0)
return 0;
- else if (offsetdir > 0)
+
+ /*
+ * The copy-scroll optimization leaves stale glyphs on backends where
+ * transparent text redraws are not preceded by a full clear. The
+ * caller has already moved offsetp/currentp; redraw from the model.
+ */
+ return renderTF(tf);
+ if (offsetdir > 0)
linesToMove = offsetdir;
else
linesToMove = -offsetdir;
Loading
Loading