Answer the rest of what a client asks its window
The other seven glimmer-vidya calls frq makes: the title, the display, the browser, the clipboard, the picture chooser and asking to close. None of it is drawing and none of it belongs in a widget, but a client cannot do any of it either — the window handle lives in the toolkit, so the questions come here. jvui.host holds that handle and answers a harmless nothing before a window exists: no title to set, no display to measure, no clipboard to read. jvui's own tests run with no window and should not have to guard every call, and neither should a client during startup. quit! is a REQUEST, not an exit. The loop owns teardown — textures, painter, fonts, the window — and tearing any of it down from under a walk that is still running is how closing turns into crashing. screen-size is the display's USABLE bounds rather than its raw ones: a window sized to the whole display is one with its bottom edge under a taskbar. pick-image! answers false and picked-image! nil, and those are the correct answers rather than gaps. The chooser exists so a phone can hand back a grant for one picture without the app holding a permission over all of them; a desktop has no such thing, browsing the filesystem is the answer there, and glimmer-vidya says exactly the same thing on a desktop. An Android jvui would replace them. clipboard-image-png! asks for image/png only. A clipboard may hold a bitmap or a TIFF, and converting those means an encoder in here — a lot of toolkit for a paste, when every desktop that puts a picture on a clipboard this decade puts a PNG there too. Tested against a real window under SDL's dummy video driver, including the before-a-window answers and that quit! actually stops the loop early. Not open-url!: succeeding would open a browser on somebody's desktop and failing would only say this container has none. That is twelve of twelve. frq's whole glimmer-vidya surface now exists here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
0b92f67 parent: 32fae8d modified
glimmer-backends/glimmer-jvui/src/glimmer_jvui/core.clj +22 -1 | @@ -40,7 +40,8 @@ | ||
| 40 | 40 | [jvui.core :as c] |
| 41 | 41 | [jvui.theme :as theme] |
| 42 | 42 | [jvui.widgets :as w] |
| 43 | - [jvui.frames :as frames])) | |
| 43 | + [jvui.frames :as frames] | |
| 44 | + [jvui.host :as host])) | |
| 44 | 45 | |
| 45 | 46 | ;; --- the retained tree ------------------------------------------------------- |
| 46 | 47 | |
| @@ -326,3 +327,23 @@ | ||
| 326 | 327 | "Every feed with a picture." |
| 327 | 328 | [] |
| 328 | 329 | (frames/keys*)) |
| 330 | + | |
| 331 | +;; --- the platform ----------------------------------------------------------- | |
| 332 | +;; The rest of what glimmer-vidya answers, so a client can ask its backend | |
| 333 | +;; about the window it is in without knowing which backend that is. Thin on | |
| 334 | +;; purpose: every one of these is jvui.host, and the indirection exists so | |
| 335 | +;; the client requires one namespace rather than two. | |
| 336 | + | |
| 337 | +(def set-title! host/set-title!) | |
| 338 | +(def window-width host/window-width) | |
| 339 | +(def screen-size host/screen-size) | |
| 340 | +(def quit! host/quit!) | |
| 341 | +(def open-url! host/open-url!) | |
| 342 | +(def clipboard-image-png! host/clipboard-image-png!) | |
| 343 | + | |
| 344 | +;; False and nil on a desktop, which is the right answer rather than a gap: | |
| 345 | +;; the chooser exists so a phone can hand back a grant for one picture, and | |
| 346 | +;; a caller reads the false and offers a file browser instead. glimmer-vidya | |
| 347 | +;; says the same thing here. | |
| 348 | +(def pick-image! host/pick-image!) | |
| 349 | +(def picked-image! host/picked-image!) | |
| @@ -40,7 +40,8 @@ | |||
| 40 | [jvui.core :as c] | 40 | [jvui.core :as c] |
| 41 | [jvui.theme :as theme] | 41 | [jvui.theme :as theme] |
| 42 | [jvui.widgets :as w] | 42 | [jvui.widgets :as w] |
| 43 | - [jvui.frames :as frames])) | 43 | + [jvui.frames :as frames] |
| 44 | + [jvui.host :as host])) | ||
| 44 | 45 | ||
| 45 | ;; --- the retained tree ------------------------------------------------------- | 46 | ;; --- the retained tree ------------------------------------------------------- |
| 46 | 47 | ||
| @@ -326,3 +327,23 @@ | |||
| 326 | "Every feed with a picture." | 327 | "Every feed with a picture." |
| 327 | [] | 328 | [] |
| 328 | (frames/keys*)) | 329 | (frames/keys*)) |
| 330 | + | ||
| 331 | +;; --- the platform ----------------------------------------------------------- | ||
| 332 | +;; The rest of what glimmer-vidya answers, so a client can ask its backend | ||
| 333 | +;; about the window it is in without knowing which backend that is. Thin on | ||
| 334 | +;; purpose: every one of these is jvui.host, and the indirection exists so | ||
| 335 | +;; the client requires one namespace rather than two. | ||
| 336 | + | ||
| 337 | +(def set-title! host/set-title!) | ||
| 338 | +(def window-width host/window-width) | ||
| 339 | +(def screen-size host/screen-size) | ||
| 340 | +(def quit! host/quit!) | ||
| 341 | +(def open-url! host/open-url!) | ||
| 342 | +(def clipboard-image-png! host/clipboard-image-png!) | ||
| 343 | + | ||
| 344 | +;; False and nil on a desktop, which is the right answer rather than a gap: | ||
| 345 | +;; the chooser exists so a phone can hand back a grant for one picture, and | ||
| 346 | +;; a caller reads the false and offers a file browser instead. glimmer-vidya | ||
| 347 | +;; says the same thing here. | ||
| 348 | +(def pick-image! host/pick-image!) | ||
| 349 | +(def picked-image! host/picked-image!) | ||
modified
jvui/src/jvui/app.clj +8 -0 | @@ -8,6 +8,7 @@ | ||
| 8 | 8 | [jvui.font :as font] |
| 9 | 9 | [jvui.paint :as paint] |
| 10 | 10 | [jvui.frames :as frames] |
| 11 | + [jvui.host :as host] | |
| 11 | 12 | [jvui.theme :as theme] |
| 12 | 13 | [jvui.core :as c])) |
| 13 | 14 | |
| @@ -73,6 +74,7 @@ | ||
| 73 | 74 | ;; into. Anything that arrives before this is dropped rather than |
| 74 | 75 | ;; queued — see jvui.frames. |
| 75 | 76 | (frames/install-renderer! renderer) |
| 77 | + (host/install! window) | |
| 76 | 78 | (sdl/start-text-input! window) |
| 77 | 79 | (try |
| 78 | 80 | (loop [n 0] |
| @@ -97,11 +99,17 @@ | ||
| 97 | 99 | (sdl/present! renderer) |
| 98 | 100 | (sdl/delay-ms! 16) |
| 99 | 101 | (when-not (or (:quit? @ctx) |
| 102 | + ;; A client asking to close — see jvui.host/quit!. | |
| 103 | + ;; Checked here so teardown stays the loop's, which | |
| 104 | + ;; is the only thing that knows what is still being | |
| 105 | + ;; walked. | |
| 106 | + (host/quit-requested?) | |
| 100 | 107 | (and frames (>= (inc n) frames)) |
| 101 | 108 | (and deadline (> (System/currentTimeMillis) deadline))) |
| 102 | 109 | (recur (inc n))))) |
| 103 | 110 | (finally |
| 104 | 111 | (frames/clear!) |
| 112 | + (host/clear!) | |
| 105 | 113 | (paint/close! painter) |
| 106 | 114 | (font/close! fonts) |
| 107 | 115 | (sdl/close! win)))))) |
| @@ -8,6 +8,7 @@ | |||
| 8 | [jvui.font :as font] | 8 | [jvui.font :as font] |
| 9 | [jvui.paint :as paint] | 9 | [jvui.paint :as paint] |
| 10 | [jvui.frames :as frames] | 10 | [jvui.frames :as frames] |
| 11 | + [jvui.host :as host] | ||
| 11 | [jvui.theme :as theme] | 12 | [jvui.theme :as theme] |
| 12 | [jvui.core :as c])) | 13 | [jvui.core :as c])) |
| 13 | 14 | ||
| @@ -73,6 +74,7 @@ | |||
| 73 | ;; into. Anything that arrives before this is dropped rather than | 74 | ;; into. Anything that arrives before this is dropped rather than |
| 74 | ;; queued — see jvui.frames. | 75 | ;; queued — see jvui.frames. |
| 75 | (frames/install-renderer! renderer) | 76 | (frames/install-renderer! renderer) |
| 77 | + (host/install! window) | ||
| 76 | (sdl/start-text-input! window) | 78 | (sdl/start-text-input! window) |
| 77 | (try | 79 | (try |
| 78 | (loop [n 0] | 80 | (loop [n 0] |
| @@ -97,11 +99,17 @@ | |||
| 97 | (sdl/present! renderer) | 99 | (sdl/present! renderer) |
| 98 | (sdl/delay-ms! 16) | 100 | (sdl/delay-ms! 16) |
| 99 | (when-not (or (:quit? @ctx) | 101 | (when-not (or (:quit? @ctx) |
| 102 | + ;; A client asking to close — see jvui.host/quit!. | ||
| 103 | + ;; Checked here so teardown stays the loop's, which | ||
| 104 | + ;; is the only thing that knows what is still being | ||
| 105 | + ;; walked. | ||
| 106 | + (host/quit-requested?) | ||
| 100 | (and frames (>= (inc n) frames)) | 107 | (and frames (>= (inc n) frames)) |
| 101 | (and deadline (> (System/currentTimeMillis) deadline))) | 108 | (and deadline (> (System/currentTimeMillis) deadline))) |
| 102 | (recur (inc n))))) | 109 | (recur (inc n))))) |
| 103 | (finally | 110 | (finally |
| 104 | (frames/clear!) | 111 | (frames/clear!) |
| 112 | + (host/clear!) | ||
| 105 | (paint/close! painter) | 113 | (paint/close! painter) |
| 106 | (font/close! fonts) | 114 | (font/close! fonts) |
| 107 | (sdl/close! win)))))) | 115 | (sdl/close! win)))))) |
added
jvui/src/jvui/host.clj +112 -0 | new file mode 100644 | ||
| @@ -0,0 +1,112 @@ | ||
| 1 | +(ns jvui.host | |
| 2 | + "The window as a thing in an operating system, rather than a thing to draw in. | |
| 3 | + | |
| 4 | + Its title, the display it is on, the browser, the clipboard, and asking it | |
| 5 | + to close. None of this is painting and none of it belongs in a widget, but | |
| 6 | + a client cannot do any of it either — the window handle lives here, so the | |
| 7 | + questions come here too. | |
| 8 | + | |
| 9 | + Everything answers a harmless nothing before a window exists: no title to | |
| 10 | + set, no display to measure, no clipboard to read. A client that runs | |
| 11 | + headless — jvui's own tests do — should not have to guard every call." | |
| 12 | + (:require [jvui.sdl :as sdl])) | |
| 13 | + | |
| 14 | +(defonce ^:private win (atom nil)) | |
| 15 | +(defonce ^:private quit? (atom false)) | |
| 16 | + | |
| 17 | +(defn install! | |
| 18 | + "Called by the frame loop when a window opens." | |
| 19 | + [window] | |
| 20 | + (reset! win window) | |
| 21 | + (reset! quit? false)) | |
| 22 | + | |
| 23 | +(defn clear! [] (reset! win nil) nil) | |
| 24 | + | |
| 25 | +(defn window | |
| 26 | + "The open window, or nil." | |
| 27 | + [] | |
| 28 | + @win) | |
| 29 | + | |
| 30 | +;; --- closing ----------------------------------------------------------------- | |
| 31 | + | |
| 32 | +(defn quit! | |
| 33 | + "Ask the frame loop to stop after the current frame. | |
| 34 | + | |
| 35 | + A request rather than an exit: the loop owns teardown — textures, painter, | |
| 36 | + fonts, the window itself — and tearing any of it down from under a walk | |
| 37 | + that is still running is how a close turns into a crash." | |
| 38 | + [] | |
| 39 | + (reset! quit? true) | |
| 40 | + nil) | |
| 41 | + | |
| 42 | +(defn quit-requested? [] @quit?) | |
| 43 | + | |
| 44 | +;; --- the window -------------------------------------------------------------- | |
| 45 | + | |
| 46 | +(defn set-title! [s] | |
| 47 | + (when-let [w @win] (sdl/set-window-title! w (str s))) | |
| 48 | + nil) | |
| 49 | + | |
| 50 | +(defn window-size | |
| 51 | + "[w h] in window units, or nil with no window." | |
| 52 | + [] | |
| 53 | + (when-let [w @win] (sdl/window-size w))) | |
| 54 | + | |
| 55 | +(defn window-width | |
| 56 | + "Width in window units, or 0 with no window — a caller dividing by it wants | |
| 57 | + a number, and zero is the honest one." | |
| 58 | + [] | |
| 59 | + (or (first (window-size)) 0)) | |
| 60 | + | |
| 61 | +(defn screen-size | |
| 62 | + "[w h] of the display this window is on, or nil with no window." | |
| 63 | + [] | |
| 64 | + (when-let [w @win] (sdl/display-bounds w))) | |
| 65 | + | |
| 66 | +;; --- the world outside ------------------------------------------------------- | |
| 67 | + | |
| 68 | +(defn open-url! | |
| 69 | + "Hand a URL to whatever the desktop opens URLs with." | |
| 70 | + [url] | |
| 71 | + (boolean (and (string? url) (seq url) (sdl/open-url! url)))) | |
| 72 | + | |
| 73 | +(defn clipboard-image-png! | |
| 74 | + "Write the clipboard's picture to `path` as PNG; true when there was one. | |
| 75 | + | |
| 76 | + Only image/png is asked for. A clipboard may also hold a bitmap or a TIFF | |
| 77 | + and converting those would mean an encoder in here, which is a lot of | |
| 78 | + toolkit for a paste — and every desktop that puts a picture on a clipboard | |
| 79 | + this decade puts a PNG there too." | |
| 80 | + [path] | |
| 81 | + (boolean | |
| 82 | + (when (sdl/has-clipboard-data "image/png") | |
| 83 | + (when-let [[p n] (sdl/clipboard-data "image/png")] | |
| 84 | + (try | |
| 85 | + (let [bs (byte-array n)] | |
| 86 | + (jolt.ffi/read-array p :byte bs) | |
| 87 | + (java.nio.file.Files/write | |
| 88 | + (java.nio.file.Path/of (str path) (into-array String [])) | |
| 89 | + bs | |
| 90 | + (into-array java.nio.file.OpenOption [])) | |
| 91 | + true) | |
| 92 | + (finally (sdl/sdl-free! p))))))) | |
| 93 | + | |
| 94 | +;; --- the picture chooser ------------------------------------------------------ | |
| 95 | +;; There is not one, and that is the same answer glimmer-vidya gives on a | |
| 96 | +;; desktop. The chooser exists so a phone can hand back a grant for one | |
| 97 | +;; picture without the app holding a permission over all of them; a desktop | |
| 98 | +;; has no such thing and browsing the filesystem is the answer there. | |
| 99 | +;; | |
| 100 | +;; So these are not stubs waiting to be filled in on this backend — they are | |
| 101 | +;; the correct answer for it, and a caller reads the false and offers a file | |
| 102 | +;; browser instead. An Android jvui would replace them. | |
| 103 | + | |
| 104 | +(defn pick-image! | |
| 105 | + "False: this platform has no picture chooser." | |
| 106 | + [] | |
| 107 | + false) | |
| 108 | + | |
| 109 | +(defn picked-image! | |
| 110 | + "nil: nothing was ever chosen, because nothing could be." | |
| 111 | + [_path] | |
| 112 | + nil) | |
| new file mode 100644 | |||
| @@ -0,0 +1,112 @@ | |||
| 1 | +(ns jvui.host | ||
| 2 | + "The window as a thing in an operating system, rather than a thing to draw in. | ||
| 3 | + | ||
| 4 | + Its title, the display it is on, the browser, the clipboard, and asking it | ||
| 5 | + to close. None of this is painting and none of it belongs in a widget, but | ||
| 6 | + a client cannot do any of it either — the window handle lives here, so the | ||
| 7 | + questions come here too. | ||
| 8 | + | ||
| 9 | + Everything answers a harmless nothing before a window exists: no title to | ||
| 10 | + set, no display to measure, no clipboard to read. A client that runs | ||
| 11 | + headless — jvui's own tests do — should not have to guard every call." | ||
| 12 | + (:require [jvui.sdl :as sdl])) | ||
| 13 | + | ||
| 14 | +(defonce ^:private win (atom nil)) | ||
| 15 | +(defonce ^:private quit? (atom false)) | ||
| 16 | + | ||
| 17 | +(defn install! | ||
| 18 | + "Called by the frame loop when a window opens." | ||
| 19 | + [window] | ||
| 20 | + (reset! win window) | ||
| 21 | + (reset! quit? false)) | ||
| 22 | + | ||
| 23 | +(defn clear! [] (reset! win nil) nil) | ||
| 24 | + | ||
| 25 | +(defn window | ||
| 26 | + "The open window, or nil." | ||
| 27 | + [] | ||
| 28 | + @win) | ||
| 29 | + | ||
| 30 | +;; --- closing ----------------------------------------------------------------- | ||
| 31 | + | ||
| 32 | +(defn quit! | ||
| 33 | + "Ask the frame loop to stop after the current frame. | ||
| 34 | + | ||
| 35 | + A request rather than an exit: the loop owns teardown — textures, painter, | ||
| 36 | + fonts, the window itself — and tearing any of it down from under a walk | ||
| 37 | + that is still running is how a close turns into a crash." | ||
| 38 | + [] | ||
| 39 | + (reset! quit? true) | ||
| 40 | + nil) | ||
| 41 | + | ||
| 42 | +(defn quit-requested? [] @quit?) | ||
| 43 | + | ||
| 44 | +;; --- the window -------------------------------------------------------------- | ||
| 45 | + | ||
| 46 | +(defn set-title! [s] | ||
| 47 | + (when-let [w @win] (sdl/set-window-title! w (str s))) | ||
| 48 | + nil) | ||
| 49 | + | ||
| 50 | +(defn window-size | ||
| 51 | + "[w h] in window units, or nil with no window." | ||
| 52 | + [] | ||
| 53 | + (when-let [w @win] (sdl/window-size w))) | ||
| 54 | + | ||
| 55 | +(defn window-width | ||
| 56 | + "Width in window units, or 0 with no window — a caller dividing by it wants | ||
| 57 | + a number, and zero is the honest one." | ||
| 58 | + [] | ||
| 59 | + (or (first (window-size)) 0)) | ||
| 60 | + | ||
| 61 | +(defn screen-size | ||
| 62 | + "[w h] of the display this window is on, or nil with no window." | ||
| 63 | + [] | ||
| 64 | + (when-let [w @win] (sdl/display-bounds w))) | ||
| 65 | + | ||
| 66 | +;; --- the world outside ------------------------------------------------------- | ||
| 67 | + | ||
| 68 | +(defn open-url! | ||
| 69 | + "Hand a URL to whatever the desktop opens URLs with." | ||
| 70 | + [url] | ||
| 71 | + (boolean (and (string? url) (seq url) (sdl/open-url! url)))) | ||
| 72 | + | ||
| 73 | +(defn clipboard-image-png! | ||
| 74 | + "Write the clipboard's picture to `path` as PNG; true when there was one. | ||
| 75 | + | ||
| 76 | + Only image/png is asked for. A clipboard may also hold a bitmap or a TIFF | ||
| 77 | + and converting those would mean an encoder in here, which is a lot of | ||
| 78 | + toolkit for a paste — and every desktop that puts a picture on a clipboard | ||
| 79 | + this decade puts a PNG there too." | ||
| 80 | + [path] | ||
| 81 | + (boolean | ||
| 82 | + (when (sdl/has-clipboard-data "image/png") | ||
| 83 | + (when-let [[p n] (sdl/clipboard-data "image/png")] | ||
| 84 | + (try | ||
| 85 | + (let [bs (byte-array n)] | ||
| 86 | + (jolt.ffi/read-array p :byte bs) | ||
| 87 | + (java.nio.file.Files/write | ||
| 88 | + (java.nio.file.Path/of (str path) (into-array String [])) | ||
| 89 | + bs | ||
| 90 | + (into-array java.nio.file.OpenOption [])) | ||
| 91 | + true) | ||
| 92 | + (finally (sdl/sdl-free! p))))))) | ||
| 93 | + | ||
| 94 | +;; --- the picture chooser ------------------------------------------------------ | ||
| 95 | +;; There is not one, and that is the same answer glimmer-vidya gives on a | ||
| 96 | +;; desktop. The chooser exists so a phone can hand back a grant for one | ||
| 97 | +;; picture without the app holding a permission over all of them; a desktop | ||
| 98 | +;; has no such thing and browsing the filesystem is the answer there. | ||
| 99 | +;; | ||
| 100 | +;; So these are not stubs waiting to be filled in on this backend — they are | ||
| 101 | +;; the correct answer for it, and a caller reads the false and offers a file | ||
| 102 | +;; browser instead. An Android jvui would replace them. | ||
| 103 | + | ||
| 104 | +(defn pick-image! | ||
| 105 | + "False: this platform has no picture chooser." | ||
| 106 | + [] | ||
| 107 | + false) | ||
| 108 | + | ||
| 109 | +(defn picked-image! | ||
| 110 | + "nil: nothing was ever chosen, because nothing could be." | ||
| 111 | + [_path] | ||
| 112 | + nil) | ||
modified
jvui/src/jvui/sdl.clj +35 -0 | @@ -282,6 +282,41 @@ | ||
| 282 | 282 | (ffi/write-array p :int pixels) |
| 283 | 283 | (raw-update-texture tex ffi/null p (* 4 w))))) |
| 284 | 284 | |
| 285 | +;; --- the platform around the window ----------------------------------------- | |
| 286 | +;; Title, display, browser, clipboard. None of it is drawing, and all of it is | |
| 287 | +;; what a client asks the toolkit for because the toolkit is the only thing | |
| 288 | +;; holding a window handle. | |
| 289 | + | |
| 290 | +(ffi/defcfn set-window-title! "SDL_SetWindowTitle" [:pointer :string] :bool) | |
| 291 | +(ffi/defcfn open-url! "SDL_OpenURL" [:string] :bool) | |
| 292 | +(ffi/defcfn display-for-window "SDL_GetDisplayForWindow" [:pointer] :uint) | |
| 293 | +(ffi/defcfn ^:private raw-display-bounds "SDL_GetDisplayUsableBounds" | |
| 294 | + [:uint :pointer] :bool) | |
| 295 | +(ffi/defcfn has-clipboard-data "SDL_HasClipboardData" [:string] :bool) | |
| 296 | +(ffi/defcfn ^:private raw-clipboard-data "SDL_GetClipboardData" | |
| 297 | + [:string :pointer] :pointer) | |
| 298 | +(ffi/defcfn sdl-free! "SDL_free" [:pointer] :void) | |
| 299 | + | |
| 300 | +(defn display-bounds | |
| 301 | + "[w h] of the display `window` is on, minus whatever the desktop reserves | |
| 302 | + for panels — usable bounds rather than raw, because a window sized to the | |
| 303 | + whole display is one with its bottom edge under a taskbar." | |
| 304 | + [window] | |
| 305 | + (ffi/with-alloc [r 16] | |
| 306 | + (when (raw-display-bounds (display-for-window window) r) | |
| 307 | + [(ffi/read (+ r 8) :int) (ffi/read (+ r 12) :int)]))) | |
| 308 | + | |
| 309 | +(defn clipboard-data | |
| 310 | + "The clipboard's contents for `mime`, as [pointer length], or nil. | |
| 311 | + | |
| 312 | + The pointer is SDL's and the caller must hand it back to `sdl-free!`." | |
| 313 | + [mime] | |
| 314 | + (ffi/with-alloc [sz 8] | |
| 315 | + (let [p (raw-clipboard-data mime sz) | |
| 316 | + n (ffi/read sz :uint64)] | |
| 317 | + (when (and p (not (ffi/null? p)) (pos? n)) | |
| 318 | + [p n])))) | |
| 319 | + | |
| 285 | 320 | (defn update-texture-raw! |
| 286 | 321 | "Upload `h` rows of `pitch` bytes from FOREIGN memory into the whole of `tex`. |
| 287 | 322 | |
| @@ -282,6 +282,41 @@ | |||
| 282 | (ffi/write-array p :int pixels) | 282 | (ffi/write-array p :int pixels) |
| 283 | (raw-update-texture tex ffi/null p (* 4 w))))) | 283 | (raw-update-texture tex ffi/null p (* 4 w))))) |
| 284 | 284 | ||
| 285 | +;; --- the platform around the window ----------------------------------------- | ||
| 286 | +;; Title, display, browser, clipboard. None of it is drawing, and all of it is | ||
| 287 | +;; what a client asks the toolkit for because the toolkit is the only thing | ||
| 288 | +;; holding a window handle. | ||
| 289 | + | ||
| 290 | +(ffi/defcfn set-window-title! "SDL_SetWindowTitle" [:pointer :string] :bool) | ||
| 291 | +(ffi/defcfn open-url! "SDL_OpenURL" [:string] :bool) | ||
| 292 | +(ffi/defcfn display-for-window "SDL_GetDisplayForWindow" [:pointer] :uint) | ||
| 293 | +(ffi/defcfn ^:private raw-display-bounds "SDL_GetDisplayUsableBounds" | ||
| 294 | + [:uint :pointer] :bool) | ||
| 295 | +(ffi/defcfn has-clipboard-data "SDL_HasClipboardData" [:string] :bool) | ||
| 296 | +(ffi/defcfn ^:private raw-clipboard-data "SDL_GetClipboardData" | ||
| 297 | + [:string :pointer] :pointer) | ||
| 298 | +(ffi/defcfn sdl-free! "SDL_free" [:pointer] :void) | ||
| 299 | + | ||
| 300 | +(defn display-bounds | ||
| 301 | + "[w h] of the display `window` is on, minus whatever the desktop reserves | ||
| 302 | + for panels — usable bounds rather than raw, because a window sized to the | ||
| 303 | + whole display is one with its bottom edge under a taskbar." | ||
| 304 | + [window] | ||
| 305 | + (ffi/with-alloc [r 16] | ||
| 306 | + (when (raw-display-bounds (display-for-window window) r) | ||
| 307 | + [(ffi/read (+ r 8) :int) (ffi/read (+ r 12) :int)]))) | ||
| 308 | + | ||
| 309 | +(defn clipboard-data | ||
| 310 | + "The clipboard's contents for `mime`, as [pointer length], or nil. | ||
| 311 | + | ||
| 312 | + The pointer is SDL's and the caller must hand it back to `sdl-free!`." | ||
| 313 | + [mime] | ||
| 314 | + (ffi/with-alloc [sz 8] | ||
| 315 | + (let [p (raw-clipboard-data mime sz) | ||
| 316 | + n (ffi/read sz :uint64)] | ||
| 317 | + (when (and p (not (ffi/null? p)) (pos? n)) | ||
| 318 | + [p n])))) | ||
| 319 | + | ||
| 285 | (defn update-texture-raw! | 320 | (defn update-texture-raw! |
| 286 | "Upload `h` rows of `pitch` bytes from FOREIGN memory into the whole of `tex`. | 321 | "Upload `h` rows of `pitch` bytes from FOREIGN memory into the whole of `tex`. |
| 287 | 322 | ||
added
jvui/test/jvui/host_check.clj +52 -0 | new file mode 100644 | ||
| @@ -0,0 +1,52 @@ | ||
| 1 | +(ns jvui.host-check | |
| 2 | + "The seven platform calls, against a real window. | |
| 3 | + | |
| 4 | + Under SDL's dummy video driver, which gives a window and a renderer and no | |
| 5 | + display. That limits what can be claimed and the claims are limited to | |
| 6 | + match: `open-url!` is not invoked, because succeeding would mean a browser | |
| 7 | + opening on somebody's desktop and failing would only mean this container | |
| 8 | + has none. Everything else is checked for real." | |
| 9 | + (:require [jvui.app :as app] | |
| 10 | + [jvui.host :as host] | |
| 11 | + [jvui.widgets :as w])) | |
| 12 | + | |
| 13 | +(defn -main [& _] | |
| 14 | + (let [out (atom []) | |
| 15 | + ck! (fn [n ok?] (swap! out conj [n (boolean ok?)])) | |
| 16 | + seen (atom 0)] | |
| 17 | + ;; Before any window: everything must answer harmlessly rather than throw. | |
| 18 | + (ck! "no window: screen-size is nil" (nil? (host/screen-size))) | |
| 19 | + (ck! "no window: window-width is 0" (zero? (host/window-width))) | |
| 20 | + (ck! "no window: set-title! is quiet" (nil? (host/set-title! "x"))) | |
| 21 | + (ck! "no window: no clipboard picture" (false? (host/clipboard-image-png! "/tmp/nope.png"))) | |
| 22 | + (ck! "no chooser on a desktop" (false? (host/pick-image!))) | |
| 23 | + (ck! "and nothing ever chosen" (nil? (host/picked-image! "/tmp/nope.png"))) | |
| 24 | + | |
| 25 | + (app/run! | |
| 26 | + (fn [] | |
| 27 | + (swap! seen inc) | |
| 28 | + (when (= 1 @seen) | |
| 29 | + (ck! "a window is installed" (some? (host/window))) | |
| 30 | + (ck! "window-width is positive" (pos? (host/window-width))) | |
| 31 | + (let [[sw sh] (or (host/screen-size) [0 0])] | |
| 32 | + (ck! "screen-size answers a display" (and (pos? sw) (pos? sh)))) | |
| 33 | + (ck! "set-title! does not throw" (nil? (host/set-title! "retitled"))) | |
| 34 | + ;; No picture on this clipboard, so false — the path that matters | |
| 35 | + ;; is that asking is safe, not that a picture appears. | |
| 36 | + (ck! "clipboard with no picture is false" | |
| 37 | + (false? (host/clipboard-image-png! "/tmp/jvui-clip.png")))) | |
| 38 | + ;; Ask to close on the third frame; the loop is allowed six, so | |
| 39 | + ;; stopping earlier is the evidence. | |
| 40 | + (when (= 3 @seen) (host/quit!)) | |
| 41 | + (w/page* {} (fn [_ _] (w/label "host")))) | |
| 42 | + {:title "host" :width 200 :height 140 :frames 6}) | |
| 43 | + | |
| 44 | + (ck! "quit! stopped the loop early" (< @seen 6)) | |
| 45 | + (ck! "and the window is released" (nil? (host/window))) | |
| 46 | + | |
| 47 | + (doseq [[n ok?] @out] (println (if ok? "- " "FAIL ") n)) | |
| 48 | + (let [bad (remove second @out)] | |
| 49 | + (println (if (seq bad) | |
| 50 | + (str (count bad) " of " (count @out) " checks FAILED") | |
| 51 | + (str "all " (count @out) " checks passed"))) | |
| 52 | + (when (seq bad) (System/exit 1))))) | |
| new file mode 100644 | |||
| @@ -0,0 +1,52 @@ | |||
| 1 | +(ns jvui.host-check | ||
| 2 | + "The seven platform calls, against a real window. | ||
| 3 | + | ||
| 4 | + Under SDL's dummy video driver, which gives a window and a renderer and no | ||
| 5 | + display. That limits what can be claimed and the claims are limited to | ||
| 6 | + match: `open-url!` is not invoked, because succeeding would mean a browser | ||
| 7 | + opening on somebody's desktop and failing would only mean this container | ||
| 8 | + has none. Everything else is checked for real." | ||
| 9 | + (:require [jvui.app :as app] | ||
| 10 | + [jvui.host :as host] | ||
| 11 | + [jvui.widgets :as w])) | ||
| 12 | + | ||
| 13 | +(defn -main [& _] | ||
| 14 | + (let [out (atom []) | ||
| 15 | + ck! (fn [n ok?] (swap! out conj [n (boolean ok?)])) | ||
| 16 | + seen (atom 0)] | ||
| 17 | + ;; Before any window: everything must answer harmlessly rather than throw. | ||
| 18 | + (ck! "no window: screen-size is nil" (nil? (host/screen-size))) | ||
| 19 | + (ck! "no window: window-width is 0" (zero? (host/window-width))) | ||
| 20 | + (ck! "no window: set-title! is quiet" (nil? (host/set-title! "x"))) | ||
| 21 | + (ck! "no window: no clipboard picture" (false? (host/clipboard-image-png! "/tmp/nope.png"))) | ||
| 22 | + (ck! "no chooser on a desktop" (false? (host/pick-image!))) | ||
| 23 | + (ck! "and nothing ever chosen" (nil? (host/picked-image! "/tmp/nope.png"))) | ||
| 24 | + | ||
| 25 | + (app/run! | ||
| 26 | + (fn [] | ||
| 27 | + (swap! seen inc) | ||
| 28 | + (when (= 1 @seen) | ||
| 29 | + (ck! "a window is installed" (some? (host/window))) | ||
| 30 | + (ck! "window-width is positive" (pos? (host/window-width))) | ||
| 31 | + (let [[sw sh] (or (host/screen-size) [0 0])] | ||
| 32 | + (ck! "screen-size answers a display" (and (pos? sw) (pos? sh)))) | ||
| 33 | + (ck! "set-title! does not throw" (nil? (host/set-title! "retitled"))) | ||
| 34 | + ;; No picture on this clipboard, so false — the path that matters | ||
| 35 | + ;; is that asking is safe, not that a picture appears. | ||
| 36 | + (ck! "clipboard with no picture is false" | ||
| 37 | + (false? (host/clipboard-image-png! "/tmp/jvui-clip.png")))) | ||
| 38 | + ;; Ask to close on the third frame; the loop is allowed six, so | ||
| 39 | + ;; stopping earlier is the evidence. | ||
| 40 | + (when (= 3 @seen) (host/quit!)) | ||
| 41 | + (w/page* {} (fn [_ _] (w/label "host")))) | ||
| 42 | + {:title "host" :width 200 :height 140 :frames 6}) | ||
| 43 | + | ||
| 44 | + (ck! "quit! stopped the loop early" (< @seen 6)) | ||
| 45 | + (ck! "and the window is released" (nil? (host/window))) | ||
| 46 | + | ||
| 47 | + (doseq [[n ok?] @out] (println (if ok? "- " "FAIL ") n)) | ||
| 48 | + (let [bad (remove second @out)] | ||
| 49 | + (println (if (seq bad) | ||
| 50 | + (str (count bad) " of " (count @out) " checks FAILED") | ||
| 51 | + (str "all " (count @out) " checks passed"))) | ||
| 52 | + (when (seq bad) (System/exit 1))))) | ||