├── .gitignore ├── cider-reload.png ├── script ├── nrepl.sh └── run.sh ├── deps.edn ├── .clj-kondo └── io.github.humbleui │ └── humbleui │ └── config.edn ├── src └── town │ └── lilac │ └── humble │ └── app │ ├── main.clj │ └── state.clj ├── LICENSE ├── dev └── user.clj └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .clj-kondo/.cache/ 2 | /.cpcache/ 3 | -------------------------------------------------------------------------------- /cider-reload.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lilactown/humble-starter/HEAD/cider-reload.png -------------------------------------------------------------------------------- /script/nrepl.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -o errexit -o nounset -o pipefail 3 | cd "$(dirname "$0")/.." 4 | 5 | clj -M:dev -m user --interactive $@ 6 | -------------------------------------------------------------------------------- /script/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -o errexit -o nounset -o pipefail 3 | cd "$(dirname "$0")/.." 4 | 5 | clj -M -m town.lilac.humble.app.main $@ 6 | -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src"] 2 | :deps {io.github.humbleui/humbleui {:git/sha "3ecd348386dd7e24abb013a958ed40b6eb3510fd"}} 3 | :aliases 4 | {:dev {:extra-paths ["dev"] 5 | :extra-deps {nrepl/nrepl {:mvn/version "1.0.0"} 6 | org.clojure/tools.namespace {:mvn/version "1.3.0"}} 7 | :jvm-opts ["-ea"]} 8 | :cider {:main-opts ["-m" "user" "--middleware" "[cider.nrepl/cider-middleware]"]}}} 9 | -------------------------------------------------------------------------------- /.clj-kondo/io.github.humbleui/humbleui/config.edn: -------------------------------------------------------------------------------- 1 | {:hooks 2 | {:analyze-call 3 | {io.github.humbleui.ui/deflazy hooks.deflazy/deflazy-hook 4 | io.github.humbleui.util/loopr hooks.loopr/loopr-hook 5 | io.github.humbleui.util/condp hooks.condplus/condplus-hook}} 6 | :lint-as 7 | {io.github.humbleui.ui/defcomp clojure.core/defn 8 | io.github.humbleui.util/deftype+ clojure.core/deftype 9 | io.github.humbleui.util/for-map clojure.core/for 10 | io.github.humbleui.util/for-vec clojure.core/for 11 | io.github.humbleui.util/memo-fn clojure.core/fn 12 | io.github.humbleui.util/thread clojure.core/future 13 | io.github.humbleui.util/when-some+ clojure.core/when-some}} 14 | -------------------------------------------------------------------------------- /src/town/lilac/humble/app/main.clj: -------------------------------------------------------------------------------- 1 | (ns town.lilac.humble.app.main 2 | "The main app namespace. 3 | Responsible for initializing the window and app state when the app starts." 4 | (:require 5 | [io.github.humbleui.ui :as ui] 6 | ;; [io.github.humbleui.window :as window] 7 | [town.lilac.humble.app.state :as state]) 8 | (:import 9 | [io.github.humbleui.skija Color ColorSpace] 10 | [io.github.humbleui.jwm Window] 11 | [io.github.humbleui.jwm.skija LayerMetalSkija])) 12 | 13 | (def app 14 | "Main app definition." 15 | (fn [] 16 | [ui/default-theme ; we must wrap our app in a theme 17 | {} 18 | ;; just some random stuff 19 | [ui/center 20 | [ui/label "hi"]]])) 21 | 22 | ;; reset current app state on eval of this ns 23 | (reset! state/*app app) 24 | 25 | (defn -main 26 | "Run once on app start, starting the humble app." 27 | [& args] 28 | (ui/start-app! 29 | (reset! state/*window 30 | (ui/window 31 | {:title "Editor" 32 | :bg-color 0xFFFFFFFF} 33 | state/*app))) 34 | (state/redraw!)) 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Will Acton 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/town/lilac/humble/app/state.clj: -------------------------------------------------------------------------------- 1 | (ns ^{:clojure.tools.namespace.repl/load false} 2 | town.lilac.humble.app.state 3 | "This namespace holds global state that will be mutated during the duration 4 | of our application running. 5 | 6 | We separate it into its own namespace for two reasons: 7 | 8 | 1. It could be used throughout the rest of our app, and we want to avoid 9 | circular dependencies occurring, so we isolate it to its own ns. 10 | 11 | 2. We want to be able to tell clojure.tools.namespace to not reload this ns 12 | on refresh (if we use c.t.n), otherwise we will lose the reference we 13 | passed to `io.github.humbleui.ui/window` in app start, which will remove 14 | our ability to redraw it." 15 | (:require 16 | [io.github.humbleui.window :as window])) 17 | 18 | (def *window 19 | "State of the main window. Gets set on app startup." 20 | (atom nil)) 21 | 22 | (def *app 23 | "Current state of what's drawn in the main app window. 24 | Gets set any time we want to draw something new." 25 | (atom nil)) 26 | 27 | (defn redraw! 28 | "Redraws the window with the current app state." 29 | [] 30 | ;; we redraw only when window state has been set. 31 | ;; this lets us call the function on ns eval and will only 32 | ;; redraw if the window has already been created in either 33 | ;; user/-main or the app -main 34 | (some-> *window deref window/request-frame)) 35 | -------------------------------------------------------------------------------- /dev/user.clj: -------------------------------------------------------------------------------- 1 | (ns ^{:clojure.tools.namespace.repl/load false} 2 | user 3 | (:require 4 | [town.lilac.humble.app.main :as main] 5 | [town.lilac.humble.app.state :as state] 6 | [io.github.humbleui.app :as app] 7 | [io.github.humbleui.debug :as debug] 8 | [io.github.humbleui.window :as window] 9 | [nrepl.cmdline :as nrepl] 10 | [clojure.tools.namespace.repl :as ns])) 11 | 12 | (defn reset-window 13 | "Resets the window position and size back to some defaults." 14 | [] 15 | (app/doui 16 | (when-some [window @state/*window] 17 | (window/set-window-position window 860 566) 18 | (window/set-content-size window 1422 800) 19 | #_(window/set-z-order window :floating)))) 20 | 21 | (defn reload 22 | "Reload all namespaces that have changed on disk and redraw the app." 23 | [] 24 | (ns/refresh :after 'town.lilac.humble.app.state/redraw!)) 25 | 26 | (defn -main 27 | "Starts both the UI and the nREPL server." 28 | [& args] 29 | (ns/set-refresh-dirs "src") 30 | ;; start app 31 | (main/-main) 32 | 33 | ;; (reset! debug/*enabled? true) 34 | 35 | ;; start nREPL server (on another thread) 36 | (apply nrepl/-main args)) 37 | 38 | (comment 39 | ;; Anything we do to the app UI, we need to eval it wrapped in `doui` so that 40 | ;; it runs on the UI thread. 41 | (reload) 42 | (reset-window) 43 | 44 | ;; keep window on top even when not focused 45 | (app/doui 46 | (window/set-z-order @state/*window :floating)) 47 | 48 | ;; set window to hide normally when not focused 49 | (app/doui 50 | (window/set-z-order @state/*window :normal))) 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # humble-starter 2 | 3 | A commented, minimal example project to get you started with developing desktop 4 | apps using [HumbleUI](https://github.com/HumbleUI/HumbleUI) and Clojure. 5 | 6 | ## Development 7 | 8 | To simply run the app, execute `./script/run.sh`. 9 | 10 | To start a REPL (including a minimal nREPL server), you can run `./script/nrepl.sh` 11 | 12 | ### Reloading 13 | 14 | To reload the app and see your changes reflected, you can: 15 | 16 | 1. Evaluate individual forms via the REPL, reset the `state/*app` atom, and then 17 | call `state/redraw!` 18 | 2. Make changes to the files, save them, then call `reload` from the user ns, 19 | which will use [clojure.tools.namespace](https://github.com/clojure/tools.namespace) 20 | to detect which ns' should be refreshed, evaluate them, and then call 21 | `state/redraw!`. 22 | 23 | ### CIDER 24 | 25 | > TL;DR: Customize the jack in command, delete the `:cider/nrepl` alias at the 26 | > end of the command and replace it with `:dev:cider` 27 | 28 | If you use an editor like Emacs or Calva which integrates using CIDER, you can 29 | customize the jack-in command to work with your HumbleUI app. 30 | 31 | > NOTE: The default jack-in command will not work, since we need to start the 32 | > HumbleUI app on a different thread than the nREPL server. By default, the 33 | > nREPL server will start and then you would evaluate commands via this 34 | > connection, but this will not work when starting the HumbleUI app. 35 | 36 | To ensure that you are loading the correct version of nREPL and CIDER, we start 37 | by running the jack-in command but customizing it. In Emacs, this is 38 | `C-u M-x cider-jack-in`. An example of what the default command looks like: 39 | 40 | ``` 41 | /opt/homebrew/bin/clojure -Sdeps '{:deps {nrepl/nrepl {:mvn/version "1.0.0"} cider/cider-nrepl {:mvn/version "0.28.6"}} :aliases {:cider/nrepl {:main-opts ["-m" "nrepl.cmdline" "--middleware" "[cider.nrepl/cider-middleware]"]}}}' -M:cider/nrepl 42 | ``` 43 | 44 | For our purposes, the `user` ns has a `-main` function which handles all of the 45 | app and nREPL server initialization. The only thing we need to replace the call to 46 | CIDER's main with our own and pass in the middlewares to it. 47 | 48 | Below, we show the command after we delete the use of the `:cider/nrepl` alias 49 | and replace it with the `:dev:cider` alias configured in our deps.edn, which 50 | calls our custom `-main` function with the CIDER middlewares. 51 | 52 | ``` 53 | /opt/homebrew/bin/clojure -Sdeps '{:deps {nrepl/nrepl {:mvn/version "1.0.0"} cider/cider-nrepl {:mvn/version "0.28.6"}} :aliases {:cider/nrepl {:main-opts ["-m" "nrepl.cmdline" "--middleware" "[cider.nrepl/cider-middleware]"]}}}' -M:dev:cider 54 | ``` 55 | 56 | ![Emacs with CIDER connected and using reload](./cider-reload.png) 57 | 58 | ## Credit 59 | 60 | A lot of this code was copied and then modified from the HumbleUI codebase 61 | itself, as well as [humble-deck](https://github.com/tonsky/humble-deck/) and 62 | [humble-animations](https://github.com/oakmac/humble-animations). Thanks to 63 | @tonsky for developing HumbleUI and releasing so many cool examples, and @oakmac 64 | for showing me some cool stuff too! 65 | 66 | ## License & Copyright 67 | 68 | Licensed under MIT. Copyright Will Acton 2022. 69 | 70 | --------------------------------------------------------------------------------