From 5c38e033012284d09a0d02caa8f8df5b82ea1908 Mon Sep 17 00:00:00 2001 From: "Adam D. Ruppe" Date: Mon, 2 Nov 2020 12:13:04 -0500 Subject: [PATCH] new declarative ui started --- cgi.d | 115 ++++++---- http2.d | 4 + minigui.d | 541 ++++++++++++++++++++++++++++++++++++++++++++- terminalemulator.d | 11 +- 4 files changed, 619 insertions(+), 52 deletions(-) diff --git a/cgi.d b/cgi.d index 1afd32b..5d50210 100644 --- a/cgi.d +++ b/cgi.d @@ -3336,7 +3336,7 @@ struct RequestServer { } /++ - Serves a single request on this thread, with an embedded server, then stops. Designed for cases like embedded oauth responders + Serves a single HTTP request on this thread, with an embedded server, then stops. Designed for cases like embedded oauth responders History: Added Oct 10, 2020. @@ -3348,7 +3348,7 @@ struct RequestServer { RequestServer server = RequestServer("127.0.0.1", 6789); string oauthCode; string oauthScope; - server.serveOnce!((cgi) { + server.serveHttpOnce!((cgi) { oauthCode = cgi.request("code"); oauthScope = cgi.request("scope"); cgi.write("Thank you, please return to the application."); @@ -3357,7 +3357,7 @@ struct RequestServer { } --- +/ - void serveOnce(alias fun, CustomCgi = Cgi, long maxContentLength = defaultMaxContentLength)() { + void serveHttpOnce(alias fun, CustomCgi = Cgi, long maxContentLength = defaultMaxContentLength)() { import std.socket; bool tcp; @@ -3387,12 +3387,10 @@ struct RequestServer { serveEmbeddedHttpdProcesses!(fun, CustomCgi)(this); } else version(embedded_httpd_threads) { - auto manager = new ListeningConnectionManager(listeningHost, listeningPort, &doThreadHttpConnection!(CustomCgi, fun)); - manager.listen(); + serveEmbeddedHttp!(fun, CustomCgi, maxContentLength)(); } else version(scgi) { - auto manager = new ListeningConnectionManager(listeningHost, listeningPort, &doThreadScgiConnection!(CustomCgi, fun, maxContentLength)); - manager.listen(); + serveScgi!(fun, CustomCgi, maxContentLength)(); } else version(fastcgi) { serveFastCgi!(fun, CustomCgi, maxContentLength)(this); @@ -3402,8 +3400,17 @@ struct RequestServer { } } - void stop() { + void serveEmbeddedHttp(alias fun, CustomCgi = Cgi, long maxContentLength = defaultMaxContentLength)() { + auto manager = new ListeningConnectionManager(listeningHost, listeningPort, &doThreadHttpConnection!(CustomCgi, fun)); + manager.listen(); + } + void serveScgi(alias fun, CustomCgi = Cgi, long maxContentLength = defaultMaxContentLength)() { + auto manager = new ListeningConnectionManager(listeningHost, listeningPort, &doThreadScgiConnection!(CustomCgi, fun, maxContentLength)); + manager.listen(); + } + void stop() { + // FIXME } } @@ -4020,7 +4027,6 @@ void doThreadHttpConnectionGuts(CustomCgi, alias fun, bool alwaysCloseConnection // I am otherwise NOT closing it here because the parent thread might still be able to make use of the keep-alive connection! } -version(scgi) void doThreadScgiConnection(CustomCgi, alias fun, long maxContentLength)(Socket connection) { // and now we can buffer scope(failure) @@ -5462,6 +5468,11 @@ version(cgi_with_websocket) { } } + // the recv thing can be invalidated so gotta copy it over ugh + if(d.length) { + m.data = m.data.dup(); + } + import core.stdc.string; memmove(receiveBuffer.ptr, d.ptr, d.length); receiveBufferUsedLength = d.length; @@ -8194,12 +8205,12 @@ html", true, true); /// typeof(null) (which is also used to represent functions returning `void`) do nothing /// in the default presenter - allowing the function to have full low-level control over the /// response. - void presentSuccessfulReturn(T : typeof(null))(Cgi cgi, T ret, typeof(null) meta, string format) { + void presentSuccessfulReturn(T : typeof(null), Meta)(Cgi cgi, T ret, Meta meta, string format) { // nothing intentionally! } /// Redirections are forwarded to [Cgi.setResponseLocation] - void presentSuccessfulReturn(T : Redirection)(Cgi cgi, T ret, typeof(null) meta, string format) { + void presentSuccessfulReturn(T : Redirection, Meta)(Cgi cgi, T ret, Meta meta, string format) { cgi.setResponseLocation(ret.to, true, getHttpCodeText(ret.code)); } @@ -8218,7 +8229,7 @@ html", true, true); } /// An instance of the [arsd.dom.FileResource] interface has its own content type; assume it is a download of some sort. - void presentSuccessfulReturn(T : FileResource)(Cgi cgi, T ret, typeof(null) meta, string format) { + void presentSuccessfulReturn(T : FileResource, Meta)(Cgi cgi, T ret, Meta meta, string format) { cgi.setCache(true); // not necessarily true but meh cgi.setResponseContentType(ret.contentType); cgi.write(ret.getData(), true); @@ -8241,10 +8252,14 @@ html", true, true); useful forms or richer error messages for the user. +/ void presentExceptionAsHtml(alias func, T)(Cgi cgi, Throwable t, T dg) { + presentExceptionAsHtmlImpl(cgi, t, createAutomaticFormForFunction!(func)(dg)); + } + + void presentExceptionAsHtmlImpl(Cgi cgi, Throwable t, Form automaticForm) { if(auto mae = cast(MissingArgumentException) t) { auto container = this.htmlContainer(); container.appendChild(Element.make("p", "Argument `" ~ mae.argumentName ~ "` of type `" ~ mae.argumentType ~ "` is missing")); - container.appendChild(createAutomaticFormForFunction!(func)(dg)); + container.appendChild(automaticForm); cgi.write(container.parentDocument.toString(), true); } else { @@ -8836,7 +8851,7 @@ private auto serveApiInternal(T)(string urlPrefix) { static if(is(typeof(overload) R == return)) static if(__traits(getProtection, overload) == "public" || __traits(getProtection, overload) == "export") { - static foreach(urlNameForMethod; urlNamesForMethod!(overload)(urlify(methodName))) + static foreach(urlNameForMethod; urlNamesForMethod!(overload, urlify(methodName))) case urlNameForMethod: static if(is(R : WebObject)) { @@ -9051,41 +9066,47 @@ struct Paginated(T) { string nextPageUrl; } -string[] urlNamesForMethod(alias method)(string def) { - auto verb = Cgi.RequestMethod.GET; - bool foundVerb = false; - bool foundNoun = false; - foreach(attr; __traits(getAttributes, method)) { - static if(is(typeof(attr) == Cgi.RequestMethod)) { - verb = attr; - if(foundVerb) - assert(0, "Multiple http verbs on one function is not currently supported"); - foundVerb = true; - } - static if(is(typeof(attr) == UrlName)) { - if(foundNoun) - assert(0, "Multiple url names on one function is not currently supported"); - foundNoun = true; - def = attr.name; +template urlNamesForMethod(alias method, string default_) { + string[] helper() { + auto verb = Cgi.RequestMethod.GET; + bool foundVerb = false; + bool foundNoun = false; + + string def = default_; + + foreach(attr; __traits(getAttributes, method)) { + static if(is(typeof(attr) == Cgi.RequestMethod)) { + verb = attr; + if(foundVerb) + assert(0, "Multiple http verbs on one function is not currently supported"); + foundVerb = true; + } + static if(is(typeof(attr) == UrlName)) { + if(foundNoun) + assert(0, "Multiple url names on one function is not currently supported"); + foundNoun = true; + def = attr.name; + } } + + if(def is null) + def = "__null"; + + string[] ret; + + static if(is(typeof(method) R == return)) { + static if(is(R : WebObject)) { + def ~= "/"; + foreach(v; __traits(allMembers, Cgi.RequestMethod)) + ret ~= v ~ " " ~ def; + } else { + ret ~= to!string(verb) ~ " " ~ def; + } + } else static assert(0); + + return ret; } - - if(def is null) - def = "__null"; - - string[] ret; - - static if(is(typeof(method) R == return)) { - static if(is(R : WebObject)) { - def ~= "/"; - foreach(v; __traits(allMembers, Cgi.RequestMethod)) - ret ~= v ~ " " ~ def; - } else { - ret ~= to!string(verb) ~ " " ~ def; - } - } else static assert(0); - - return ret; + enum urlNamesForMethod = helper(); } diff --git a/http2.d b/http2.d index 19f1466..1fb2d2e 100644 --- a/http2.d +++ b/http2.d @@ -2878,6 +2878,10 @@ class WebSocket { } } + if(d.length) { + m.data = m.data.dup(); + } + import core.stdc.string; memmove(receiveBuffer.ptr, d.ptr, d.length); receiveBufferUsedLength = d.length; diff --git a/minigui.d b/minigui.d index 3739de7..1319eb1 100644 --- a/minigui.d +++ b/minigui.d @@ -64,8 +64,6 @@ disabled widgets and menu items - TrackBar controls - event cleanup tooltips. api improvements @@ -423,7 +421,6 @@ class DropDownSelection : ComboboxBase { } sendResizeEvent(); } - } /++ @@ -1230,6 +1227,240 @@ struct WidgetPainter { // done.......... } +/++ + History: + Added Oct 28, 2020 ++/ +struct ControlledBy_(T, Args...) { + Args args; + this(Args args) { + this.args = args; + } + + private T construct(Widget parent) { + return new T(args, parent); + } +} + +/++ + History: + Added Oct 28, 2020 ++/ +auto ControlledBy(T, Args...)(Args args) { + return ControlledBy_!(T, Args)(args); +} + +struct ContainerMeta { + string name; + ContainerMeta[] children; + Widget function(Widget parent) factory; + + Widget instantiate(Widget parent) { + auto n = factory(parent); + n.name = name; + foreach(child; children) + child.instantiate(n); + return n; + } +} + +template Container(CArgs...) { + static if(CArgs.length && is(CArgs[0] : Widget)) { + private alias Super = CArgs[0]; + private alias CArgs2 = CArgs[1 .. $]; + } else { + private alias Super = Layout; + private alias CArgs2 = CArgs; + } + + class Container : Super { + this(Widget parent) { super(parent); } + + // just to partially support old gdc versions + version(GNU) { + static if(CArgs2.length >= 1) { enum tmp0 = CArgs2[0]; mixin typeof(tmp0).MethodOverride!(CArgs2[0]); } + static if(CArgs2.length >= 2) { enum tmp1 = CArgs2[1]; mixin typeof(tmp1).MethodOverride!(CArgs2[1]); } + static if(CArgs2.length >= 3) { enum tmp2 = CArgs2[2]; mixin typeof(tmp2).MethodOverride!(CArgs2[2]); } + static if(CArgs2.length > 3) static assert(0, "only a few overrides like this supported on your compiler version at this time"); + } else mixin(q{ + static foreach(Arg; CArgs2) { + mixin Arg.MethodOverride!(Arg); + } + }); + + static ContainerMeta opCall(string name, ContainerMeta[] children...) { + return ContainerMeta( + name, + children.dup, + function (Widget parent) { return new typeof(this)(parent); } + ); + } + + static ContainerMeta opCall(ContainerMeta[] children...) { + return opCall(null, children); + } + } +} + +struct Style { + static struct helper(string m, T) { + enum method = m; + T v; + + mixin template MethodOverride(typeof(this) v) { + mixin("override typeof(v.v) "~v.method~"() { return v.v; }"); + } + } + + static auto opDispatch(string method, T)(T value) { + return helper!(method, T)(value); + } +} + +/++ + The data controller widget is created by reflecting over the given + data type. You can use [ControlledBy] as a UDA on a struct or + just let it create things automatically. + + Unlike [dialog], this uses real-time updating of the data and + you add it to another window yourself. + + --- + struct Test { + int x; + int y; + } + + auto window = new Window(); + auto dcw = new DataControllerWidget!Test(new Test, window); + --- + + The way it works is any public members are given a widget based + on their data type, and public methods trigger an action button + if no relevant parameters or a dialog action if it does have + parameters, similar to the [menu] facility. + + If you change data programmatically, without going through the + DataControllerWidget methods, you will have to tell it something + has changed and it needs to redraw. This is done with the `invalidate` + method. + + History: + Added Oct 28, 2020 ++/ +/// Group: generating_from_code +class DataControllerWidget(T) : Widget { + static if(is(T == class) || is(T : const E[], E)) + private alias Tref = T; + else + private alias Tref = T*; + + Tref datum; + + /++ + See_also: [addDataControllerWidget] + +/ + this(Tref datum, Widget parent) { + this.datum = datum; + + Widget cp = this; + + super(parent); + + foreach(attr; __traits(getAttributes, T)) + static if(is(typeof(attr) == ContainerMeta)) { + cp = attr.instantiate(this); + } + + auto def = this.getByName("default"); + if(def !is null) + cp = def; + + Widget helper(string name) { + auto maybe = this.getByName(name); + if(maybe is null) + return cp; + return maybe; + + } + + foreach(member; __traits(allMembers, T)) + static if(member != "this") // wtf + static if(__traits(getProtection, __traits(getMember, this.datum, member)) == "public") { + auto w = widgetFor!(__traits(getMember, T, member))(&__traits(getMember, this.datum, member), helper(member)); + static if(is(typeof(__traits(getMember, this.datum, member)) == function)) + w.addEventListener("triggered", &__traits(getMember, this.datum, member)); + else static if(is(w : DropDownSelection)) + w.addEventListener("change", (Event e) { genericSetValue(&__traits(getMember, this.datum, member), e.stringValue); } ); + else + w.addEventListener("change", (Event e) { genericSetValue(&__traits(getMember, this.datum, member), e.intValue); } ); + } + } + + Widget[string] memberWidgets; +} + +void genericSetValue(T, W)(T* where, W what) { + import std.conv; + *where = to!T(what); + //*where = cast(T) stringToLong(what); +} + +// FIXME: integrate with AutomaticDialog +static auto widgetFor(alias tt, P)(P valptr, Widget parent) { + static if(controlledByCount!tt == 1) { + foreach(i, attr; __traits(getAttributes, tt)) { + static if(is(typeof(attr) == ControlledBy_!(T, Args), T, Args...)) { + auto w = attr.construct(parent); + static if(__traits(compiles, w.setPosition(*valptr))) + w.setPosition(*valptr); + else static if(__traits(compiles, w.setValue(*valptr))) + w.setValue(*valptr); + return w; + } + } + } else static if(controlledByCount!tt == 0) { + static if(is(typeof(tt) == enum)) { + auto dds = new DropDownSelection(parent); + foreach(idx, option; __traits(allMembers, typeof(tt))) { + dds.addOption(option); + if(__traits(getMember, typeof(tt), option) == *valptr) + dds.setSelection(cast(int) idx); + } + return dds; + } else static if(is(typeof(tt) : const long)) { + static assert(0); + } else static if(is(typeof(tt) : const string)) { + static assert(0); + } + } else static assert(0, "multiple controllers not yet supported"); +} + +private template controlledByCount(alias tt) { + static int helper() { + int count; + foreach(i, attr; __traits(getAttributes, tt)) + static if(is(typeof(attr) == ControlledBy_!(T, Args), T, Args...)) + count++; + return count; + } + + enum controlledByCount = helper; +} + +/++ + Intended for UFCS action like `window.addDataControllerWidget(new MyObject());` + ++/ +DataControllerWidget!T addDataControllerWidget(T)(Widget parent, T t) if(is(T == class)) { + return new DataControllerWidget!T(t, parent); +} + +/// ditto +DataControllerWidget!T addDataControllerWidget(T)(Widget parent, T* t) if(is(T == struct)) { + return new DataControllerWidget!T(t, parent); +} + /** The way this module works is it builds on top of a SimpleWindow from simpledisplay to provide simple controls and such. @@ -2627,6 +2858,308 @@ class ScrollableClientWidget : Widget { } */ +/++ + A slider, also known as a trackbar control, is commonly used in applications like volume controls where you want the user to select a value between a min and a max without needing a specific value or otherwise precise input. ++/ +abstract class Slider : Widget { + this(int min, int max, int step, Widget parent) { + min_ = min; + max_ = max; + step_ = step; + page_ = step; + super(parent); + } + + private int min_; + private int max_; + private int step_; + private int position_; + private int page_; + + // selection start and selection end + // tics + // tooltip? + // some way to see and just type the value + // win32 buddy controls are labels + + /// + void setMin(int a) { + min_ = a; + version(custom_widgets) + redraw(); + version(win32_widgets) + SendMessage(hwnd, TBM_SETRANGEMIN, true, a); + } + /// + int min() { + return min_; + } + /// + void setMax(int a) { + max_ = a; + version(custom_widgets) + redraw(); + version(win32_widgets) + SendMessage(hwnd, TBM_SETRANGEMAX, true, a); + } + /// + int max() { + return max_; + } + /// + void setPosition(int a) { + if(a > max) + a = max; + if(a < min) + a = min; + position_ = a; + version(custom_widgets) + setPositionCustom(a); + + version(win32_widgets) + setPositionWindows(a); + } + version(win32_widgets) { + protected abstract void setPositionWindows(int a); + } + + protected abstract int win32direction(); + + /// + int position() { + return position_; + } + /// + void setStep(int a) { + step_ = a; + version(win32_widgets) + SendMessage(hwnd, TBM_SETLINESIZE, 0, a); + } + /// + int step() { + return step_; + } + /// + void setPageSize(int a) { + page_ = a; + version(win32_widgets) + SendMessage(hwnd, TBM_SETPAGESIZE, 0, a); + } + /// + int pageSize() { + return page_; + } + + private void notify() { + auto event = new Event("change", this); + event.intValue = this.position; + event.dispatch(); + } + + version(win32_widgets) + void win32Setup(int style) { + createWin32Window(this, TRACKBAR_CLASS, "", + 0|WS_CHILD|WS_VISIBLE|style|TBS_TOOLTIPS, 0); + + // the trackbar sends the same messages as scroll, which + // our other layer sends as these... just gonna translate + // here + this.addDirectEventListener("scrolltoposition", (Event event) { + event.stopPropagation(); + this.setPosition(this.win32direction > 0 ? event.intValue : max - event.intValue); + notify(); + }); + this.addDirectEventListener("scrolltonextline", (Event event) { + event.stopPropagation(); + this.setPosition(this.position + this.step_ * this.win32direction); + notify(); + }); + this.addDirectEventListener("scrolltopreviousline", (Event event) { + event.stopPropagation(); + this.setPosition(this.position - this.step_ * this.win32direction); + notify(); + }); + this.addDirectEventListener("scrolltonextpage", (Event event) { + event.stopPropagation(); + this.setPosition(this.position + this.page_ * this.win32direction); + notify(); + }); + this.addDirectEventListener("scrolltopreviouspage", (Event event) { + event.stopPropagation(); + this.setPosition(this.position - this.page_ * this.win32direction); + notify(); + }); + + setMin(min_); + setMax(max_); + setStep(step_); + setPageSize(page_); + } + + version(custom_widgets) { + protected MouseTrackingWidget thumb; + + protected abstract void setPositionCustom(int a); + + override void defaultEventHandler_keydown(Event event) { + switch(event.key) { + case Key.Up: + case Key.Right: + setPosition(position() - step() * win32direction); + changed(); + break; + case Key.Down: + case Key.Left: + setPosition(position() + step() * win32direction); + changed(); + break; + case Key.Home: + setPosition(win32direction > 0 ? min() : max()); + changed(); + break; + case Key.End: + setPosition(win32direction > 0 ? max() : min()); + changed(); + break; + case Key.PageUp: + setPosition(position() - pageSize() * win32direction); + changed(); + break; + case Key.PageDown: + setPosition(position() + pageSize() * win32direction); + changed(); + break; + default: + } + super.defaultEventHandler_keydown(event); + } + + protected void changed() { + auto ev = new Event("change", this); + ev.intValue = position_; + ev.dispatch(); + } + + + } +} + +/++ + ++/ +class VerticalSlider : Slider { + this(int min, int max, int step, Widget parent) { + version(custom_widgets) + initialize(); + + super(min, max, step, parent); + + version(win32_widgets) + win32Setup(TBS_VERT | TBS_REVERSED); + } + + protected override int win32direction() { + return -1; + } + + version(win32_widgets) + protected override void setPositionWindows(int a) { + // the windows thing makes the top 0 and i don't like that. + SendMessage(hwnd, TBM_SETPOS, true, max - a); + } + + version(custom_widgets) + private void initialize() { + thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.vertical, this); + + thumb.tabStop = false; + + thumb.thumbWidth = width; + thumb.thumbHeight = 16; + + thumb.addEventListener(EventType.change, () { + auto sx = thumb.positionY * max() / (thumb.height - 16); + sx = max - sx; + //informProgramThatUserChangedPosition(sx); + + position_ = sx; + + changed(); + }); + } + + version(custom_widgets) + override void recomputeChildLayout() { + thumb.thumbWidth = this.width; + super.recomputeChildLayout(); + setPositionCustom(position_); + } + + version(custom_widgets) + protected override void setPositionCustom(int a) { + if(max()) + thumb.positionY = (max - a) * (thumb.height - 16) / max(); + redraw(); + } +} + +/++ + ++/ +class HorizontalSlider : Slider { + this(int min, int max, int step, Widget parent) { + version(custom_widgets) + initialize(); + + super(min, max, step, parent); + + version(win32_widgets) + win32Setup(TBS_HORZ); + } + + version(win32_widgets) + protected override void setPositionWindows(int a) { + SendMessage(hwnd, TBM_SETPOS, true, a); + } + + protected override int win32direction() { + return 1; + } + + version(custom_widgets) + private void initialize() { + thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.horizontal, this); + + thumb.tabStop = false; + + thumb.thumbWidth = 16; + thumb.thumbHeight = height; + + thumb.addEventListener(EventType.change, () { + auto sx = thumb.positionX * max() / (thumb.width - 16); + //informProgramThatUserChangedPosition(sx); + + position_ = sx; + + changed(); + }); + } + + version(custom_widgets) + override void recomputeChildLayout() { + thumb.thumbHeight = this.height; + super.recomputeChildLayout(); + setPositionCustom(position_); + } + + version(custom_widgets) + protected override void setPositionCustom(int a) { + if(max()) + thumb.positionX = a * (thumb.width - 16) / max(); + } +} + + /// abstract class ScrollbarBase : Widget { /// @@ -7584,10 +8117,12 @@ class AutomaticDialog(T) : Dialog { }); } else static if(is(type : long)) { auto le = new LabeledLineEdit(memberName.beautify ~ ": ", this); + /+ le.addEventListener("char", (Event ev) { if((ev.character < '0' || ev.character > '9') && ev.character != '-') ev.preventDefault(); }); + +/ le.addEventListener(EventType.change, (Event ev) { __traits(getMember, t, memberName) = cast(type) stringToLong(ev.stringValue); }); diff --git a/terminalemulator.d b/terminalemulator.d index 6ea6884..e4d4a59 100644 --- a/terminalemulator.d +++ b/terminalemulator.d @@ -4004,8 +4004,15 @@ mixin template SdpyDraw() { this.font = new OperatingSystemFont("Courier New", size, FontWeight.medium); } - fontWidth = font.averageWidth; - fontHeight = font.height; + if(font.isNull) { + // no way to really tell... just guess so it doesn't crash but like eeek. + fontWidth = size / 2; + fontHeight = size; + + } else { + fontWidth = font.averageWidth; + fontHeight = font.height; + } } bool lastDrawAlternativeScreen;