├── traced-config ├── proxy-config.json ├── predefined-classes-config.json ├── serialization-config.json ├── resource-config.json ├── jni-config.json └── reflect-config.json ├── .cljfmt.edn ├── .gitignore ├── script ├── run.sh └── nrepl.sh ├── .clj-kondo └── config.edn ├── tests.edn ├── deps.edn ├── LICENSE ├── src └── humble_outliner │ ├── main.clj │ ├── theme.clj │ ├── state.clj │ ├── demo.clj │ ├── events.clj │ ├── views.clj │ └── model.clj ├── dev └── user.clj ├── test └── humble_outliner │ ├── helpers.clj │ ├── helpers_test.clj │ ├── events_test.clj │ └── model_test.clj └── README.md /traced-config/proxy-config.json: -------------------------------------------------------------------------------- 1 | [ 2 | ] 3 | -------------------------------------------------------------------------------- /.cljfmt.edn: -------------------------------------------------------------------------------- 1 | {:indents 2 | {#re "io\\.github\\.humbleui\\..*" [[:inner 0]]}} 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .clj-kondo/* 2 | !.clj-kondo/config.edn 3 | /.cpcache/ 4 | .nrepl-port 5 | /target 6 | -------------------------------------------------------------------------------- /script/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -o errexit -o nounset -o pipefail 3 | cd "$(dirname "$0")/.." 4 | 5 | clj -M -m humble-outliner.main $@ 6 | -------------------------------------------------------------------------------- /traced-config/predefined-classes-config.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "type":"agent-extracted", 4 | "classes":[ 5 | ] 6 | } 7 | ] 8 | 9 | -------------------------------------------------------------------------------- /traced-config/serialization-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "types":[ 3 | ], 4 | "lambdaCapturingTypes":[ 5 | ], 6 | "proxies":[ 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.clj-kondo/config.edn: -------------------------------------------------------------------------------- 1 | {:linters {:unresolved-var {:exclude [io.github.humbleui.ui]}} 2 | :lint-as {io.github.humbleui.core/deftype+ clojure.core/deftype}} 3 | -------------------------------------------------------------------------------- /tests.edn: -------------------------------------------------------------------------------- 1 | #kaocha/v1 2 | {:tests [{:id :unit 3 | ;; add src to test paths for co-located tests 4 | :test-paths [#_"src" "test"]}] 5 | :cloverage/opts {:ns-exclude-regex ["^.*-test$"]}} 6 | -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src"] 2 | :deps {;io.github.humbleui/humbleui {:git/sha "d7c2915b4900d34b727cd2006ff0f8cca1907242"} 3 | io.github.dundalek/humbleui {:git/sha "192cb4001a248feae4b247220ae7a1709035de90"}} 4 | 5 | :aliases 6 | {:dev {:extra-paths ["dev"] 7 | :extra-deps {nrepl/nrepl {:mvn/version "1.0.0"} 8 | org.clojure/tools.namespace {:mvn/version "1.3.0"}} 9 | :jvm-opts ["-ea"]} 10 | :cider {:main-opts ["-m" "user" "--middleware" "[cider.nrepl/cider-middleware]"]} 11 | 12 | :test {:main-opts ["-m" "kaocha.runner"] 13 | :extra-deps {lambdaisland/kaocha {:mvn/version "1.78.1249"}}} 14 | 15 | :coverage {:extra-deps {lambdaisland/kaocha-cloverage {:mvn/version "1.1.89"}} 16 | :main-opts ["-m" "kaocha.runner" "--plugin" "cloverage"]} 17 | 18 | :build {:deps {io.github.humbleui/jwm {:mvn/version "0.4.16"} ; using JWM during build time for platform detection 19 | io.github.clojure/tools.build {:mvn/version "0.9.4"} 20 | babashka/process {:mvn/version "0.5.21"}} 21 | :ns-default build}}} 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Jakub Dundalek 4 | Copyright (c) 2022 Will Acton 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /src/humble_outliner/main.clj: -------------------------------------------------------------------------------- 1 | (set! *warn-on-reflection* true) 2 | (ns humble-outliner.main 3 | "The main app namespace. 4 | Responsible for initializing the window and app state when the app starts." 5 | (:gen-class) 6 | (:require 7 | [humble-outliner.state :as state] 8 | [humble-outliner.views :as views] 9 | [io.github.humbleui.app :as app] 10 | [io.github.humbleui.ui :as ui])) 11 | 12 | (defn window [] 13 | (ui/window 14 | ;; Ideally, we would pass :bg-color option since window does canvas/clear. 15 | ;; But it does not seem to work to grab the theme from context via top-level ui/dynamic. 16 | ;; Therefore there is another canvas/clear in the `app` component that sets the background. 17 | {:title "Outliner"} 18 | state/*app)) 19 | 20 | ;; reset current app state on eval of this ns 21 | #_(reset! state/*app views/app) 22 | 23 | ;; Replacement for `ui/start-app!` that does not start a separate thread. 24 | ;; Workaround for a window not showing when compiled with Graal native image on macOS. 25 | (defmacro start-app! [& body] 26 | `(app/start 27 | (fn [] 28 | ~@body))) 29 | 30 | (defn -main 31 | "Run once on app start, starting the humble app." 32 | [& args] 33 | (reset! state/*app (views/app)) 34 | (start-app! 35 | (reset! state/*window (window))) 36 | (state/redraw!)) 37 | -------------------------------------------------------------------------------- /dev/user.clj: -------------------------------------------------------------------------------- 1 | (ns ^{:clojure.tools.namespace.repl/load false} 2 | user 3 | (:require 4 | [humble-outliner.main :as main] 5 | [humble-outliner.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 'humble-outliner.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 | -------------------------------------------------------------------------------- /src/humble_outliner/theme.clj: -------------------------------------------------------------------------------- 1 | (ns humble-outliner.theme 2 | (:require 3 | [io.github.humbleui.paint :as paint] 4 | [io.github.humbleui.ui :as ui])) 5 | 6 | (defn make-theme [opts] 7 | (assoc opts :cap-height 12)) 8 | 9 | (defn make-light [] 10 | (let [background-fill (paint/fill 0xFFFFFFFF)] 11 | (make-theme {:fill-text (paint/fill 0xFF000000) 12 | ::background-fill background-fill 13 | ::bullet-fill (paint/fill 0xFFCDCCCA) 14 | ::indentline-fill (paint/fill 0xFFEFEDEB) 15 | :hui.button/bg background-fill 16 | :hui.button/bg-hovered (paint/fill 0xFFE9E9E9) 17 | :hui.button/bg-active (paint/fill 0xFFF0F0F0)}))) 18 | 19 | (defn make-dark [] 20 | (let [background-fill (paint/fill 0xFF002B36)] 21 | (make-theme {:fill-text (paint/fill 0xFF93A1A1) 22 | ::background-fill background-fill 23 | ::bullet-fill (paint/fill 0xFF5A878B) 24 | ::indentline-fill (paint/fill 0xFF0B4A5A) 25 | :hui.button/bg background-fill 26 | :hui.button/bg-hovered (paint/fill 0xFF003C48) 27 | :hui.button/bg-active (paint/fill 0xFF003742)}))) 28 | 29 | (def *themes 30 | (delay {:light (make-light) 31 | :dark (make-dark)})) 32 | 33 | (def default-theme :light) 34 | 35 | (defn with-theme 36 | ([comp] (with-theme default-theme comp)) 37 | ([theme comp] 38 | (ui/default-theme (@*themes theme) comp))) 39 | 40 | (defn next-theme [current-theme] 41 | (if (= current-theme :light) 42 | :dark 43 | :light)) 44 | -------------------------------------------------------------------------------- /test/humble_outliner/helpers.clj: -------------------------------------------------------------------------------- 1 | (ns humble-outliner.helpers 2 | (:require 3 | [clojure.walk :as walk] 4 | [humble-outliner.model :as model])) 5 | 6 | (defn to-compact-impl 7 | [key-fn entities] 8 | (walk/prewalk 9 | (fn [x] 10 | (cond 11 | (vector? x) (->> x 12 | (mapcat (fn [{:keys [children] :as entity}] 13 | (cond-> [(key-fn entity)] 14 | (seq children) (conj children)))) 15 | (into [])) 16 | :else x)) 17 | (model/stratify entities))) 18 | 19 | (defn from-compact-impl 20 | ([kw items] 21 | (from-compact-impl kw items nil)) 22 | ([kw items parent-id] 23 | (loop [entities {} 24 | i 0 25 | [id maybe-children & other] items] 26 | (if id 27 | (let [entities (assoc entities id (cond-> {:order i} 28 | (not= kw :id) (assoc kw id) 29 | parent-id (assoc :parent parent-id)))] 30 | (if (vector? maybe-children) 31 | (recur (merge entities (from-compact-impl kw maybe-children id)) 32 | (inc i) 33 | other) 34 | (recur entities 35 | (inc i) 36 | (cons maybe-children other)))) 37 | entities)))) 38 | 39 | (def to-compact (partial to-compact-impl :id)) 40 | (def from-compact (partial from-compact-impl :id)) 41 | (def to-compact-by-text (partial to-compact-impl :text)) 42 | (def from-compact-by-text (partial from-compact-impl :text)) 43 | 44 | (defn update-compact [compact f & args] 45 | (to-compact (apply f (from-compact compact) args))) 46 | -------------------------------------------------------------------------------- /test/humble_outliner/helpers_test.clj: -------------------------------------------------------------------------------- 1 | (ns humble-outliner.helpers-test 2 | (:require 3 | [clojure.test :refer [are deftest is]] 4 | [humble-outliner.helpers :refer [to-compact to-compact-by-text from-compact from-compact-by-text update-compact]])) 5 | 6 | (deftest to-compact-test 7 | (is (= [4 5 6] 8 | (to-compact {6 {:order 2} 9 | 5 {:order 1} 10 | 4 {:order 0}}))) 11 | (is (= [1 12 | 2 [21 13 | 22 [221]] 14 | 3] 15 | (to-compact {1 {:order 0} 16 | 2 {:order 1} 17 | 21 {:order 0 :parent 2} 18 | 22 {:order 1 :parent 2} 19 | 221 {:order 0 :parent 22} 20 | 3 {:order 2}}))) 21 | (is (= ["four" "five" "six"] 22 | (to-compact-by-text {6 {:order 2 :text "six"} 23 | 5 {:order 1 :text "five"} 24 | 4 {:order 0 :text "four"}})))) 25 | 26 | (deftest from-compact-test 27 | (is (= {6 {:order 2} 28 | 5 {:order 1} 29 | 4 {:order 0}} 30 | (from-compact [4 5 6]))) 31 | 32 | (is (= {1 {:order 0} 33 | 2 {:order 1} 34 | 21 {:order 0 :parent 2} 35 | 22 {:order 1 :parent 2} 36 | 221 {:order 0 :parent 22} 37 | 3 {:order 2}} 38 | (from-compact [1 39 | 2 [21 40 | 22 [221]] 41 | 3]))) 42 | (is (= {"four" {:order 0 :text "four"} 43 | "five" {:order 1 :text "five"} 44 | "six" {:order 2 :text "six"}} 45 | (from-compact-by-text ["four" "five" "six"])))) 46 | 47 | (deftest update-compact-test 48 | (is (= [1 2] 49 | (update-compact [1 2 3] #(dissoc % 3))))) 50 | 51 | (deftest compact-round-trip 52 | (are [compact] (= compact (update-compact compact identity)) 53 | [] 54 | 55 | [1 2 3] 56 | 57 | [1 58 | 2 [21 59 | 22 [221]] 60 | 3]) 61 | 62 | (are [items] (= items (to-compact-by-text (from-compact-by-text items))) 63 | [] 64 | 65 | ["four" "five" "six"] 66 | 67 | ["1" 68 | "2" ["21" 69 | "22" ["221"]] 70 | "3"])) 71 | -------------------------------------------------------------------------------- /src/humble_outliner/state.clj: -------------------------------------------------------------------------------- 1 | (ns ^{:clojure.tools.namespace.repl/load false} 2 | humble-outliner.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 | [humble-outliner.model :as model] 17 | [humble-outliner.theme :as theme] 18 | [io.github.humbleui.window :as window])) 19 | 20 | (def default-db 21 | (let [entities 22 | {1 {:text ""}} 23 | #_{1 {:text "hello"} 24 | 2 {:text "world"} 25 | 3 {:text "abc"} 26 | 4 {:text "cdf" :parent 3}} 27 | #_(->> (range 50) 28 | (map (fn [i] 29 | [i {:text (str "Item " i)}])) 30 | (into {})) 31 | next-id (inc (reduce max (keys entities))) 32 | order (vec (sort (keys entities)))] 33 | (-> {:entities entities 34 | :next-id next-id 35 | :focused-id (first order) 36 | :input-states {} 37 | :theme theme/default-theme} 38 | (update :entities model/recalculate-entities-order order)))) 39 | 40 | (def *db 41 | (atom default-db)) 42 | 43 | (defn dispatch! [action] 44 | (swap! *db action) 45 | ;; return true for convenience to be used in event handlers to stop bubbling 46 | true) 47 | 48 | (comment 49 | (reset! *db default-db)) 50 | 51 | (def *window 52 | "State of the main window. Gets set on app startup." 53 | (atom nil)) 54 | 55 | (def *app 56 | "Current state of what's drawn in the main app window. 57 | Gets set any time we want to draw something new." 58 | (atom nil)) 59 | 60 | (defn redraw! 61 | "Redraws the window with the current app state." 62 | [] 63 | ;; we redraw only when window state has been set. 64 | ;; this lets us call the function on ns eval and will only 65 | ;; redraw if the window has already been created in either 66 | ;; user/-main or the app -main 67 | (some-> *window deref window/request-frame)) 68 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Humble Outliner 3 | 4 | This is a demo project implementing an outliner view inspired by [Logseq](https://logseq.com/) using [HumbleUI](https://github.com/HumbleUI/HumbleUI). 5 | It is based on the [humble-starter](https://github.com/lilactown/humble-starter) template. 6 | The focus of the demo is on the UI, it is not indented for use as it does not store data permanently. 7 | 8 | https://github.com/dundalek/humble-outliner/assets/755611/c8103408-84c6-491c-a253-d25c24b7189c 9 | 10 | ## Features 11 | 12 | - `up/down` go up/down 13 | - `enter` new item 14 | - `backspace` join items 15 | - `tab` indent 16 | - `shift+tab` outdent 17 | - `alt+shift+up/down` move item up/down 18 | - switch between light and dark theme 19 | 20 | ## Not Implemented 21 | 22 | Features they might be interesting to implement as an exercise in the future: 23 | 24 | - [ ] block-wise selection 25 | - [ ] collapse/expand 26 | - [ ] zoom in/out 27 | - [ ] undo/redo 28 | - [ ] copy/paste 29 | - [ ] dragging 30 | - [ ] text wrapping 31 | - [ ] rich text formatting 32 | 33 | ## Demo 34 | 35 | Run the app with: 36 | 37 | ```sh 38 | script/run.sh 39 | ``` 40 | 41 | ### Building executable with GraalVM native-image 42 | 43 | Make sure to have `GRAALVM_HOME` env variable pointing to your GraalVM installation, e.g.: 44 | ```sh 45 | export GRAALVM_HOME="$HOME/Downloads/graalvm-jdk-21.0.1+12.1" 46 | ``` 47 | 48 | Compile with: 49 | ```sh 50 | clj -T:build native 51 | ``` 52 | 53 | Run the executable: 54 | ```sh 55 | target/outliner 56 | ``` 57 | 58 | See [humble-graal](https://github.com/dundalek/humble-graal) for more details. 59 | 60 | ## Development 61 | 62 | Run the app including nREPL server: 63 | ```sh 64 | script/nrepl.sh 65 | ``` 66 | 67 | Run tests with: 68 | ```sh 69 | clj -M:test 70 | ``` 71 | 72 | Run tests with alternative reporter: 73 | ```sh 74 | clj -M:test --reporter kaocha.report/documentation 75 | ``` 76 | 77 | Run tests in watch mode: 78 | ```sh 79 | clj -M:test --watch 80 | ``` 81 | 82 | Generate code coverage report: 83 | ```sh 84 | clj -M:test:coverage 85 | ``` 86 | 87 | ### Reloading 88 | 89 | To reload the app and see your changes reflected, you can: 90 | 91 | 1. Evaluate individual forms via the REPL, reset the `state/*app` atom, and then 92 | call `state/redraw!` 93 | 2. Make changes to the files, save them, then call `reload` from the user ns, 94 | which will use [clojure.tools.namespace](https://github.com/clojure/tools.namespace) 95 | to detect which ns' should be refreshed, evaluate them, and then call 96 | `state/redraw!`. 97 | 98 | ## License 99 | 100 | Licensed under MIT. 101 | Copyright Jakub Dundalek 2023. 102 | Parts of the code Copyright Will Acton 2022. 103 | -------------------------------------------------------------------------------- /src/humble_outliner/demo.clj: -------------------------------------------------------------------------------- 1 | (ns humble-outliner.demo 2 | (:require 3 | [humble-outliner.events :as events] 4 | [humble-outliner.state :as state] 5 | [io.github.humbleui.core :as hcore])) 6 | 7 | ;; Pumping events for UI demo showcase. 8 | ;; Taking a lot of shortcuts and hardcoding, but seems that with more effort 9 | ;; it could also be viable to use an approach like this to drive integration tests. 10 | 11 | (defn dispatch-event! [event] 12 | (let [window @state/*window 13 | ctx {:window window 14 | :scale 1.0 #_(window/scale window) 15 | :mouse-pos (hcore/ipoint 0 0)}] 16 | (hcore/event @state/*app ctx event))) 17 | 18 | (defn press-key! 19 | ([key] (press-key! key #{})) 20 | ([key modifiers] 21 | ;; Note: not a full proper event, omitting :key-name, :key-types 22 | ;; Also modifiers should be pressed before and released after 23 | (let [event {:event :key 24 | :key key 25 | :modifiers modifiers 26 | :location :default}] 27 | (dispatch-event! (assoc event :pressed? true)) 28 | (dispatch-event! (assoc event :pressed? false))))) 29 | 30 | (defn input-text! [text] 31 | (dispatch-event! {:event :text-input 32 | :text text 33 | :replacement-start -1 34 | :replacement-end -1})) 35 | 36 | (def demo-steps 37 | [[input-text! "Outliner features"] 38 | [press-key! :enter] 39 | [press-key! :tab] 40 | [input-text! "up and down arrows to move between items"] 41 | [press-key! :enter] 42 | [press-key! :enter] 43 | [press-key! :enter] 44 | [press-key! :up] 45 | [press-key! :up] 46 | [input-text! "enter to add new items"] 47 | [press-key! :enter] 48 | [press-key! :tab] 49 | [press-key! :enter] 50 | [input-text! "abc"] 51 | [press-key! :enter] 52 | [input-text! "xyz"] 53 | [press-key! :enter] 54 | [press-key! :enter] 55 | [press-key! :tab #{:shift}] 56 | [input-text! "backspace to delete/join items"] 57 | [press-key! :up] 58 | [press-key! :backspace] 59 | [press-key! :home] 60 | [press-key! :backspace] 61 | [press-key! :home] 62 | [press-key! :backspace] 63 | [press-key! :down] 64 | [press-key! :end] 65 | [press-key! :enter] 66 | [input-text! "showcase dark theme"] 67 | [press-key! :enter] 68 | ;; cheating by dispatching event directly instead of mouse event on the button 69 | [state/dispatch! (events/theme-toggled)] 70 | [input-text! "tab to indent items"] 71 | [press-key! :tab] 72 | [press-key! :enter] 73 | [press-key! :tab] 74 | [input-text! "shift-tab to unindent"] 75 | [press-key! :tab #{:shift}] 76 | [press-key! :tab #{:shift}] 77 | [press-key! :end] 78 | [press-key! :enter] 79 | [input-text! "moving item"] 80 | [press-key! :enter] 81 | [press-key! :tab] 82 | [input-text! "alt+shift+up/down to move item up/down"] 83 | [press-key! :up #{:shift :alt}] 84 | [press-key! :up #{:shift :alt}] 85 | [press-key! :up #{:shift :alt}] 86 | [press-key! :down #{:shift :alt}] 87 | [press-key! :down #{:shift :alt}] 88 | [press-key! :down #{:shift :alt}] 89 | [press-key! :down] 90 | [press-key! :down] 91 | [input-text! "That's all for now!"]]) 92 | 93 | (defn play-demo! [] 94 | (let [delay-ms 600] 95 | (doseq [[f & args] demo-steps] 96 | (apply f args) 97 | (Thread/sleep delay-ms)))) 98 | -------------------------------------------------------------------------------- /src/humble_outliner/events.clj: -------------------------------------------------------------------------------- 1 | (ns humble-outliner.events 2 | (:require 3 | [clojure.string :as str] 4 | [humble-outliner.model :as model] 5 | [humble-outliner.theme :as theme] 6 | [io.github.humbleui.core :as core])) 7 | 8 | ;; Helpers 9 | 10 | (defn- update-input-state! [db id f & args] 11 | (apply update-in db [:input-states id] f args)) 12 | 13 | (defn- reset-input-blink-state! [db id] 14 | (update-in db [:input-states id] assoc :cursor-blink-pivot (core/now))) 15 | 16 | (defn- focus-item! 17 | ([db id] (focus-item! db id {:from 0 :to 0})) 18 | ([db id {:keys [from to]}] 19 | (-> db 20 | (update-input-state! id assoc 21 | :from from 22 | :to to 23 | ;; reset blink state on focus so that cursor is always visible when switching focus and does not "disappear" for brief moments 24 | :cursor-blink-pivot (core/now)) 25 | (assoc :focused-id id)))) 26 | 27 | (defn- switch-focus! [db id] 28 | (let [{:keys [input-states focused-id]} db 29 | {:keys [from]} (get input-states focused-id 0)] 30 | (focus-item! db id {:from from :to from}))) 31 | 32 | ;; Events 33 | 34 | ;; Experimenting with having event creator functions. The benefits are 35 | ;; possibility to add parameter validation and use static analysis (like clj-kondo) 36 | ;; to detect unused events. 37 | ;; In practice these would return a data representation with implementation 38 | ;; being separated. 39 | 40 | (defn focus-before [id] 41 | (fn [db] 42 | (if-some [focus-id (model/find-item-up (:entities db) id)] 43 | (switch-focus! db focus-id) 44 | db))) 45 | 46 | (defn focus-after [id] 47 | (fn [db] 48 | (if-some [focus-id (model/find-item-down (:entities db) id)] 49 | (switch-focus! db focus-id) 50 | db))) 51 | 52 | (defn item-input-focused [id] 53 | (fn [db] 54 | (-> db (assoc :focused-id id)))) 55 | 56 | (defn item-input-changed [id text] 57 | (fn [db] 58 | (-> db 59 | (update-in [:entities id] assoc :text text)))) 60 | 61 | (defn item-indented [id] 62 | (fn [{:keys [entities] :as db}] 63 | (let [new-entities (model/item-indent entities id)] 64 | (if (identical? entities new-entities) 65 | db 66 | (-> db 67 | (reset-input-blink-state! id) 68 | (assoc :entities new-entities)))))) 69 | 70 | (defn item-outdented [id] 71 | (fn [{:keys [entities] :as db}] 72 | (let [new-entities (model/item-outdent entities id)] 73 | (if (identical? entities new-entities) 74 | db 75 | (-> db 76 | (reset-input-blink-state! id) 77 | (assoc :entities new-entities)))))) 78 | 79 | (defn item-move-up [id] 80 | (fn [db] 81 | (update db :entities model/item-move-up id))) 82 | 83 | (defn item-move-down [id] 84 | (fn [db] 85 | (update db :entities model/item-move-down id))) 86 | 87 | (defn item-enter-pressed [target-id from] 88 | (fn [db] 89 | (let [{:keys [next-id]} db 90 | existing-text (get-in db [:entities target-id :text])] 91 | (if (pos? from) 92 | (let [new-current-text (subs existing-text 0 from) 93 | new-text (subs existing-text from)] 94 | (-> db 95 | (update :next-id inc) 96 | (model/set-item-text target-id new-current-text) 97 | (model/set-item-text next-id new-text) 98 | (update :entities model/item-add target-id next-id) 99 | (focus-item! next-id))) 100 | (let [parent-id (get-in db [:entities target-id :parent]) 101 | order (-> (model/get-children-order (:entities db) parent-id) 102 | (model/insert-before target-id next-id))] 103 | (-> db 104 | (update :next-id inc) 105 | (model/set-item-text next-id "") 106 | (assoc-in [:entities next-id :parent] parent-id) 107 | (update :entities model/recalculate-entities-order order) 108 | (reset-input-blink-state! target-id))))))) 109 | 110 | (defn item-beginning-backspace-pressed [item-id] 111 | (fn [db] 112 | (let [{:keys [entities]} db 113 | sibling-id (model/find-prev-sibling entities item-id) 114 | children-order (model/get-children-order entities item-id) 115 | merge-allowed? (or (zero? (count children-order)) 116 | (and sibling-id 117 | (zero? (count (model/get-children-order entities sibling-id)))))] 118 | (if-some [merge-target-id (when merge-allowed? 119 | (model/find-item-up entities item-id))] 120 | (let [text (get-in entities [item-id :text]) 121 | text-above (get-in entities [merge-target-id :text]) 122 | new-text-above (str (str/trimr text-above) text) 123 | new-cursor-position (count text-above)] 124 | (-> db 125 | (model/set-item-text merge-target-id new-text-above) 126 | (update :entities dissoc item-id) 127 | (update :entities model/reparent-items children-order merge-target-id) 128 | (focus-item! merge-target-id {:from new-cursor-position 129 | :to new-cursor-position}))) 130 | db)))) 131 | 132 | (defn theme-toggled [] 133 | (fn [db] 134 | (update db :theme theme/next-theme))) 135 | 136 | (defn clear-items [] 137 | (fn [db] 138 | (-> db 139 | (assoc :entities {1 {:text "" :order 0}}) 140 | (focus-item! 1)))) 141 | 142 | -------------------------------------------------------------------------------- /src/humble_outliner/views.clj: -------------------------------------------------------------------------------- 1 | (ns humble-outliner.views 2 | (:require 3 | [humble-outliner.demo :as demo] 4 | [humble-outliner.events :as events] 5 | [humble-outliner.model :as model] 6 | [humble-outliner.state :as state :refer [dispatch!]] 7 | [humble-outliner.theme :as theme] 8 | [io.github.humbleui.cursor :as cursor] 9 | [io.github.humbleui.ui :as ui] 10 | [io.github.humbleui.ui.clip :as clip] 11 | [io.github.humbleui.ui.focusable :as focusable] 12 | [io.github.humbleui.ui.listeners :as listeners] 13 | [io.github.humbleui.ui.with-cursor :as with-cursor])) 14 | 15 | ;; Components are couple to state directly, in practice we would likely use 16 | ;; some kind of subscriptions. Would it make sense to make the `*db` available 17 | ;; through context? 18 | 19 | (defn text-field [{:keys [id focused *state]}] 20 | (let [opts {:focused focused 21 | :on-focus (fn [] ; no parameters for on-focus 22 | (dispatch! (events/item-input-focused id))) 23 | :on-change (fn [{:keys [text]}] 24 | (dispatch! (events/item-input-changed id text)))} 25 | keymap {:enter #(dispatch! (events/item-enter-pressed id (:from @*state))) 26 | :up #(dispatch! (events/focus-before id)) 27 | :down #(dispatch! (events/focus-after id))}] 28 | (ui/with-context {:hui.text-field/cursor-blink-interval 500 29 | :hui.text-field/cursor-width 1 30 | :hui.text-field/padding-top (float 8) 31 | :hui.text-field/padding-bottom (float 8) 32 | :hui.text-field/padding-left (float 0) 33 | :hui.text-field/padding-right (float 0)} 34 | (focusable/focusable opts 35 | (listeners/event-listener {:capture? true} :key 36 | (fn [e ctx] 37 | (when (and (:hui/focused? ctx) (:pressed? e)) 38 | (cond 39 | (and (= :backspace (:key e)) 40 | (zero? (:from @*state)) 41 | (zero? (:to @*state))) 42 | (do (dispatch! (events/item-beginning-backspace-pressed id)) 43 | true) 44 | 45 | (and (= :tab (:key e)) 46 | (:shift (:modifiers e))) 47 | (dispatch! (events/item-outdented id)) 48 | 49 | (= :tab (:key e)) 50 | (dispatch! (events/item-indented id)) 51 | 52 | (and (= :up (:key e)) 53 | (= #{:shift :alt} (:modifiers e))) 54 | (dispatch! (events/item-move-up id)) 55 | 56 | (and (= :down (:key e)) 57 | (= #{:shift :alt} (:modifiers e))) 58 | (dispatch! (events/item-move-down id))))) 59 | 60 | (listeners/on-key-focused keymap 61 | (with-cursor/with-cursor :ibeam 62 | (ui/text-input opts *state)))))))) 63 | 64 | (def dot-size 6) 65 | 66 | (def dot-spacer 67 | (ui/gap dot-size dot-size)) 68 | 69 | (defn dot [] 70 | (ui/dynamic ctx [{::theme/keys [bullet-fill]} ctx] 71 | (ui/valign 0.5 72 | (clip/clip-rrect 3 73 | (ui/rect bullet-fill 74 | dot-spacer))))) 75 | 76 | (defn outline-item [id] 77 | (ui/dynamic _ [{:keys [focused-id]} @state/*db 78 | {:keys [text]} (get-in @state/*db [:entities id]) 79 | focused (= id focused-id) 80 | *state (cursor/cursor-in state/*db [:input-states id]) 81 | _ (swap! *state assoc :text text)] 82 | (ui/row 83 | (if (or focused (seq text)) 84 | (dot) 85 | dot-spacer) 86 | (ui/gap 12 0) 87 | (ui/width 500 88 | (text-field {:id id 89 | :focused focused 90 | :*state *state}))))) 91 | 92 | (defn indentline [] 93 | (ui/dynamic ctx [{::theme/keys [indentline-fill]} ctx] 94 | (ui/row 95 | ;; dot-size is 6px, with 1px line and 2px left gap the line is technically off center. 96 | ;; But if the dot size is an odd number and line centered, then it looks optically off. 97 | (ui/gap 2 0) 98 | (ui/rect indentline-fill 99 | (ui/gap 1 0))))) 100 | 101 | (defn outline-tree [items] 102 | (ui/row 103 | (ui/gap 24 0) 104 | (ui/column 105 | (for [{:keys [id children]} items] 106 | (ui/column 107 | (outline-item id) 108 | (when (seq children) 109 | (ui/row 110 | (indentline) 111 | (outline-tree children)))))))) 112 | 113 | (defn theme-switcher [] 114 | (ui/row 115 | [:stretch 1 nil] 116 | (ui/padding 6 117 | (ui/button #(dispatch! (events/theme-toggled)) 118 | (ui/label "Switch theme"))) 119 | (ui/padding 6 120 | (ui/button #(dispatch! (events/clear-items)) 121 | (ui/label "Clear items"))) 122 | (ui/padding 6 123 | (ui/button (fn [] 124 | ;; Good enough for demo, but potentially bad things waiting 125 | ;; to happen as the handlers will run on the driving thread. 126 | ;; Ideally, the events would be processed on the main UI thread. 127 | (.start (Thread. #(demo/play-demo!)))) 128 | (ui/label "Play Demo"))))) 129 | 130 | (defn app [] 131 | ; we must wrap our app in a theme 132 | (ui/dynamic _ [{:keys [theme]} @state/*db] 133 | (theme/with-theme 134 | theme 135 | (ui/dynamic ctx [{::theme/keys [background-fill]} ctx] 136 | (ui/rect background-fill 137 | (ui/column 138 | (theme-switcher) 139 | (ui/vscrollbar 140 | ;; We don't need top padding as there is a gap from the theme switcher. 141 | ;; There is an extra bottom padding to compesate, otherwise items 142 | ;; are cut off when scrolling, not sure why. 143 | (ui/padding 20 0 20 80 144 | (ui/column 145 | (ui/dynamic _ [items (->> (:entities @state/*db) 146 | (model/stratify))] 147 | (outline-tree items))))))))))) 148 | -------------------------------------------------------------------------------- /test/humble_outliner/events_test.clj: -------------------------------------------------------------------------------- 1 | (ns humble-outliner.events-test 2 | (:require 3 | [clojure.test :refer [deftest is testing]] 4 | [humble-outliner.events :as events] 5 | [humble-outliner.helpers :refer [to-compact to-compact-by-text from-compact from-compact-by-text]])) 6 | 7 | (deftest event-item-enter-pressed 8 | (let [entities (from-compact-by-text ["a" 9 | "b" ["xy" 10 | "bb"]]) 11 | db {:entities entities 12 | :focused-id "b" 13 | :next-id 1}] 14 | (testing "adds empty item when pressing enter at the beginning of item" 15 | (let [result ((events/item-enter-pressed "b" 0) db)] 16 | (is (= ["a" 17 | "" 18 | "b" ["xy" 19 | "bb"]] 20 | (to-compact-by-text (:entities result)))) 21 | (is (= "b" (get-in result [:entities (:focused-id result) :text]))))) 22 | (testing "adds child item when pressing enter at the end of a parent" 23 | (let [result ((events/item-enter-pressed "b" 1) db)] 24 | (is (= ["a" 25 | "b" ["" 26 | "xy" 27 | "bb"]] 28 | (to-compact-by-text (:entities result)))) 29 | (is (= "" (get-in result [:entities (:focused-id result) :text]))))) 30 | (testing "splits item when pressing enter somewhere in the middle" 31 | (let [result ((events/item-enter-pressed "xy" 1) db)] 32 | (is (= ["a" 33 | "b" ["x" 34 | "y" 35 | "bb"]] 36 | (to-compact-by-text (:entities result)))) 37 | (is (= "y" (get-in result [:entities (:focused-id result) :text]))))))) 38 | 39 | (deftest item-backspace 40 | (let [items [1 2 3] 41 | entities (-> (from-compact items) 42 | (assoc-in [1 :text] "abc") 43 | (assoc-in [2 :text] "def"))] 44 | (testing "without children" 45 | (testing "top-level has sibling before without children, merge into it" 46 | (let [result ((events/item-beginning-backspace-pressed 2) {:entities entities})] 47 | (is (= [1 3] (to-compact (:entities result)))) 48 | (is (= 1 (:focused-id result))) 49 | (is (= "abcdef" (get-in result [:entities 1 :text]))))) 50 | (testing "strips trailing spaces when merging item text" 51 | (let [entities (-> entities (assoc-in [1 :text] "abc ")) 52 | result ((events/item-beginning-backspace-pressed 2) {:entities entities})] 53 | (is (= [1 3] (to-compact (:entities result)))) 54 | (is (= 1 (:focused-id result))) 55 | (is (= "abcdef" (get-in result [:entities 1 :text]))))) 56 | (testing "nested has sibling before without children, merge into it" 57 | (let [items [1 [11 58 | 12 [121 59 | 122] 60 | 13] 61 | 2] 62 | entities (-> (from-compact items) 63 | (assoc-in [122 :text] "abc") 64 | (assoc-in [13 :text] "def")) 65 | result ((events/item-beginning-backspace-pressed 13) {:entities entities})] 66 | (is (= [1 [11 67 | 12 [121 68 | 122]] 69 | 2] 70 | (to-compact (:entities result)))) 71 | (is (= 122 (:focused-id result))) 72 | (is (= "abcdef" (get-in result [:entities 122 :text]))))) 73 | (testing "has sibling before with children, merge into last descendent (like focus up)" 74 | (let [items [1 [11 75 | 12 [121 76 | 122]] 77 | 2 78 | 3] 79 | entities (-> (from-compact items) 80 | (assoc-in [122 :text] "abc") 81 | (assoc-in [2 :text] "def")) 82 | result ((events/item-beginning-backspace-pressed 2) {:entities entities})] 83 | (is (= [1 [11 84 | 12 [121 85 | 122]] 86 | 3] 87 | (to-compact (:entities result)))) 88 | (is (= 122 (:focused-id result))) 89 | (is (= "abcdef" (get-in result [:entities 122 :text]))))) 90 | (testing "is first child, merge into parent" 91 | (let [items [1 [11 92 | 12] 93 | 2] 94 | entities (-> (from-compact items) 95 | (assoc-in [1 :text] "abc") 96 | (assoc-in [11 :text] "def")) 97 | result ((events/item-beginning-backspace-pressed 11) {:entities entities})] 98 | (is (= [1 [12] 99 | 2] 100 | (to-compact (:entities result)))) 101 | (is (= 1 (:focused-id result))) 102 | (is (= "abcdef" (get-in result [:entities 1 :text]))))) 103 | (testing "is first top-level item, noop" 104 | (is (= entities (:entities ((events/item-beginning-backspace-pressed 1) {:entities entities})))))) 105 | (testing "with children" 106 | (testing "has sibling before without children, merge into it and reparent children" 107 | (let [items [1 108 | 2 [21 109 | 22 [221 110 | 222] 111 | 23] 112 | 3] 113 | entities (-> (from-compact items) 114 | (assoc-in [1 :text] "abc1") 115 | (assoc-in [2 :text] "def1") 116 | (assoc-in [21 :text] "abc2") 117 | (assoc-in [22 :text] "def2"))] 118 | (testing "nested" 119 | (let [result ((events/item-beginning-backspace-pressed 22) {:entities entities})] 120 | (is (= [1 121 | 2 [21 [221 122 | 222] 123 | 23] 124 | 3] 125 | (to-compact (:entities result)))) 126 | (is (= 21 (:focused-id result))) 127 | (is (= "abc2def2" (get-in result [:entities 21 :text]))))) 128 | (testing "top-level" 129 | (let [result ((events/item-beginning-backspace-pressed 2) {:entities entities})] 130 | (is (= [1 [21 131 | 22 [221 132 | 222] 133 | 23] 134 | 3] 135 | (to-compact (:entities result)))) 136 | (is (= 1 (:focused-id result))) 137 | (is (= "abc1def1" (get-in result [:entities 1 :text]))))))) 138 | (testing "has sibling before with children, noop" 139 | (let [items [1 [11 140 | 12] 141 | 2 [21]] 142 | entities (-> (from-compact items)) 143 | result ((events/item-beginning-backspace-pressed 2) {:entities entities})] 144 | (is (= items (to-compact (:entities result)))))) 145 | (testing "is first child, noop" 146 | (let [items [1 [11 [111 112] 147 | 12]] 148 | entities (-> (from-compact items)) 149 | result ((events/item-beginning-backspace-pressed 11) {:entities entities})] 150 | (is (= items (to-compact (:entities result)))))) 151 | (testing "is first top-level item, noop" 152 | (let [items [1 [11 153 | 12]] 154 | entities (-> (from-compact items)) 155 | result ((events/item-beginning-backspace-pressed 1) {:entities entities})] 156 | (is (= items (to-compact (:entities result))))))))) 157 | -------------------------------------------------------------------------------- /src/humble_outliner/model.clj: -------------------------------------------------------------------------------- 1 | (ns humble-outliner.model) 2 | 3 | ;; Assumption the order is integer based and we re-number all children within 4 | ;; a level. In practice we would likely use fractional indexing like: 5 | ;; https://github.com/rocicorp/fractional-indexing 6 | 7 | (defn recalculate-entities-order [entities order] 8 | (reduce (fn [entities [order id]] 9 | (assoc-in entities [id :order] order)) 10 | entities 11 | (map-indexed list order))) 12 | 13 | (defn stratify 14 | ([entities] 15 | (stratify (group-by #(-> % val :parent) entities) nil)) 16 | ([parent->children id] 17 | (->> (parent->children id) 18 | (sort-by #(-> % val :order)) 19 | (map (fn [[id value]] 20 | (assoc value 21 | :id id 22 | :children (stratify parent->children id)))) 23 | (into [])))) 24 | 25 | (comment 26 | (stratify 27 | {3 {:order 2} 28 | 2 {:order 1} 29 | 1 {:order 0} 30 | 5 {:order 1 :parent 1} 31 | 4 {:order 0 :parent 1}})) 32 | 33 | (defn index-of [coll e] 34 | #_(first (keep-indexed #(when (= e %2) %1) coll)) 35 | (loop [i 0 36 | coll (seq coll)] 37 | (when (some? coll) 38 | (if (= e (first coll)) 39 | i 40 | (recur (inc i) (next coll)))))) 41 | 42 | (defn insert-after [v target value] 43 | (if-some [idx (some-> (index-of v target) inc)] 44 | (into (conj (subvec v 0 idx) value) 45 | (subvec v idx)) 46 | (conj v value))) 47 | 48 | (defn insert-before [v target value] 49 | (if-some [idx (index-of v target)] 50 | (into (conj (subvec v 0 idx) value) 51 | (subvec v idx)) 52 | (into [value] v))) 53 | 54 | (defn remove-at [v idx] 55 | (into (subvec v 0 idx) 56 | (subvec v (inc idx)))) 57 | 58 | (defn set-item-text [db id text] 59 | (-> db 60 | (update-in [:entities id] assoc :text text))) 61 | 62 | (defn get-children-order [entities parent-id] 63 | (->> entities 64 | (filter #(= parent-id (-> % val :parent))) 65 | (sort-by #(-> % val :order)) 66 | (map key) 67 | (into []))) 68 | 69 | (defn last-child [entities id] 70 | (some->> entities 71 | (filter #(= id (-> % val :parent))) 72 | (sort-by #(-> % val :order)) 73 | last 74 | key)) 75 | 76 | (defn first-child [entities id] 77 | (some->> entities 78 | (filter #(= id (-> % val :parent))) 79 | (sort-by #(-> % val :order)) 80 | first 81 | key)) 82 | 83 | (defn find-last-descendent [entities id] 84 | (loop [id id] 85 | (if-some [child-id (last-child entities id)] 86 | (recur child-id) 87 | id))) 88 | 89 | (defn find-prev-sibling [entities id] 90 | ;; can return nil 91 | (let [parent-id (get-in entities [id :parent]) 92 | order (get-children-order entities parent-id) 93 | idx (index-of order id)] 94 | (assert (some? idx)) 95 | (when (pos? idx) 96 | (get order (dec idx))))) 97 | 98 | (defn find-item-up [entities id] 99 | ;; can return nil 100 | (if-some [prev-id (find-prev-sibling entities id)] 101 | (find-last-descendent entities prev-id) 102 | (get-in entities [id :parent]))) 103 | 104 | (defn next-sibling [entities id] 105 | (let [parent-id (get-in entities [id :parent]) 106 | order (get-children-order entities parent-id) 107 | idx (index-of order id) 108 | _ (assert (some? idx)) 109 | last-item? (= idx (dec (count order)))] 110 | (when-not last-item? 111 | (get order (inc idx))))) 112 | 113 | (defn find-next-successor [entities id] 114 | (loop [id id] 115 | (if-some [sibling-id (next-sibling entities id)] 116 | sibling-id 117 | (when-some [parent-id (get-in entities [id :parent])] 118 | (recur parent-id))))) 119 | 120 | (defn find-item-down [entities id] 121 | ;; can return nil 122 | (if-some [child-id (first-child entities id)] 123 | child-id 124 | (find-next-successor entities id))) 125 | 126 | (defn reparent-items [entities item-ids new-parent-id] 127 | (reduce (fn [entities sibling-id] 128 | (update entities sibling-id assoc :parent new-parent-id)) 129 | entities 130 | item-ids)) 131 | 132 | (defn indent-following-siblings [entities id] 133 | (let [order (get-children-order entities (get-in entities [id :parent])) 134 | idx (index-of order id) 135 | following-siblings (subvec order (inc idx))] 136 | (reparent-items entities following-siblings id))) 137 | 138 | (defn item-add [entities target-id new-id] 139 | (let [order (get-children-order entities target-id) 140 | has-children? (seq order)] 141 | (if has-children? 142 | (-> entities 143 | (update new-id assoc :parent target-id) 144 | (recalculate-entities-order (into [new-id] order))) 145 | (let [parent-id (get-in entities [target-id :parent]) 146 | order (-> (get-children-order entities parent-id) 147 | (insert-after target-id new-id))] 148 | (-> entities 149 | (update new-id assoc :parent parent-id) 150 | (recalculate-entities-order order)))))) 151 | 152 | (defn item-move-up [entities id] 153 | (let [parent-id (get-in entities [id :parent]) 154 | order (get-children-order entities parent-id) 155 | idx (index-of order id) 156 | above-idx (dec idx) 157 | above-id (get order above-idx)] 158 | (if above-id 159 | (let [new-order (assoc order 160 | idx above-id 161 | above-idx id)] 162 | (recalculate-entities-order entities new-order)) 163 | (if-some [previous-parent-sibling (when parent-id 164 | (find-prev-sibling entities parent-id))] 165 | (let [order (-> (get-children-order entities previous-parent-sibling) 166 | (conj id))] 167 | (-> entities 168 | (assoc-in [id :parent] previous-parent-sibling) 169 | (recalculate-entities-order order))) 170 | entities)))) 171 | 172 | (defn item-move-down [entities id] 173 | (let [parent-id (get-in entities [id :parent]) 174 | order (get-children-order entities parent-id) 175 | idx (index-of order id) 176 | below-idx (inc idx) 177 | below-id (get order below-idx)] 178 | (if below-id 179 | (let [new-order (assoc order 180 | idx below-id 181 | below-idx id)] 182 | (recalculate-entities-order entities new-order)) 183 | (if-some [next-parent-sibling (when parent-id 184 | (next-sibling entities parent-id))] 185 | (let [order (into [id] (get-children-order entities next-parent-sibling))] 186 | (-> entities 187 | (assoc-in [id :parent] next-parent-sibling) 188 | (recalculate-entities-order order))) 189 | entities)))) 190 | 191 | (defn item-indent [entities id] 192 | (let [parent-id (get-in entities [id :parent]) 193 | order (get-children-order entities parent-id) 194 | above-idx (dec (index-of order id)) 195 | above-id (get order above-idx)] 196 | (if above-id 197 | (let [last-order (inc (->> entities 198 | (filter #(= above-id (-> % val :parent))) 199 | (map #(-> % val :order)) 200 | (reduce max -1)))] 201 | (update entities id assoc 202 | :parent above-id 203 | :order last-order)) 204 | entities))) 205 | 206 | (defn item-outdent [entities id] 207 | (if-some [parent-id (get-in entities [id :parent])] 208 | (let [grad-parent-id (get-in entities [parent-id :parent]) 209 | order (-> (get-children-order entities grad-parent-id) 210 | (insert-after parent-id id))] 211 | (-> entities 212 | (indent-following-siblings id) 213 | (assoc-in [id :parent] grad-parent-id) 214 | (recalculate-entities-order order))) 215 | entities)) 216 | -------------------------------------------------------------------------------- /test/humble_outliner/model_test.clj: -------------------------------------------------------------------------------- 1 | (ns humble-outliner.model-test 2 | (:require 3 | [clojure.test :refer [deftest is testing]] 4 | [humble-outliner.helpers :refer [from-compact update-compact]] 5 | [humble-outliner.model :as model])) 6 | 7 | (deftest index-of 8 | (is (= 1 (model/index-of [4 5 6] 5))) 9 | (is (nil? (model/index-of [4 5 6] 42)))) 10 | 11 | (deftest remove-at 12 | (is (= [5 6] (model/remove-at [4 5 6] 0))) 13 | (is (= [4 6] (model/remove-at [4 5 6] 1))) 14 | (is (= [4 5] (model/remove-at [4 5 6] 2))) 15 | (is (thrown? java.lang.IndexOutOfBoundsException (model/remove-at [4 5 6] 3)))) 16 | 17 | (deftest insert-after 18 | (is (= [4 42 5 6] (model/insert-after [4 5 6] 4 42))) 19 | (is (= [4 5 6 42] (model/insert-after [4 5 6] 6 42))) 20 | (testing "if no item then append" 21 | (is (= [4 5 6 42] (model/insert-after [4 5 6] 7 42))))) 22 | 23 | (deftest insert-before 24 | (is (= [42 4 5 6] (model/insert-before [4 5 6] 4 42))) 25 | (is (= [4 5 42 6] (model/insert-before [4 5 6] 6 42))) 26 | (testing "if no item then prepend" 27 | (is (= [42 4 5 6] (model/insert-before [4 5 6] 7 42))))) 28 | 29 | (deftest recalculate-entities-order 30 | (let [entities {6 {:order 2} 31 | 5 {:order 1} 32 | 4 {:order 0}} 33 | entities' (model/recalculate-entities-order entities [6 5 4])] 34 | (is (= entities' {6 {:order 0} 35 | 5 {:order 1} 36 | 4 {:order 2}})) 37 | 38 | (is (= [4 5 6] (model/get-children-order entities nil))) 39 | (is (= [6 5 4] (model/get-children-order entities' nil))))) 40 | 41 | (deftest get-children-order 42 | (let [entities {7 {:order 1 :parent 4} 43 | 6 {:order 1} 44 | 5 {:order 0 :parent 4} 45 | 4 {:order 0}}] 46 | (is (= [4 6] (model/get-children-order entities nil))) 47 | (is (= [5 7] (model/get-children-order entities 4))))) 48 | 49 | (deftest item-indent 50 | (let [entities 51 | {6 {:order 2} 52 | 5 {:order 1} 53 | 4 {:order 0}}] 54 | 55 | (is (= {6 {:order 2} 56 | 5 {:order 0 :parent 4} 57 | 4 {:order 0}} 58 | (model/item-indent entities 5))) 59 | 60 | (is (= {6 {:order 1 :parent 4} 61 | 5 {:order 0 :parent 4} 62 | 4 {:order 0}} 63 | (-> entities 64 | (model/item-indent 5) 65 | (model/item-indent 6)))) 66 | 67 | (is (= {6 {:order 0 :parent 5} 68 | 5 {:order 0 :parent 4} 69 | 4 {:order 0}} 70 | (-> entities 71 | (model/item-indent 5) 72 | (model/item-indent 6) 73 | (model/item-indent 6)))) 74 | 75 | (testing "noop indenting first item" 76 | (is (= entities (model/item-indent entities 4)))) 77 | 78 | (testing "noop indenting first sub-item" 79 | (is (= (model/item-indent entities 5) 80 | (-> entities 81 | (model/item-indent 5) 82 | (model/item-indent 5))))))) 83 | 84 | (deftest item-outdent 85 | (let [items [1 86 | 2 [21 87 | 22 [221]] 88 | 3]] 89 | (testing "noop outdenting top-level item" 90 | (is (= items (update-compact items model/item-outdent 2)))) 91 | 92 | (testing "straightforward outdenting last item" 93 | (is (= [1 94 | 2 [21 95 | 22 96 | 221] 97 | 3] 98 | (update-compact items model/item-outdent 221)))) 99 | 100 | (testing "outdenting reparents siblings" 101 | (is (= [1 102 | 2 103 | 21 [22 [221]] 104 | 3] 105 | (update-compact items model/item-outdent 21)))))) 106 | 107 | (deftest last-child 108 | (let [entities {6 {:order 2} 109 | 5 {:order 0 :parent 4} 110 | 4 {:order 0}}] 111 | (is (= 6 (model/last-child entities nil))) 112 | (is (= 5 (model/last-child entities 4))) 113 | (is (= nil (model/last-child entities 6))))) 114 | 115 | (deftest find-item-up 116 | (let [entities (from-compact 117 | [1 118 | 2 [21 119 | 22 [221]] 120 | 3])] 121 | (testing "focus previous top-level" 122 | (is (= 1 (model/find-item-up entities 2)))) 123 | 124 | (testing "focus previous nested" 125 | (is (= 21 (model/find-item-up entities 22)))) 126 | 127 | (testing "focus parent of nested first item" 128 | (is (= 2 (model/find-item-up entities 21)))) 129 | 130 | (testing "if previous item has children it focuses last child" 131 | (is (= 221 (model/find-item-up entities 3)))) 132 | 133 | (testing "no item before first top-level item" 134 | (is (= nil (model/find-item-up entities 1)))))) 135 | 136 | (deftest find-item-down 137 | (let [entities (from-compact 138 | [1 139 | 2 [21 140 | 22 [221]] 141 | 3])] 142 | (testing "focus next top-level" 143 | (is (= 2 (model/find-item-down entities 1)))) 144 | 145 | (testing "focus first nested child" 146 | (is (= 21 (model/find-item-down entities 2)))) 147 | 148 | (testing "focus next nested item" 149 | (is (= 22 (model/find-item-down entities 21)))) 150 | 151 | (testing "focus next sibling for last child" 152 | (is (= 3 (model/find-item-down entities 221)))) 153 | 154 | (testing "no next item for last top-level item" 155 | (is (= nil (model/find-item-down entities 3)))))) 156 | 157 | (deftest item-add 158 | (let [items [1 159 | 2 [21]]] 160 | (testing "adding to childless item adds as sibling" 161 | (is (= [1 162 | 7 163 | 2 [21]] 164 | (update-compact items model/item-add 1 7))) 165 | (is (= [1 166 | 2 [21 167 | 7]] 168 | (update-compact items model/item-add 21 7)))) 169 | 170 | (testing "adding to item with children adds as a first child" 171 | (is (= [1 172 | 2 173 | [7 174 | 21]] 175 | (update-compact items model/item-add 2 7)))))) 176 | 177 | (deftest item-move-up 178 | (let [items [1 [11 179 | 12] 180 | 2 [21 181 | 22 [223]]]] 182 | (testing "moving top level item" 183 | (is (= [2 [21 184 | 22 [223]] 185 | 1 [11 186 | 12]] 187 | (update-compact items model/item-move-up 2)))) 188 | (testing "moving nested item within same parent" 189 | (is (= [1 [11 190 | 12] 191 | 2 [22 [223] 192 | 21]] 193 | (update-compact items model/item-move-up 22)))) 194 | (testing "moving nested item that keeps indentation level but changes parent" 195 | (is (= [1 [11 196 | 12 197 | 21] 198 | 2 [22 [223]]] 199 | (update-compact items model/item-move-up 21)))) 200 | (testing "moving nested item that keeps indentation level but changes parent" 201 | (let [expected-items [1 [11 202 | 12] 203 | 2 [21 [223] 204 | 22]]] 205 | (is (= expected-items (update-compact items model/item-move-up 223))) 206 | (testing "moving nested item that can't keep indent level does nothing" 207 | (is (= expected-items (-> items 208 | (update-compact model/item-move-up 223) 209 | (update-compact model/item-move-up 223))))))) 210 | (testing "moving first top level item up does nothing" 211 | (is (= items (update-compact items model/item-move-up 1)))) 212 | (testing "moving nested item that can't keep indent level does nothing" 213 | (is (= items (update-compact items model/item-move-up 11)))))) 214 | 215 | (deftest item-move-down 216 | (let [items [1 [11 217 | 12] 218 | 2 [21 219 | 22 [223]]]] 220 | (testing "moving top level item" 221 | (is (= [2 [21 222 | 22 [223]] 223 | 1 [11 224 | 12]] 225 | (update-compact items model/item-move-down 1)))) 226 | (testing "moving nested item within same parent" 227 | (is (= [1 [11 228 | 12] 229 | 2 [22 [223] 230 | 21]] 231 | (update-compact items model/item-move-down 21)))) 232 | (testing "moving nested item that keeps indentation level but changes parent" 233 | (is (= [1 [11] 234 | 2 [12 235 | 21 236 | 22 [223]]] 237 | (update-compact items model/item-move-down 12)))) 238 | (testing "moving nested item that keeps indentation level but changes parent" 239 | (let [items [1 [11 240 | 12] 241 | 2 [21 [223] 242 | 22]] 243 | expected-items [1 [11 244 | 12] 245 | 2 [21 246 | 22 [223]]]] 247 | (is (= expected-items (update-compact items model/item-move-down 223))) 248 | (testing "moving nested item that can't keep indent level does nothing" 249 | (is (= expected-items (-> items 250 | (update-compact model/item-move-down 223) 251 | (update-compact model/item-move-down 223))))))) 252 | (testing "moving last top level item up does nothing" 253 | (is (= items (update-compact items model/item-move-down 2)))) 254 | (testing "moving nested item that can't keep indent level does nothing" 255 | (is (= items (update-compact items model/item-move-down 22))) 256 | (is (= items (update-compact items model/item-move-down 223)))))) 257 | -------------------------------------------------------------------------------- /traced-config/resource-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "resources":{ 3 | "includes":[{ 4 | "pattern":"\\QMETA-INF/services/java.lang.System$LoggerFinder\\E" 5 | }, { 6 | "pattern":"\\QMETA-INF/services/java.time.zone.ZoneRulesProvider\\E" 7 | }, { 8 | "pattern":"\\Qclojure/core.clj\\E" 9 | }, { 10 | "pattern":"\\Qclojure/core/protocols.clj\\E" 11 | }, { 12 | "pattern":"\\Qclojure/core/protocols__init.class\\E" 13 | }, { 14 | "pattern":"\\Qclojure/core/server.clj\\E" 15 | }, { 16 | "pattern":"\\Qclojure/core/server__init.class\\E" 17 | }, { 18 | "pattern":"\\Qclojure/core__init.class\\E" 19 | }, { 20 | "pattern":"\\Qclojure/core_deftype.clj\\E" 21 | }, { 22 | "pattern":"\\Qclojure/core_deftype__init.class\\E" 23 | }, { 24 | "pattern":"\\Qclojure/core_print.clj\\E" 25 | }, { 26 | "pattern":"\\Qclojure/core_print__init.class\\E" 27 | }, { 28 | "pattern":"\\Qclojure/core_proxy.clj\\E" 29 | }, { 30 | "pattern":"\\Qclojure/core_proxy__init.class\\E" 31 | }, { 32 | "pattern":"\\Qclojure/edn.clj\\E" 33 | }, { 34 | "pattern":"\\Qclojure/edn__init.class\\E" 35 | }, { 36 | "pattern":"\\Qclojure/genclass.clj\\E" 37 | }, { 38 | "pattern":"\\Qclojure/genclass__init.class\\E" 39 | }, { 40 | "pattern":"\\Qclojure/gvec.clj\\E" 41 | }, { 42 | "pattern":"\\Qclojure/gvec__init.class\\E" 43 | }, { 44 | "pattern":"\\Qclojure/instant.clj\\E" 45 | }, { 46 | "pattern":"\\Qclojure/instant__init.class\\E" 47 | }, { 48 | "pattern":"\\Qclojure/java/io.clj\\E" 49 | }, { 50 | "pattern":"\\Qclojure/java/io__init.class\\E" 51 | }, { 52 | "pattern":"\\Qclojure/main.clj\\E" 53 | }, { 54 | "pattern":"\\Qclojure/main__init.class\\E" 55 | }, { 56 | "pattern":"\\Qclojure/math.clj\\E" 57 | }, { 58 | "pattern":"\\Qclojure/math__init.class\\E" 59 | }, { 60 | "pattern":"\\Qclojure/set.clj\\E" 61 | }, { 62 | "pattern":"\\Qclojure/set__init.class\\E" 63 | }, { 64 | "pattern":"\\Qclojure/spec/alpha.clj\\E" 65 | }, { 66 | "pattern":"\\Qclojure/spec/alpha__init.class\\E" 67 | }, { 68 | "pattern":"\\Qclojure/spec/gen/alpha.clj\\E" 69 | }, { 70 | "pattern":"\\Qclojure/spec/gen/alpha__init.class\\E" 71 | }, { 72 | "pattern":"\\Qclojure/stacktrace.clj\\E" 73 | }, { 74 | "pattern":"\\Qclojure/stacktrace__init.class\\E" 75 | }, { 76 | "pattern":"\\Qclojure/string.clj\\E" 77 | }, { 78 | "pattern":"\\Qclojure/string__init.class\\E" 79 | }, { 80 | "pattern":"\\Qclojure/uuid.clj\\E" 81 | }, { 82 | "pattern":"\\Qclojure/uuid__init.class\\E" 83 | }, { 84 | "pattern":"\\Qclojure/version.properties\\E" 85 | }, { 86 | "pattern":"\\Qclojure/walk.clj\\E" 87 | }, { 88 | "pattern":"\\Qclojure/walk__init.class\\E" 89 | }, { 90 | "pattern":"\\Qdata_readers.clj\\E" 91 | }, { 92 | "pattern":"\\Qdata_readers.cljc\\E" 93 | }, { 94 | "pattern":"\\Qhumble_outliner/demo.clj\\E" 95 | }, { 96 | "pattern":"\\Qhumble_outliner/demo__init.class\\E" 97 | }, { 98 | "pattern":"\\Qhumble_outliner/events.clj\\E" 99 | }, { 100 | "pattern":"\\Qhumble_outliner/events__init.class\\E" 101 | }, { 102 | "pattern":"\\Qhumble_outliner/main.clj\\E" 103 | }, { 104 | "pattern":"\\Qhumble_outliner/main__init.class\\E" 105 | }, { 106 | "pattern":"\\Qhumble_outliner/model.clj\\E" 107 | }, { 108 | "pattern":"\\Qhumble_outliner/model__init.class\\E" 109 | }, { 110 | "pattern":"\\Qhumble_outliner/state.clj\\E" 111 | }, { 112 | "pattern":"\\Qhumble_outliner/state__init.class\\E" 113 | }, { 114 | "pattern":"\\Qhumble_outliner/theme.clj\\E" 115 | }, { 116 | "pattern":"\\Qhumble_outliner/theme__init.class\\E" 117 | }, { 118 | "pattern":"\\Qhumble_outliner/views.clj\\E" 119 | }, { 120 | "pattern":"\\Qhumble_outliner/views__init.class\\E" 121 | }, { 122 | "pattern":"\\Qio/github/humbleui/app.clj\\E" 123 | }, { 124 | "pattern":"\\Qio/github/humbleui/app__init.class\\E" 125 | }, { 126 | "pattern":"\\Qio/github/humbleui/canvas.clj\\E" 127 | }, { 128 | "pattern":"\\Qio/github/humbleui/canvas__init.class\\E" 129 | }, { 130 | "pattern":"\\Qio/github/humbleui/clipboard.clj\\E" 131 | }, { 132 | "pattern":"\\Qio/github/humbleui/clipboard__init.class\\E" 133 | }, { 134 | "pattern":"\\Qio/github/humbleui/core.clj\\E" 135 | }, { 136 | "pattern":"\\Qio/github/humbleui/core__init.class\\E" 137 | }, { 138 | "pattern":"\\Qio/github/humbleui/cursor.clj\\E" 139 | }, { 140 | "pattern":"\\Qio/github/humbleui/cursor__init.class\\E" 141 | }, { 142 | "pattern":"\\Qio/github/humbleui/debug.clj\\E" 143 | }, { 144 | "pattern":"\\Qio/github/humbleui/debug__init.class\\E" 145 | }, { 146 | "pattern":"\\Qio/github/humbleui/error.clj\\E" 147 | }, { 148 | "pattern":"\\Qio/github/humbleui/error__init.class\\E" 149 | }, { 150 | "pattern":"\\Qio/github/humbleui/event.clj\\E" 151 | }, { 152 | "pattern":"\\Qio/github/humbleui/event__init.class\\E" 153 | }, { 154 | "pattern":"\\Qio/github/humbleui/font.clj\\E" 155 | }, { 156 | "pattern":"\\Qio/github/humbleui/font__init.class\\E" 157 | }, { 158 | "pattern":"\\Qio/github/humbleui/fonts/Inter-Regular.ttf\\E" 159 | }, { 160 | "pattern":"\\Qio/github/humbleui/paint.clj\\E" 161 | }, { 162 | "pattern":"\\Qio/github/humbleui/paint__init.class\\E" 163 | }, { 164 | "pattern":"\\Qio/github/humbleui/protocols.clj\\E" 165 | }, { 166 | "pattern":"\\Qio/github/humbleui/protocols__init.class\\E" 167 | }, { 168 | "pattern":"\\Qio/github/humbleui/skija/impl/Library.class\\E" 169 | }, { 170 | "pattern":"\\Qio/github/humbleui/skija/linux/x64/libskija.so\\E" 171 | }, { 172 | "pattern":"\\Qio/github/humbleui/skija/linux/x64/skija.version\\E" 173 | }, { 174 | "pattern":"\\Qio/github/humbleui/typeface.clj\\E" 175 | }, { 176 | "pattern":"\\Qio/github/humbleui/typeface__init.class\\E" 177 | }, { 178 | "pattern":"\\Qio/github/humbleui/ui.clj\\E" 179 | }, { 180 | "pattern":"\\Qio/github/humbleui/ui/align.clj\\E" 181 | }, { 182 | "pattern":"\\Qio/github/humbleui/ui/align__init.class\\E" 183 | }, { 184 | "pattern":"\\Qio/github/humbleui/ui/animation.clj\\E" 185 | }, { 186 | "pattern":"\\Qio/github/humbleui/ui/animation__init.class\\E" 187 | }, { 188 | "pattern":"\\Qio/github/humbleui/ui/backdrop.clj\\E" 189 | }, { 190 | "pattern":"\\Qio/github/humbleui/ui/backdrop__init.class\\E" 191 | }, { 192 | "pattern":"\\Qio/github/humbleui/ui/button.clj\\E" 193 | }, { 194 | "pattern":"\\Qio/github/humbleui/ui/button__init.class\\E" 195 | }, { 196 | "pattern":"\\Qio/github/humbleui/ui/canvas.clj\\E" 197 | }, { 198 | "pattern":"\\Qio/github/humbleui/ui/canvas__init.class\\E" 199 | }, { 200 | "pattern":"\\Qio/github/humbleui/ui/checkbox.clj\\E" 201 | }, { 202 | "pattern":"\\Qio/github/humbleui/ui/checkbox__init.class\\E" 203 | }, { 204 | "pattern":"\\Qio/github/humbleui/ui/clickable.clj\\E" 205 | }, { 206 | "pattern":"\\Qio/github/humbleui/ui/clickable__init.class\\E" 207 | }, { 208 | "pattern":"\\Qio/github/humbleui/ui/clip.clj\\E" 209 | }, { 210 | "pattern":"\\Qio/github/humbleui/ui/clip__init.class\\E" 211 | }, { 212 | "pattern":"\\Qio/github/humbleui/ui/containers.clj\\E" 213 | }, { 214 | "pattern":"\\Qio/github/humbleui/ui/containers__init.class\\E" 215 | }, { 216 | "pattern":"\\Qio/github/humbleui/ui/draggable.clj\\E" 217 | }, { 218 | "pattern":"\\Qio/github/humbleui/ui/draggable__init.class\\E" 219 | }, { 220 | "pattern":"\\Qio/github/humbleui/ui/dynamic.clj\\E" 221 | }, { 222 | "pattern":"\\Qio/github/humbleui/ui/dynamic__init.class\\E" 223 | }, { 224 | "pattern":"\\Qio/github/humbleui/ui/focusable.clj\\E" 225 | }, { 226 | "pattern":"\\Qio/github/humbleui/ui/focusable__init.class\\E" 227 | }, { 228 | "pattern":"\\Qio/github/humbleui/ui/gap.clj\\E" 229 | }, { 230 | "pattern":"\\Qio/github/humbleui/ui/gap__init.class\\E" 231 | }, { 232 | "pattern":"\\Qio/github/humbleui/ui/grid.clj\\E" 233 | }, { 234 | "pattern":"\\Qio/github/humbleui/ui/grid__init.class\\E" 235 | }, { 236 | "pattern":"\\Qio/github/humbleui/ui/hoverable.clj\\E" 237 | }, { 238 | "pattern":"\\Qio/github/humbleui/ui/hoverable__init.class\\E" 239 | }, { 240 | "pattern":"\\Qio/github/humbleui/ui/image.clj\\E" 241 | }, { 242 | "pattern":"\\Qio/github/humbleui/ui/image__init.class\\E" 243 | }, { 244 | "pattern":"\\Qio/github/humbleui/ui/image_snapshot.clj\\E" 245 | }, { 246 | "pattern":"\\Qio/github/humbleui/ui/image_snapshot__init.class\\E" 247 | }, { 248 | "pattern":"\\Qio/github/humbleui/ui/label.clj\\E" 249 | }, { 250 | "pattern":"\\Qio/github/humbleui/ui/label__init.class\\E" 251 | }, { 252 | "pattern":"\\Qio/github/humbleui/ui/listeners.clj\\E" 253 | }, { 254 | "pattern":"\\Qio/github/humbleui/ui/listeners__init.class\\E" 255 | }, { 256 | "pattern":"\\Qio/github/humbleui/ui/padding.clj\\E" 257 | }, { 258 | "pattern":"\\Qio/github/humbleui/ui/padding__init.class\\E" 259 | }, { 260 | "pattern":"\\Qio/github/humbleui/ui/paragraph.clj\\E" 261 | }, { 262 | "pattern":"\\Qio/github/humbleui/ui/paragraph__init.class\\E" 263 | }, { 264 | "pattern":"\\Qio/github/humbleui/ui/rect.clj\\E" 265 | }, { 266 | "pattern":"\\Qio/github/humbleui/ui/rect__init.class\\E" 267 | }, { 268 | "pattern":"\\Qio/github/humbleui/ui/scroll.clj\\E" 269 | }, { 270 | "pattern":"\\Qio/github/humbleui/ui/scroll__init.class\\E" 271 | }, { 272 | "pattern":"\\Qio/github/humbleui/ui/shadow.clj\\E" 273 | }, { 274 | "pattern":"\\Qio/github/humbleui/ui/shadow__init.class\\E" 275 | }, { 276 | "pattern":"\\Qio/github/humbleui/ui/sizing.clj\\E" 277 | }, { 278 | "pattern":"\\Qio/github/humbleui/ui/sizing__init.class\\E" 279 | }, { 280 | "pattern":"\\Qio/github/humbleui/ui/slider.clj\\E" 281 | }, { 282 | "pattern":"\\Qio/github/humbleui/ui/slider__init.class\\E" 283 | }, { 284 | "pattern":"\\Qio/github/humbleui/ui/stack.clj\\E" 285 | }, { 286 | "pattern":"\\Qio/github/humbleui/ui/stack__init.class\\E" 287 | }, { 288 | "pattern":"\\Qio/github/humbleui/ui/svg.clj\\E" 289 | }, { 290 | "pattern":"\\Qio/github/humbleui/ui/svg__init.class\\E" 291 | }, { 292 | "pattern":"\\Qio/github/humbleui/ui/text_field.clj\\E" 293 | }, { 294 | "pattern":"\\Qio/github/humbleui/ui/text_field__init.class\\E" 295 | }, { 296 | "pattern":"\\Qio/github/humbleui/ui/theme.clj\\E" 297 | }, { 298 | "pattern":"\\Qio/github/humbleui/ui/theme__init.class\\E" 299 | }, { 300 | "pattern":"\\Qio/github/humbleui/ui/toggle.clj\\E" 301 | }, { 302 | "pattern":"\\Qio/github/humbleui/ui/toggle__init.class\\E" 303 | }, { 304 | "pattern":"\\Qio/github/humbleui/ui/tooltip.clj\\E" 305 | }, { 306 | "pattern":"\\Qio/github/humbleui/ui/tooltip__init.class\\E" 307 | }, { 308 | "pattern":"\\Qio/github/humbleui/ui/window.clj\\E" 309 | }, { 310 | "pattern":"\\Qio/github/humbleui/ui/window__init.class\\E" 311 | }, { 312 | "pattern":"\\Qio/github/humbleui/ui/with_bounds.clj\\E" 313 | }, { 314 | "pattern":"\\Qio/github/humbleui/ui/with_bounds__init.class\\E" 315 | }, { 316 | "pattern":"\\Qio/github/humbleui/ui/with_context.clj\\E" 317 | }, { 318 | "pattern":"\\Qio/github/humbleui/ui/with_context__init.class\\E" 319 | }, { 320 | "pattern":"\\Qio/github/humbleui/ui/with_cursor.clj\\E" 321 | }, { 322 | "pattern":"\\Qio/github/humbleui/ui/with_cursor__init.class\\E" 323 | }, { 324 | "pattern":"\\Qio/github/humbleui/ui__init.class\\E" 325 | }, { 326 | "pattern":"\\Qio/github/humbleui/window.clj\\E" 327 | }, { 328 | "pattern":"\\Qio/github/humbleui/window__init.class\\E" 329 | }, { 330 | "pattern":"\\Qjwm.version\\E" 331 | }, { 332 | "pattern":"\\Qlibjwm_x64.so\\E" 333 | }, { 334 | "pattern":"\\Quser.clj\\E" 335 | }]}, 336 | "bundles":[] 337 | } 338 | -------------------------------------------------------------------------------- /traced-config/jni-config.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name":"[Lio.github.humbleui.jwm.Screen;" 4 | }, 5 | { 6 | "name":"io.github.humbleui.jwm.Clipboard", 7 | "methods":[{"name":"_registerPredefinedFormat","parameterTypes":["java.lang.String"] }] 8 | }, 9 | { 10 | "name":"io.github.humbleui.jwm.ClipboardEntry", 11 | "fields":[{"name":"_data"}, {"name":"_format"}], 12 | "methods":[{"name":"make","parameterTypes":["io.github.humbleui.jwm.ClipboardFormat","byte[]"] }] 13 | }, 14 | { 15 | "name":"io.github.humbleui.jwm.ClipboardFormat", 16 | "fields":[{"name":"_formatId"}] 17 | }, 18 | { 19 | "name":"io.github.humbleui.jwm.EventFrame", 20 | "fields":[{"name":"INSTANCE"}] 21 | }, 22 | { 23 | "name":"io.github.humbleui.jwm.EventKey", 24 | "methods":[{"name":"","parameterTypes":["int","boolean","int","int"] }] 25 | }, 26 | { 27 | "name":"io.github.humbleui.jwm.EventMouseButton", 28 | "methods":[{"name":"","parameterTypes":["int","boolean","int","int","int"] }] 29 | }, 30 | { 31 | "name":"io.github.humbleui.jwm.EventMouseMove", 32 | "methods":[{"name":"","parameterTypes":["int","int","int","int","int","int"] }] 33 | }, 34 | { 35 | "name":"io.github.humbleui.jwm.EventMouseScroll", 36 | "methods":[{"name":"","parameterTypes":["float","float","float","float","float","int","int","int"] }] 37 | }, 38 | { 39 | "name":"io.github.humbleui.jwm.EventTextInput", 40 | "methods":[{"name":"","parameterTypes":["java.lang.String","int","int"] }] 41 | }, 42 | { 43 | "name":"io.github.humbleui.jwm.EventTextInputMarked", 44 | "methods":[{"name":"","parameterTypes":["java.lang.String","int","int","int","int"] }] 45 | }, 46 | { 47 | "name":"io.github.humbleui.jwm.EventTouchCancel", 48 | "methods":[{"name":"","parameterTypes":["int"] }] 49 | }, 50 | { 51 | "name":"io.github.humbleui.jwm.EventTouchEnd", 52 | "methods":[{"name":"","parameterTypes":["int"] }] 53 | }, 54 | { 55 | "name":"io.github.humbleui.jwm.EventTouchMove", 56 | "methods":[{"name":"","parameterTypes":["int","float","float"] }] 57 | }, 58 | { 59 | "name":"io.github.humbleui.jwm.EventTouchStart", 60 | "methods":[{"name":"","parameterTypes":["int","float","float","int","float","float","int"] }] 61 | }, 62 | { 63 | "name":"io.github.humbleui.jwm.EventWindowCloseRequest", 64 | "fields":[{"name":"INSTANCE"}] 65 | }, 66 | { 67 | "name":"io.github.humbleui.jwm.EventWindowFocusIn", 68 | "fields":[{"name":"INSTANCE"}] 69 | }, 70 | { 71 | "name":"io.github.humbleui.jwm.EventWindowFocusOut", 72 | "fields":[{"name":"INSTANCE"}] 73 | }, 74 | { 75 | "name":"io.github.humbleui.jwm.EventWindowFullScreenEnter", 76 | "fields":[{"name":"INSTANCE"}] 77 | }, 78 | { 79 | "name":"io.github.humbleui.jwm.EventWindowFullScreenExit", 80 | "fields":[{"name":"INSTANCE"}] 81 | }, 82 | { 83 | "name":"io.github.humbleui.jwm.EventWindowMaximize", 84 | "fields":[{"name":"INSTANCE"}] 85 | }, 86 | { 87 | "name":"io.github.humbleui.jwm.EventWindowMinimize", 88 | "fields":[{"name":"INSTANCE"}] 89 | }, 90 | { 91 | "name":"io.github.humbleui.jwm.EventWindowMove", 92 | "methods":[{"name":"","parameterTypes":["int","int"] }] 93 | }, 94 | { 95 | "name":"io.github.humbleui.jwm.EventWindowResize", 96 | "methods":[{"name":"","parameterTypes":["int","int","int","int"] }] 97 | }, 98 | { 99 | "name":"io.github.humbleui.jwm.EventWindowRestore", 100 | "fields":[{"name":"INSTANCE"}] 101 | }, 102 | { 103 | "name":"io.github.humbleui.jwm.EventWindowScreenChange", 104 | "fields":[{"name":"INSTANCE"}] 105 | }, 106 | { 107 | "name":"io.github.humbleui.jwm.LayerNotSupportedException" 108 | }, 109 | { 110 | "name":"io.github.humbleui.jwm.Screen", 111 | "methods":[{"name":"","parameterTypes":["long","boolean","io.github.humbleui.types.IRect","io.github.humbleui.types.IRect","float"] }] 112 | }, 113 | { 114 | "name":"io.github.humbleui.jwm.TextInputClient", 115 | "methods":[{"name":"getMarkedRange","parameterTypes":[] }, {"name":"getRectForMarkedRange","parameterTypes":["int","int"] }, {"name":"getSelectedRange","parameterTypes":[] }, {"name":"getSubstring","parameterTypes":["int","int"] }] 116 | }, 117 | { 118 | "name":"io.github.humbleui.jwm.Window", 119 | "fields":[{"name":"_textInputClient"}] 120 | }, 121 | { 122 | "name":"io.github.humbleui.jwm.impl.Native", 123 | "fields":[{"name":"_ptr"}] 124 | }, 125 | { 126 | "name":"io.github.humbleui.skija.AnimationFrameInfo", 127 | "methods":[{"name":"","parameterTypes":["int","int","boolean","int","boolean","int","int","io.github.humbleui.types.IRect"] }] 128 | }, 129 | { 130 | "name":"io.github.humbleui.skija.Color4f", 131 | "methods":[{"name":"","parameterTypes":["float","float","float","float"] }] 132 | }, 133 | { 134 | "name":"io.github.humbleui.skija.Drawable", 135 | "methods":[{"name":"_onDraw","parameterTypes":["long"] }, {"name":"onGetBounds","parameterTypes":[] }] 136 | }, 137 | { 138 | "name":"io.github.humbleui.skija.FontFamilyName", 139 | "methods":[{"name":"","parameterTypes":["java.lang.String","java.lang.String"] }] 140 | }, 141 | { 142 | "name":"io.github.humbleui.skija.FontFeature", 143 | "fields":[{"name":"_end"}, {"name":"_start"}, {"name":"_tag"}, {"name":"_value"}], 144 | "methods":[{"name":"","parameterTypes":["java.lang.String","int"] }] 145 | }, 146 | { 147 | "name":"io.github.humbleui.skija.FontMetrics", 148 | "methods":[{"name":"","parameterTypes":["float","float","float","float","float","float","float","float","float","float","float","java.lang.Float","java.lang.Float","java.lang.Float","java.lang.Float"] }] 149 | }, 150 | { 151 | "name":"io.github.humbleui.skija.FontMgr" 152 | }, 153 | { 154 | "name":"io.github.humbleui.skija.FontVariation", 155 | "fields":[{"name":"_tag"}, {"name":"_value"}], 156 | "methods":[{"name":"","parameterTypes":["int","float"] }] 157 | }, 158 | { 159 | "name":"io.github.humbleui.skija.FontVariationAxis", 160 | "methods":[{"name":"","parameterTypes":["int","float","float","float","boolean"] }] 161 | }, 162 | { 163 | "name":"io.github.humbleui.skija.ImageInfo", 164 | "methods":[{"name":"","parameterTypes":["int","int","int","int","long"] }] 165 | }, 166 | { 167 | "name":"io.github.humbleui.skija.PaintFilterCanvas", 168 | "methods":[{"name":"onFilter","parameterTypes":["long"] }] 169 | }, 170 | { 171 | "name":"io.github.humbleui.skija.Path", 172 | "methods":[{"name":"","parameterTypes":["long"] }] 173 | }, 174 | { 175 | "name":"io.github.humbleui.skija.PathSegment", 176 | "methods":[{"name":"","parameterTypes":[] }, {"name":"","parameterTypes":["float","float","float","float","float","float","float","float","boolean"] }, {"name":"","parameterTypes":["float","float","float","float","float","float","float","boolean"] }, {"name":"","parameterTypes":["float","float","float","float","float","float","boolean"] }, {"name":"","parameterTypes":["float","float","float","float","boolean","boolean"] }, {"name":"","parameterTypes":["int","float","float","boolean"] }] 177 | }, 178 | { 179 | "name":"io.github.humbleui.skija.RSXform", 180 | "methods":[{"name":"","parameterTypes":["float","float","float","float"] }] 181 | }, 182 | { 183 | "name":"io.github.humbleui.skija.SurfaceProps", 184 | "methods":[{"name":"","parameterTypes":["boolean","int"] }, {"name":"_getFlags","parameterTypes":[] }, {"name":"_getPixelGeometryOrdinal","parameterTypes":[] }] 185 | }, 186 | { 187 | "name":"io.github.humbleui.skija.impl.Native", 188 | "fields":[{"name":"_ptr"}] 189 | }, 190 | { 191 | "name":"io.github.humbleui.skija.paragraph.DecorationStyle", 192 | "methods":[{"name":"","parameterTypes":["boolean","boolean","boolean","boolean","int","int","float"] }] 193 | }, 194 | { 195 | "name":"io.github.humbleui.skija.paragraph.LineMetrics", 196 | "methods":[{"name":"","parameterTypes":["long","long","long","long","boolean","double","double","double","double","double","double","double","long"] }] 197 | }, 198 | { 199 | "name":"io.github.humbleui.skija.paragraph.Shadow", 200 | "methods":[{"name":"","parameterTypes":["int","float","float","double"] }] 201 | }, 202 | { 203 | "name":"io.github.humbleui.skija.paragraph.TextBox", 204 | "methods":[{"name":"","parameterTypes":["float","float","float","float","int"] }] 205 | }, 206 | { 207 | "name":"io.github.humbleui.skija.shaper.BidiRun", 208 | "fields":[{"name":"_end"}, {"name":"_level"}] 209 | }, 210 | { 211 | "name":"io.github.humbleui.skija.shaper.FontMgrRunIterator" 212 | }, 213 | { 214 | "name":"io.github.humbleui.skija.shaper.FontRun", 215 | "fields":[{"name":"_end"}], 216 | "methods":[{"name":"_getFontPtr","parameterTypes":[] }] 217 | }, 218 | { 219 | "name":"io.github.humbleui.skija.shaper.HbIcuScriptRunIterator" 220 | }, 221 | { 222 | "name":"io.github.humbleui.skija.shaper.IcuBidiRunIterator" 223 | }, 224 | { 225 | "name":"io.github.humbleui.skija.shaper.LanguageRun", 226 | "fields":[{"name":"_end"}, {"name":"_language"}] 227 | }, 228 | { 229 | "name":"io.github.humbleui.skija.shaper.RunHandler", 230 | "methods":[{"name":"beginLine","parameterTypes":[] }, {"name":"commitLine","parameterTypes":[] }, {"name":"commitRun","parameterTypes":["io.github.humbleui.skija.shaper.RunInfo","short[]","io.github.humbleui.types.Point[]","int[]"] }, {"name":"commitRunInfo","parameterTypes":[] }, {"name":"runInfo","parameterTypes":["io.github.humbleui.skija.shaper.RunInfo"] }, {"name":"runOffset","parameterTypes":["io.github.humbleui.skija.shaper.RunInfo"] }] 231 | }, 232 | { 233 | "name":"io.github.humbleui.skija.shaper.RunInfo", 234 | "fields":[{"name":"_fontPtr"}], 235 | "methods":[{"name":"","parameterTypes":["long","int","float","float","long","int","int"] }] 236 | }, 237 | { 238 | "name":"io.github.humbleui.skija.shaper.ScriptRun", 239 | "fields":[{"name":"_end"}, {"name":"_scriptTag"}] 240 | }, 241 | { 242 | "name":"io.github.humbleui.skija.shaper.ShapingOptions", 243 | "fields":[{"name":"_approximatePunctuation"}, {"name":"_approximateSpaces"}, {"name":"_features"}, {"name":"_fontMgr"}, {"name":"_leftToRight"}] 244 | }, 245 | { 246 | "name":"io.github.humbleui.skija.shaper.TextBlobBuilderRunHandler" 247 | }, 248 | { 249 | "name":"io.github.humbleui.skija.shaper.TextLineRunHandler" 250 | }, 251 | { 252 | "name":"io.github.humbleui.skija.skottie.LogLevel", 253 | "fields":[{"name":"ERROR"}, {"name":"WARNING"}] 254 | }, 255 | { 256 | "name":"io.github.humbleui.skija.skottie.Logger", 257 | "methods":[{"name":"log","parameterTypes":["io.github.humbleui.skija.skottie.LogLevel","java.lang.String","java.lang.String"] }] 258 | }, 259 | { 260 | "name":"io.github.humbleui.skija.svg.SVGLength", 261 | "methods":[{"name":"","parameterTypes":["float","int"] }] 262 | }, 263 | { 264 | "name":"io.github.humbleui.skija.svg.SVGPreserveAspectRatio", 265 | "methods":[{"name":"","parameterTypes":["int","int"] }] 266 | }, 267 | { 268 | "name":"io.github.humbleui.types.IPoint", 269 | "methods":[{"name":"","parameterTypes":["int","int"] }] 270 | }, 271 | { 272 | "name":"io.github.humbleui.types.IRange", 273 | "fields":[{"name":"_end"}, {"name":"_start"}], 274 | "methods":[{"name":"","parameterTypes":["int","int"] }] 275 | }, 276 | { 277 | "name":"io.github.humbleui.types.IRect", 278 | "fields":[{"name":"_bottom"}, {"name":"_left"}, {"name":"_right"}, {"name":"_top"}], 279 | "methods":[{"name":"","parameterTypes":["int","int","int","int"] }, {"name":"makeLTRB","parameterTypes":["int","int","int","int"] }] 280 | }, 281 | { 282 | "name":"io.github.humbleui.types.Point", 283 | "fields":[{"name":"_x"}, {"name":"_y"}], 284 | "methods":[{"name":"","parameterTypes":["float","float"] }] 285 | }, 286 | { 287 | "name":"io.github.humbleui.types.RRect", 288 | "fields":[{"name":"_radii"}], 289 | "methods":[{"name":"makeComplexLTRB","parameterTypes":["float","float","float","float","float[]"] }, {"name":"makeLTRB","parameterTypes":["float","float","float","float","float"] }, {"name":"makeLTRB","parameterTypes":["float","float","float","float","float","float"] }, {"name":"makeLTRB","parameterTypes":["float","float","float","float","float","float","float","float"] }, {"name":"makeNinePatchLTRB","parameterTypes":["float","float","float","float","float","float","float","float"] }] 290 | }, 291 | { 292 | "name":"io.github.humbleui.types.Rect", 293 | "fields":[{"name":"_bottom"}, {"name":"_left"}, {"name":"_right"}, {"name":"_top"}], 294 | "methods":[{"name":"makeLTRB","parameterTypes":["float","float","float","float"] }] 295 | }, 296 | { 297 | "name":"java.io.OutputStream", 298 | "methods":[{"name":"flush","parameterTypes":[] }, {"name":"write","parameterTypes":["byte[]","int","int"] }] 299 | }, 300 | { 301 | "name":"java.lang.Boolean", 302 | "methods":[{"name":"getBoolean","parameterTypes":["java.lang.String"] }] 303 | }, 304 | { 305 | "name":"java.lang.Float", 306 | "methods":[{"name":"","parameterTypes":["float"] }] 307 | }, 308 | { 309 | "name":"java.lang.Runnable", 310 | "methods":[{"name":"run","parameterTypes":[] }] 311 | }, 312 | { 313 | "name":"java.lang.RuntimeException" 314 | }, 315 | { 316 | "name":"java.lang.String" 317 | }, 318 | { 319 | "name":"java.lang.Throwable", 320 | "methods":[{"name":"printStackTrace","parameterTypes":[] }] 321 | }, 322 | { 323 | "name":"java.util.Iterator", 324 | "methods":[{"name":"hasNext","parameterTypes":[] }, {"name":"next","parameterTypes":[] }] 325 | }, 326 | { 327 | "name":"java.util.function.BooleanSupplier", 328 | "methods":[{"name":"getAsBoolean","parameterTypes":[] }] 329 | }, 330 | { 331 | "name":"java.util.function.Consumer", 332 | "methods":[{"name":"accept","parameterTypes":["java.lang.Object"] }] 333 | } 334 | ] 335 | -------------------------------------------------------------------------------- /traced-config/reflect-config.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name":"[B" 4 | }, 5 | { 6 | "name":"[C" 7 | }, 8 | { 9 | "name":"[D" 10 | }, 11 | { 12 | "name":"[F" 13 | }, 14 | { 15 | "name":"[I" 16 | }, 17 | { 18 | "name":"[J" 19 | }, 20 | { 21 | "name":"[Ljava.lang.Object;" 22 | }, 23 | { 24 | "name":"[S" 25 | }, 26 | { 27 | "name":"[Z" 28 | }, 29 | { 30 | "name":"clojure.asm.ClassVisitor" 31 | }, 32 | { 33 | "name":"clojure.asm.ClassWriter" 34 | }, 35 | { 36 | "name":"clojure.asm.Opcodes" 37 | }, 38 | { 39 | "name":"clojure.asm.Type" 40 | }, 41 | { 42 | "name":"clojure.asm.commons.GeneratorAdapter" 43 | }, 44 | { 45 | "name":"clojure.asm.commons.Method" 46 | }, 47 | { 48 | "name":"clojure.core.ArrayChunk" 49 | }, 50 | { 51 | "name":"clojure.core.ArrayManager" 52 | }, 53 | { 54 | "name":"clojure.core.Eduction" 55 | }, 56 | { 57 | "name":"clojure.core.IVecImpl" 58 | }, 59 | { 60 | "name":"clojure.core.Inst" 61 | }, 62 | { 63 | "name":"clojure.core.Vec" 64 | }, 65 | { 66 | "name":"clojure.core.VecNode" 67 | }, 68 | { 69 | "name":"clojure.core.VecSeq" 70 | }, 71 | { 72 | "name":"clojure.core.protocols.CollReduce" 73 | }, 74 | { 75 | "name":"clojure.core.protocols.Datafiable" 76 | }, 77 | { 78 | "name":"clojure.core.protocols.IKVReduce" 79 | }, 80 | { 81 | "name":"clojure.core.protocols.InternalReduce" 82 | }, 83 | { 84 | "name":"clojure.core.protocols.Navigable" 85 | }, 86 | { 87 | "name":"clojure.core.protocols__init" 88 | }, 89 | { 90 | "name":"clojure.core.server__init" 91 | }, 92 | { 93 | "name":"clojure.core__init" 94 | }, 95 | { 96 | "name":"clojure.core_deftype__init" 97 | }, 98 | { 99 | "name":"clojure.core_print__init" 100 | }, 101 | { 102 | "name":"clojure.core_proxy__init" 103 | }, 104 | { 105 | "name":"clojure.edn__init" 106 | }, 107 | { 108 | "name":"clojure.genclass__init" 109 | }, 110 | { 111 | "name":"clojure.gvec__init" 112 | }, 113 | { 114 | "name":"clojure.instant__init" 115 | }, 116 | { 117 | "name":"clojure.java.io.Coercions" 118 | }, 119 | { 120 | "name":"clojure.java.io.IOFactory" 121 | }, 122 | { 123 | "name":"clojure.java.io__init" 124 | }, 125 | { 126 | "name":"clojure.lang.APersistentMap$KeySeq" 127 | }, 128 | { 129 | "name":"clojure.lang.APersistentMap$ValSeq" 130 | }, 131 | { 132 | "name":"clojure.lang.ASeq" 133 | }, 134 | { 135 | "name":"clojure.lang.BigInt" 136 | }, 137 | { 138 | "name":"clojure.lang.ChunkBuffer" 139 | }, 140 | { 141 | "name":"clojure.lang.Compiler" 142 | }, 143 | { 144 | "name":"clojure.lang.Compiler$CompilerException" 145 | }, 146 | { 147 | "name":"clojure.lang.DynamicClassLoader" 148 | }, 149 | { 150 | "name":"clojure.lang.ExceptionInfo" 151 | }, 152 | { 153 | "name":"clojure.lang.Fn" 154 | }, 155 | { 156 | "name":"clojure.lang.IChunk" 157 | }, 158 | { 159 | "name":"clojure.lang.IChunkedSeq" 160 | }, 161 | { 162 | "name":"clojure.lang.IDeref" 163 | }, 164 | { 165 | "name":"clojure.lang.IExceptionInfo" 166 | }, 167 | { 168 | "name":"clojure.lang.IFn" 169 | }, 170 | { 171 | "name":"clojure.lang.IHashEq" 172 | }, 173 | { 174 | "name":"clojure.lang.IKVReduce" 175 | }, 176 | { 177 | "name":"clojure.lang.IPersistentCollection" 178 | }, 179 | { 180 | "name":"clojure.lang.IPersistentList" 181 | }, 182 | { 183 | "name":"clojure.lang.IPersistentMap" 184 | }, 185 | { 186 | "name":"clojure.lang.IPersistentSet" 187 | }, 188 | { 189 | "name":"clojure.lang.IPersistentVector" 190 | }, 191 | { 192 | "name":"clojure.lang.IProxy" 193 | }, 194 | { 195 | "name":"clojure.lang.IRecord" 196 | }, 197 | { 198 | "name":"clojure.lang.IReduceInit" 199 | }, 200 | { 201 | "name":"clojure.lang.ISeq" 202 | }, 203 | { 204 | "name":"clojure.lang.Keyword" 205 | }, 206 | { 207 | "name":"clojure.lang.LazilyPersistentVector" 208 | }, 209 | { 210 | "name":"clojure.lang.LazySeq" 211 | }, 212 | { 213 | "name":"clojure.lang.LineNumberingPushbackReader" 214 | }, 215 | { 216 | "name":"clojure.lang.LispReader$ReaderException" 217 | }, 218 | { 219 | "name":"clojure.lang.Murmur3" 220 | }, 221 | { 222 | "name":"clojure.lang.Namespace" 223 | }, 224 | { 225 | "name":"clojure.lang.PersistentHashMap" 226 | }, 227 | { 228 | "name":"clojure.lang.PersistentHashSet" 229 | }, 230 | { 231 | "name":"clojure.lang.PersistentVector" 232 | }, 233 | { 234 | "name":"clojure.lang.RT" 235 | }, 236 | { 237 | "name":"clojure.lang.Ratio" 238 | }, 239 | { 240 | "name":"clojure.lang.ReaderConditional" 241 | }, 242 | { 243 | "name":"clojure.lang.Reflector" 244 | }, 245 | { 246 | "name":"clojure.lang.SeqIterator" 247 | }, 248 | { 249 | "name":"clojure.lang.Sequential" 250 | }, 251 | { 252 | "name":"clojure.lang.StringSeq" 253 | }, 254 | { 255 | "name":"clojure.lang.Symbol" 256 | }, 257 | { 258 | "name":"clojure.lang.TaggedLiteral" 259 | }, 260 | { 261 | "name":"clojure.lang.Util" 262 | }, 263 | { 264 | "name":"clojure.lang.Var" 265 | }, 266 | { 267 | "name":"clojure.lang.Volatile" 268 | }, 269 | { 270 | "name":"clojure.main__init" 271 | }, 272 | { 273 | "name":"clojure.math__init" 274 | }, 275 | { 276 | "name":"clojure.set__init" 277 | }, 278 | { 279 | "name":"clojure.spec.alpha.Spec" 280 | }, 281 | { 282 | "name":"clojure.spec.alpha.Specize" 283 | }, 284 | { 285 | "name":"clojure.spec.alpha__init" 286 | }, 287 | { 288 | "name":"clojure.spec.gen.alpha__init" 289 | }, 290 | { 291 | "name":"clojure.stacktrace__init" 292 | }, 293 | { 294 | "name":"clojure.string__init" 295 | }, 296 | { 297 | "name":"clojure.uuid__init" 298 | }, 299 | { 300 | "name":"clojure.walk__init" 301 | }, 302 | { 303 | "name":"humble_outliner.demo__init" 304 | }, 305 | { 306 | "name":"humble_outliner.events__init" 307 | }, 308 | { 309 | "name":"humble_outliner.main__init" 310 | }, 311 | { 312 | "name":"humble_outliner.model__init" 313 | }, 314 | { 315 | "name":"humble_outliner.state__init" 316 | }, 317 | { 318 | "name":"humble_outliner.theme__init" 319 | }, 320 | { 321 | "name":"humble_outliner.views__init" 322 | }, 323 | { 324 | "name":"io.github.humbleui.app__init" 325 | }, 326 | { 327 | "name":"io.github.humbleui.canvas__init" 328 | }, 329 | { 330 | "name":"io.github.humbleui.clipboard__init" 331 | }, 332 | { 333 | "name":"io.github.humbleui.core__init" 334 | }, 335 | { 336 | "name":"io.github.humbleui.cursor.Cursor" 337 | }, 338 | { 339 | "name":"io.github.humbleui.cursor__init" 340 | }, 341 | { 342 | "name":"io.github.humbleui.debug__init" 343 | }, 344 | { 345 | "name":"io.github.humbleui.error__init" 346 | }, 347 | { 348 | "name":"io.github.humbleui.event__init" 349 | }, 350 | { 351 | "name":"io.github.humbleui.font__init" 352 | }, 353 | { 354 | "name":"io.github.humbleui.jwm.App" 355 | }, 356 | { 357 | "name":"io.github.humbleui.jwm.Clipboard" 358 | }, 359 | { 360 | "name":"io.github.humbleui.jwm.ClipboardEntry" 361 | }, 362 | { 363 | "name":"io.github.humbleui.jwm.ClipboardFormat" 364 | }, 365 | { 366 | "name":"io.github.humbleui.jwm.EventFrame" 367 | }, 368 | { 369 | "name":"io.github.humbleui.jwm.EventKey" 370 | }, 371 | { 372 | "name":"io.github.humbleui.jwm.EventMouseButton" 373 | }, 374 | { 375 | "name":"io.github.humbleui.jwm.EventMouseMove" 376 | }, 377 | { 378 | "name":"io.github.humbleui.jwm.EventMouseScroll" 379 | }, 380 | { 381 | "name":"io.github.humbleui.jwm.EventTextInput" 382 | }, 383 | { 384 | "name":"io.github.humbleui.jwm.EventTextInputMarked" 385 | }, 386 | { 387 | "name":"io.github.humbleui.jwm.EventWindowClose" 388 | }, 389 | { 390 | "name":"io.github.humbleui.jwm.EventWindowCloseRequest" 391 | }, 392 | { 393 | "name":"io.github.humbleui.jwm.EventWindowFocusIn" 394 | }, 395 | { 396 | "name":"io.github.humbleui.jwm.EventWindowFocusOut" 397 | }, 398 | { 399 | "name":"io.github.humbleui.jwm.EventWindowMaximize" 400 | }, 401 | { 402 | "name":"io.github.humbleui.jwm.EventWindowMove" 403 | }, 404 | { 405 | "name":"io.github.humbleui.jwm.EventWindowResize" 406 | }, 407 | { 408 | "name":"io.github.humbleui.jwm.EventWindowRestore" 409 | }, 410 | { 411 | "name":"io.github.humbleui.jwm.EventWindowScreenChange" 412 | }, 413 | { 414 | "name":"io.github.humbleui.jwm.Key" 415 | }, 416 | { 417 | "name":"io.github.humbleui.jwm.KeyLocation" 418 | }, 419 | { 420 | "name":"io.github.humbleui.jwm.MouseButton" 421 | }, 422 | { 423 | "name":"io.github.humbleui.jwm.MouseCursor" 424 | }, 425 | { 426 | "name":"io.github.humbleui.jwm.Platform" 427 | }, 428 | { 429 | "name":"io.github.humbleui.jwm.Screen" 430 | }, 431 | { 432 | "name":"io.github.humbleui.jwm.TextInputClient" 433 | }, 434 | { 435 | "name":"io.github.humbleui.jwm.Window" 436 | }, 437 | { 438 | "name":"io.github.humbleui.jwm.ZOrder" 439 | }, 440 | { 441 | "name":"io.github.humbleui.jwm.impl.Library" 442 | }, 443 | { 444 | "name":"io.github.humbleui.jwm.skija.EventFrameSkija" 445 | }, 446 | { 447 | "name":"io.github.humbleui.jwm.skija.LayerD3D12Skija" 448 | }, 449 | { 450 | "name":"io.github.humbleui.jwm.skija.LayerGLSkija" 451 | }, 452 | { 453 | "name":"io.github.humbleui.jwm.skija.LayerMetalSkija" 454 | }, 455 | { 456 | "name":"io.github.humbleui.paint__init" 457 | }, 458 | { 459 | "name":"io.github.humbleui.protocols.IComponent" 460 | }, 461 | { 462 | "name":"io.github.humbleui.protocols.IContext" 463 | }, 464 | { 465 | "name":"io.github.humbleui.protocols.ISettable" 466 | }, 467 | { 468 | "name":"io.github.humbleui.protocols__init" 469 | }, 470 | { 471 | "name":"io.github.humbleui.skija.AnimationFrameInfo" 472 | }, 473 | { 474 | "name":"io.github.humbleui.skija.Bitmap" 475 | }, 476 | { 477 | "name":"io.github.humbleui.skija.BreakIterator" 478 | }, 479 | { 480 | "name":"io.github.humbleui.skija.Canvas" 481 | }, 482 | { 483 | "name":"io.github.humbleui.skija.Codec" 484 | }, 485 | { 486 | "name":"io.github.humbleui.skija.Color" 487 | }, 488 | { 489 | "name":"io.github.humbleui.skija.ColorAlphaType" 490 | }, 491 | { 492 | "name":"io.github.humbleui.skija.Data" 493 | }, 494 | { 495 | "name":"io.github.humbleui.skija.FilterBlurMode" 496 | }, 497 | { 498 | "name":"io.github.humbleui.skija.Font" 499 | }, 500 | { 501 | "name":"io.github.humbleui.skija.FontMetrics" 502 | }, 503 | { 504 | "name":"io.github.humbleui.skija.Image" 505 | }, 506 | { 507 | "name":"io.github.humbleui.skija.ImageFilter" 508 | }, 509 | { 510 | "name":"io.github.humbleui.skija.ImageInfo" 511 | }, 512 | { 513 | "name":"io.github.humbleui.skija.MaskFilter" 514 | }, 515 | { 516 | "name":"io.github.humbleui.skija.Paint" 517 | }, 518 | { 519 | "name":"io.github.humbleui.skija.PaintMode" 520 | }, 521 | { 522 | "name":"io.github.humbleui.skija.Path" 523 | }, 524 | { 525 | "name":"io.github.humbleui.skija.PathDirection" 526 | }, 527 | { 528 | "name":"io.github.humbleui.skija.SaveLayerRec" 529 | }, 530 | { 531 | "name":"io.github.humbleui.skija.Surface" 532 | }, 533 | { 534 | "name":"io.github.humbleui.skija.TextLine" 535 | }, 536 | { 537 | "name":"io.github.humbleui.skija.Typeface" 538 | }, 539 | { 540 | "name":"io.github.humbleui.skija.shaper.Shaper" 541 | }, 542 | { 543 | "name":"io.github.humbleui.skija.shaper.ShapingOptions" 544 | }, 545 | { 546 | "name":"io.github.humbleui.skija.svg.SVGDOM" 547 | }, 548 | { 549 | "name":"io.github.humbleui.skija.svg.SVGLength" 550 | }, 551 | { 552 | "name":"io.github.humbleui.skija.svg.SVGPreserveAspectRatio" 553 | }, 554 | { 555 | "name":"io.github.humbleui.skija.svg.SVGPreserveAspectRatioAlign" 556 | }, 557 | { 558 | "name":"io.github.humbleui.skija.svg.SVGPreserveAspectRatioScale" 559 | }, 560 | { 561 | "name":"io.github.humbleui.typeface__init" 562 | }, 563 | { 564 | "name":"io.github.humbleui.types.IPoint" 565 | }, 566 | { 567 | "name":"io.github.humbleui.types.IRange" 568 | }, 569 | { 570 | "name":"io.github.humbleui.types.IRect" 571 | }, 572 | { 573 | "name":"io.github.humbleui.types.Point" 574 | }, 575 | { 576 | "name":"io.github.humbleui.types.RRect" 577 | }, 578 | { 579 | "name":"io.github.humbleui.types.Rect" 580 | }, 581 | { 582 | "name":"io.github.humbleui.ui.align.HAlign" 583 | }, 584 | { 585 | "name":"io.github.humbleui.ui.align.VAlign" 586 | }, 587 | { 588 | "name":"io.github.humbleui.ui.align__init" 589 | }, 590 | { 591 | "name":"io.github.humbleui.ui.animation.Animation" 592 | }, 593 | { 594 | "name":"io.github.humbleui.ui.animation__init" 595 | }, 596 | { 597 | "name":"io.github.humbleui.ui.backdrop.Backdrop" 598 | }, 599 | { 600 | "name":"io.github.humbleui.ui.backdrop__init" 601 | }, 602 | { 603 | "name":"io.github.humbleui.ui.button__init" 604 | }, 605 | { 606 | "name":"io.github.humbleui.ui.canvas.ACanvas" 607 | }, 608 | { 609 | "name":"io.github.humbleui.ui.canvas__init" 610 | }, 611 | { 612 | "name":"io.github.humbleui.ui.checkbox__init" 613 | }, 614 | { 615 | "name":"io.github.humbleui.ui.clickable.Clickable" 616 | }, 617 | { 618 | "name":"io.github.humbleui.ui.clickable__init" 619 | }, 620 | { 621 | "name":"io.github.humbleui.ui.clip.Clip" 622 | }, 623 | { 624 | "name":"io.github.humbleui.ui.clip.ClipRRect" 625 | }, 626 | { 627 | "name":"io.github.humbleui.ui.clip__init" 628 | }, 629 | { 630 | "name":"io.github.humbleui.ui.containers.Column" 631 | }, 632 | { 633 | "name":"io.github.humbleui.ui.containers.Row" 634 | }, 635 | { 636 | "name":"io.github.humbleui.ui.containers__init" 637 | }, 638 | { 639 | "name":"io.github.humbleui.ui.draggable.Draggable" 640 | }, 641 | { 642 | "name":"io.github.humbleui.ui.draggable__init" 643 | }, 644 | { 645 | "name":"io.github.humbleui.ui.dynamic.Contextual" 646 | }, 647 | { 648 | "name":"io.github.humbleui.ui.dynamic__init" 649 | }, 650 | { 651 | "name":"io.github.humbleui.ui.focusable.FocusController" 652 | }, 653 | { 654 | "name":"io.github.humbleui.ui.focusable.Focusable" 655 | }, 656 | { 657 | "name":"io.github.humbleui.ui.focusable__init" 658 | }, 659 | { 660 | "name":"io.github.humbleui.ui.gap.Gap" 661 | }, 662 | { 663 | "name":"io.github.humbleui.ui.gap__init" 664 | }, 665 | { 666 | "name":"io.github.humbleui.ui.grid.Grid" 667 | }, 668 | { 669 | "name":"io.github.humbleui.ui.grid__init" 670 | }, 671 | { 672 | "name":"io.github.humbleui.ui.hoverable.Hoverable" 673 | }, 674 | { 675 | "name":"io.github.humbleui.ui.hoverable__init" 676 | }, 677 | { 678 | "name":"io.github.humbleui.ui.image.AnImage" 679 | }, 680 | { 681 | "name":"io.github.humbleui.ui.image__init" 682 | }, 683 | { 684 | "name":"io.github.humbleui.ui.image_snapshot.ImageSnapshot" 685 | }, 686 | { 687 | "name":"io.github.humbleui.ui.image_snapshot__init" 688 | }, 689 | { 690 | "name":"io.github.humbleui.ui.label.Label" 691 | }, 692 | { 693 | "name":"io.github.humbleui.ui.label__init" 694 | }, 695 | { 696 | "name":"io.github.humbleui.ui.listeners.EventListener" 697 | }, 698 | { 699 | "name":"io.github.humbleui.ui.listeners.KeyListener" 700 | }, 701 | { 702 | "name":"io.github.humbleui.ui.listeners.MouseListener" 703 | }, 704 | { 705 | "name":"io.github.humbleui.ui.listeners.TextListener" 706 | }, 707 | { 708 | "name":"io.github.humbleui.ui.listeners__init" 709 | }, 710 | { 711 | "name":"io.github.humbleui.ui.padding.Padding" 712 | }, 713 | { 714 | "name":"io.github.humbleui.ui.padding__init" 715 | }, 716 | { 717 | "name":"io.github.humbleui.ui.paragraph.Paragraph" 718 | }, 719 | { 720 | "name":"io.github.humbleui.ui.paragraph__init" 721 | }, 722 | { 723 | "name":"io.github.humbleui.ui.rect.Rect" 724 | }, 725 | { 726 | "name":"io.github.humbleui.ui.rect.RoundedRect" 727 | }, 728 | { 729 | "name":"io.github.humbleui.ui.rect__init" 730 | }, 731 | { 732 | "name":"io.github.humbleui.ui.scroll.VScroll" 733 | }, 734 | { 735 | "name":"io.github.humbleui.ui.scroll.VScrollbar" 736 | }, 737 | { 738 | "name":"io.github.humbleui.ui.scroll__init" 739 | }, 740 | { 741 | "name":"io.github.humbleui.ui.shadow__init" 742 | }, 743 | { 744 | "name":"io.github.humbleui.ui.sizing.Height" 745 | }, 746 | { 747 | "name":"io.github.humbleui.ui.sizing.MaxWidth" 748 | }, 749 | { 750 | "name":"io.github.humbleui.ui.sizing.Width" 751 | }, 752 | { 753 | "name":"io.github.humbleui.ui.sizing__init" 754 | }, 755 | { 756 | "name":"io.github.humbleui.ui.slider.Slider" 757 | }, 758 | { 759 | "name":"io.github.humbleui.ui.slider.SliderThumb" 760 | }, 761 | { 762 | "name":"io.github.humbleui.ui.slider.SliderTrack" 763 | }, 764 | { 765 | "name":"io.github.humbleui.ui.slider__init" 766 | }, 767 | { 768 | "name":"io.github.humbleui.ui.stack.Stack" 769 | }, 770 | { 771 | "name":"io.github.humbleui.ui.stack__init" 772 | }, 773 | { 774 | "name":"io.github.humbleui.ui.svg.SVG" 775 | }, 776 | { 777 | "name":"io.github.humbleui.ui.svg__init" 778 | }, 779 | { 780 | "name":"io.github.humbleui.ui.text_field.TextInput" 781 | }, 782 | { 783 | "name":"io.github.humbleui.ui.text_field__init" 784 | }, 785 | { 786 | "name":"io.github.humbleui.ui.theme__init" 787 | }, 788 | { 789 | "name":"io.github.humbleui.ui.toggle.Toggle" 790 | }, 791 | { 792 | "name":"io.github.humbleui.ui.toggle__init" 793 | }, 794 | { 795 | "name":"io.github.humbleui.ui.tooltip.RelativeRect" 796 | }, 797 | { 798 | "name":"io.github.humbleui.ui.tooltip__init" 799 | }, 800 | { 801 | "name":"io.github.humbleui.ui.window__init" 802 | }, 803 | { 804 | "name":"io.github.humbleui.ui.with_bounds.WithBounds" 805 | }, 806 | { 807 | "name":"io.github.humbleui.ui.with_bounds__init" 808 | }, 809 | { 810 | "name":"io.github.humbleui.ui.with_context.WithContext" 811 | }, 812 | { 813 | "name":"io.github.humbleui.ui.with_context__init" 814 | }, 815 | { 816 | "name":"io.github.humbleui.ui.with_cursor.WithCursor" 817 | }, 818 | { 819 | "name":"io.github.humbleui.ui.with_cursor__init" 820 | }, 821 | { 822 | "name":"io.github.humbleui.ui__init" 823 | }, 824 | { 825 | "name":"io.github.humbleui.window__init" 826 | }, 827 | { 828 | "name":"java.io.BufferedInputStream" 829 | }, 830 | { 831 | "name":"java.io.BufferedOutputStream" 832 | }, 833 | { 834 | "name":"java.io.BufferedReader" 835 | }, 836 | { 837 | "name":"java.io.BufferedWriter" 838 | }, 839 | { 840 | "name":"java.io.ByteArrayInputStream" 841 | }, 842 | { 843 | "name":"java.io.ByteArrayOutputStream" 844 | }, 845 | { 846 | "name":"java.io.CharArrayReader" 847 | }, 848 | { 849 | "name":"java.io.Closeable" 850 | }, 851 | { 852 | "name":"java.io.File" 853 | }, 854 | { 855 | "name":"java.io.FileInputStream" 856 | }, 857 | { 858 | "name":"java.io.FileOutputStream" 859 | }, 860 | { 861 | "name":"java.io.FileWriter" 862 | }, 863 | { 864 | "name":"java.io.InputStream" 865 | }, 866 | { 867 | "name":"java.io.InputStreamReader" 868 | }, 869 | { 870 | "name":"java.io.NotSerializableException" 871 | }, 872 | { 873 | "name":"java.io.OutputStream" 874 | }, 875 | { 876 | "name":"java.io.OutputStreamWriter" 877 | }, 878 | { 879 | "name":"java.io.PrintWriter" 880 | }, 881 | { 882 | "name":"java.io.PushbackReader" 883 | }, 884 | { 885 | "name":"java.io.Reader" 886 | }, 887 | { 888 | "name":"java.io.Serializable" 889 | }, 890 | { 891 | "name":"java.io.StringReader" 892 | }, 893 | { 894 | "name":"java.io.Writer" 895 | }, 896 | { 897 | "name":"java.lang.AutoCloseable" 898 | }, 899 | { 900 | "name":"java.lang.Boolean" 901 | }, 902 | { 903 | "name":"java.lang.Character" 904 | }, 905 | { 906 | "name":"java.lang.Class" 907 | }, 908 | { 909 | "name":"java.lang.Double" 910 | }, 911 | { 912 | "name":"java.lang.Float" 913 | }, 914 | { 915 | "name":"java.lang.Iterable" 916 | }, 917 | { 918 | "name":"java.lang.Long" 919 | }, 920 | { 921 | "name":"java.lang.Number" 922 | }, 923 | { 924 | "name":"java.lang.Object" 925 | }, 926 | { 927 | "name":"java.lang.StackTraceElement" 928 | }, 929 | { 930 | "name":"java.lang.String" 931 | }, 932 | { 933 | "name":"java.lang.ThreadLocal" 934 | }, 935 | { 936 | "name":"java.lang.Throwable" 937 | }, 938 | { 939 | "name":"java.lang.UnsupportedOperationException" 940 | }, 941 | { 942 | "name":"java.lang.annotation.Annotation" 943 | }, 944 | { 945 | "name":"java.lang.annotation.Retention" 946 | }, 947 | { 948 | "name":"java.lang.reflect.Array" 949 | }, 950 | { 951 | "name":"java.lang.reflect.Constructor" 952 | }, 953 | { 954 | "name":"java.lang.reflect.Field" 955 | }, 956 | { 957 | "name":"java.lang.reflect.Modifier" 958 | }, 959 | { 960 | "name":"java.math.BigDecimal" 961 | }, 962 | { 963 | "name":"java.math.BigInteger" 964 | }, 965 | { 966 | "name":"java.net.InetAddress" 967 | }, 968 | { 969 | "name":"java.net.MalformedURLException" 970 | }, 971 | { 972 | "name":"java.net.ServerSocket" 973 | }, 974 | { 975 | "name":"java.net.Socket" 976 | }, 977 | { 978 | "name":"java.net.SocketException" 979 | }, 980 | { 981 | "name":"java.net.URI" 982 | }, 983 | { 984 | "name":"java.net.URL" 985 | }, 986 | { 987 | "name":"java.net.URLDecoder" 988 | }, 989 | { 990 | "name":"java.net.URLEncoder" 991 | }, 992 | { 993 | "name":"java.nio.file.Files" 994 | }, 995 | { 996 | "name":"java.nio.file.attribute.FileAttribute" 997 | }, 998 | { 999 | "name":"java.sql.Timestamp" 1000 | }, 1001 | { 1002 | "name":"java.time.Instant" 1003 | }, 1004 | { 1005 | "name":"java.util.Calendar" 1006 | }, 1007 | { 1008 | "name":"java.util.Collection" 1009 | }, 1010 | { 1011 | "name":"java.util.Date" 1012 | }, 1013 | { 1014 | "name":"java.util.GregorianCalendar" 1015 | }, 1016 | { 1017 | "name":"java.util.List" 1018 | }, 1019 | { 1020 | "name":"java.util.Map" 1021 | }, 1022 | { 1023 | "name":"java.util.RandomAccess" 1024 | }, 1025 | { 1026 | "name":"java.util.Set" 1027 | }, 1028 | { 1029 | "name":"java.util.TimeZone" 1030 | }, 1031 | { 1032 | "name":"java.util.Timer" 1033 | }, 1034 | { 1035 | "name":"java.util.TimerTask" 1036 | }, 1037 | { 1038 | "name":"java.util.UUID" 1039 | }, 1040 | { 1041 | "name":"java.util.concurrent.ArrayBlockingQueue" 1042 | }, 1043 | { 1044 | "name":"java.util.concurrent.BlockingQueue" 1045 | }, 1046 | { 1047 | "name":"java.util.concurrent.LinkedBlockingQueue" 1048 | }, 1049 | { 1050 | "name":"java.util.concurrent.atomic.AtomicBoolean", 1051 | "fields":[{"name":"value"}] 1052 | }, 1053 | { 1054 | "name":"java.util.concurrent.atomic.AtomicReference", 1055 | "fields":[{"name":"value"}] 1056 | }, 1057 | { 1058 | "name":"java.util.concurrent.locks.ReentrantLock" 1059 | }, 1060 | { 1061 | "name":"java.util.function.Consumer" 1062 | }, 1063 | { 1064 | "name":"java.util.regex.Matcher" 1065 | }, 1066 | { 1067 | "name":"java.util.regex.Pattern" 1068 | } 1069 | ] 1070 | --------------------------------------------------------------------------------