Still nothing works, but I have a feeling that progress is being made.

This commit is contained in:
Simon Brooke 2023-07-29 09:43:35 +01:00
parent 4fe7d265ca
commit c8ca13c186
38 changed files with 431 additions and 510 deletions

1
barra.edn Normal file

File diff suppressed because one or more lines are too long

View file

@ -1,13 +1,15 @@
(defproject mw-desktop "0.1.0-SNAPSHOT" (defproject mw-desktop "0.3.0-SNAPSHOT"
:description "FIXME: write description" :description "FIXME: write description"
:url "http://example.com/FIXME" :url "http://example.com/FIXME"
:license {:name "EPL-2.0 OR GPL-2.0-or-later WITH Classpath-exception-2.0" :license {:name "EPL-2.0 OR GPL-2.0-or-later WITH Classpath-exception-2.0"
:url "https://www.eclipse.org/legal/epl-2.0/"} :url "https://www.eclipse.org/legal/epl-2.0/"}
:dependencies [[cljfx "1.7.23"] :dependencies [[cljfx "1.7.23"]
[com.novemberain/pantomime "2.11.0"]
[com.taoensso/timbre "6.2.2"]
[de.codecentric.centerdevice/javafxsvg "1.3.0"] ;; used by markdown-editor-example [de.codecentric.centerdevice/javafxsvg "1.3.0"] ;; used by markdown-editor-example
[markdown-clj "1.11.4"] [markdown-clj "1.11.4"]
[mw-engine "0.2.0-SNAPSHOT"] [mw-engine "0.3.0-SNAPSHOT"]
[mw-parser "0.2.0-SNAPSHOT"] [mw-parser "0.3.0-SNAPSHOT"]
[net.sourceforge.htmlcleaner/htmlcleaner "2.29"] [net.sourceforge.htmlcleaner/htmlcleaner "2.29"]
[org.clojure/clojure "1.10.3"] [org.clojure/clojure "1.10.3"]
[org.clojure/core.cache "1.0.225"] [org.clojure/core.cache "1.0.225"]

View file

Before

Width:  |  Height:  |  Size: 169 B

After

Width:  |  Height:  |  Size: 169 B

View file

Before

Width:  |  Height:  |  Size: 160 B

After

Width:  |  Height:  |  Size: 160 B

View file

Before

Width:  |  Height:  |  Size: 164 B

After

Width:  |  Height:  |  Size: 164 B

View file

Before

Width:  |  Height:  |  Size: 605 B

After

Width:  |  Height:  |  Size: 605 B

View file

Before

Width:  |  Height:  |  Size: 395 B

After

Width:  |  Height:  |  Size: 395 B

View file

Before

Width:  |  Height:  |  Size: 921 B

After

Width:  |  Height:  |  Size: 921 B

View file

Before

Width:  |  Height:  |  Size: 641 B

After

Width:  |  Height:  |  Size: 641 B

View file

Before

Width:  |  Height:  |  Size: 160 B

After

Width:  |  Height:  |  Size: 160 B

View file

Before

Width:  |  Height:  |  Size: 977 B

After

Width:  |  Height:  |  Size: 977 B

View file

Before

Width:  |  Height:  |  Size: 937 B

After

Width:  |  Height:  |  Size: 937 B

View file

Before

Width:  |  Height:  |  Size: 163 B

After

Width:  |  Height:  |  Size: 163 B

View file

Before

Width:  |  Height:  |  Size: 719 B

After

Width:  |  Height:  |  Size: 719 B

View file

Before

Width:  |  Height:  |  Size: 855 B

After

Width:  |  Height:  |  Size: 855 B

View file

Before

Width:  |  Height:  |  Size: 591 B

After

Width:  |  Height:  |  Size: 591 B

View file

Before

Width:  |  Height:  |  Size: 782 B

After

Width:  |  Height:  |  Size: 782 B

View file

Before

Width:  |  Height:  |  Size: 587 B

After

Width:  |  Height:  |  Size: 587 B

View file

Before

Width:  |  Height:  |  Size: 805 B

After

Width:  |  Height:  |  Size: 805 B

View file

Before

Width:  |  Height:  |  Size: 368 B

After

Width:  |  Height:  |  Size: 368 B

View file

Before

Width:  |  Height:  |  Size: 160 B

After

Width:  |  Height:  |  Size: 160 B

View file

Before

Width:  |  Height:  |  Size: 368 B

After

Width:  |  Height:  |  Size: 368 B

View file

Before

Width:  |  Height:  |  Size: 643 B

After

Width:  |  Height:  |  Size: 643 B

View file

Before

Width:  |  Height:  |  Size: 718 B

After

Width:  |  Height:  |  Size: 718 B

View file

Before

Width:  |  Height:  |  Size: 621 B

After

Width:  |  Height:  |  Size: 621 B

View file

Before

Width:  |  Height:  |  Size: 687 B

After

Width:  |  Height:  |  Size: 687 B

View file

Before

Width:  |  Height:  |  Size: 621 B

After

Width:  |  Height:  |  Size: 621 B

View file

Before

Width:  |  Height:  |  Size: 782 B

After

Width:  |  Height:  |  Size: 782 B

View file

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

Before

Width:  |  Height:  |  Size: 621 B

After

Width:  |  Height:  |  Size: 621 B

229
src/e07_extra_props.clj Normal file
View file

@ -0,0 +1,229 @@
(ns e07-extra-props
(:require [cljfx.api :as fx]))
(def anchor-pane
{:fx/type :anchor-pane
:children [{:fx/type :label
:anchor-pane/left 10
:anchor-pane/bottom 10
:text "bottom-left"}
{:fx/type :label
:anchor-pane/top 10
:anchor-pane/right 10
:text "top-right"}
{:fx/type :label
:anchor-pane/left 100
:anchor-pane/top 100
:anchor-pane/right 100
:anchor-pane/bottom 100
:style {:-fx-background-color :lightgray
:-fx-alignment :center}
:text "Try resizing window too!"}]})
(def border-pane
{:fx/type :border-pane
:top {:fx/type :label
:border-pane/alignment :center
:border-pane/margin 10
:text "top header"}
:left {:fx/type :label
:border-pane/margin 10
:text "left sidebar"}
:right {:fx/type :label
:border-pane/margin 10
:text "right sidebar"}
:center {:fx/type :label
:border-pane/margin 10
:text "center content"}
:bottom {:fx/type :label
:border-pane/margin 10
:text "bottom footer"}})
(def flow-pane
{:fx/type :flow-pane
:vgap 5
:hgap 5
:padding 5
:children (repeat 100 {:fx/type :rectangle :width 25 :height 25})})
(def grid-pane
{:fx/type :grid-pane
:children (concat
(for [i (range 16)]
{:fx/type :label
:grid-pane/column i
:grid-pane/row i
:grid-pane/hgrow :always
:grid-pane/vgrow :always
:text "boop"})
[{:fx/type :label
:grid-pane/row 2
:grid-pane/column 3
:grid-pane/column-span 2
:text "I am a long label spanning 2 columns"}])})
(def h-box
{:fx/type :h-box
:spacing 5
:children [{:fx/type :label
:text "just label"}
{:fx/type :label
:h-box/hgrow :always
:max-width Double/MAX_VALUE
:style {:-fx-background-color :lightgray}
:text "expanded label"}
{:fx/type :label
:h-box/margin 100
:text "label with big margin"}]})
(def stack-pane
{:fx/type :stack-pane
:children [{:fx/type :rectangle
:width 200
:height 200
:fill :lightgray}
{:fx/type :label
:stack-pane/alignment :bottom-left
:stack-pane/margin 5
:text "stacked label"}
{:fx/type :text-field
:stack-pane/alignment :top-right
:stack-pane/margin 5
:max-width 300
:text "Text field in top-right corner"}]})
(defn- tile-image [{:keys [url]}]
{:fx/type :image-view
:image {:url url
:requested-width 310
:preserve-ratio true
:background-loading true}})
(def tile-pane
{:fx/type :scroll-pane
:fit-to-width false
:content {:fx/type :tile-pane
:pref-columns 3
:hgap 5
:vgap 5
:children [{:fx/type tile-image
:tile-pane/alignment :bottom-center
:url "https://i.imgur.com/oy91jyx.gif"}
{:fx/type tile-image
:tile-pane/alignment :bottom-center
:url "https://i.imgur.com/B4DdoER.png"}
{:fx/type tile-image
:tile-pane/alignment :bottom-center
:url "https://i.imgur.com/mQOeSe5.png"}
{:fx/type tile-image
:tile-pane/alignment :bottom-center
:url "https://i.redd.it/6906qzxo55711.png"}
{:fx/type tile-image
:tile-pane/alignment :bottom-center
:url "https://i.redd.it/810g0l3sgis01.gif"}
{:fx/type tile-image
:tile-pane/alignment :bottom-center
:url "https://i.redd.it/rpkzzc0awr411.gif"}
{:fx/type tile-image
:tile-pane/alignment :bottom-center
:url "http://i.imgur.com/G3dVZpk.jpg"}
{:fx/type tile-image
:tile-pane/alignment :bottom-center
:url "https://i.redd.it/k4hax2x5yyhy.png"}
{:fx/type tile-image
:tile-pane/alignment :bottom-center
:url "https://i.imgur.com/PRxRkne.png"}
{:fx/type tile-image
:tile-pane/alignment :bottom-center
:url "https://i.redd.it/zusrb3sxsz211.gif"}
{:fx/type tile-image
:tile-pane/alignment :bottom-center
:url "https://i.redd.it/fagm2fhxv1yz.gif"}
{:fx/type tile-image
:tile-pane/alignment :bottom-center
:url "https://i.redd.it/w49wc60kys401.gif"}]}})
(def v-box
{:fx/type :v-box
:spacing 5
:fill-width true
:alignment :top-center
:children [{:fx/type :label :text "just label"}
{:fx/type :label
:v-box/vgrow :always
:style {:-fx-background-color :lightgray}
:max-height Double/MAX_VALUE
:max-width Double/MAX_VALUE
:text "expanded label"}]})
(def button-bar
{:fx/type :button-bar
:button-min-width 100
:buttons [{:fx/type :button
:button-bar/button-data :yes
:text "Yes"}
{:fx/type :button
:button-bar/button-data :no
:text "No"}]})
(def split-pane
{:fx/type :split-pane
:divider-positions [0.5]
:items [{:fx/type :label
:split-pane/resizable-with-parent false
:padding 50
:text "This is label that is NOT resizable with parent"}
{:fx/type :label
:padding 50
:text "This is a label resizable with parent"}]})
(fx/on-fx-thread
(fx/create-component
{:fx/type :stage
:showing true
:title "Pane examples"
:scene {:fx/type :scene
:root {:fx/type :tab-pane
:pref-width 960
:pref-height 540
:tabs [{:fx/type :tab
:text "Anchor Pane"
:closable false
:content anchor-pane}
{:fx/type :tab
:text "Border Pane"
:closable false
:content border-pane}
{:fx/type :tab
:text "Flow Pane"
:closable false
:content flow-pane}
{:fx/type :tab
:text "Grid Pane"
:closable false
:content grid-pane}
{:fx/type :tab
:text "HBox"
:closable false
:content h-box}
{:fx/type :tab
:text "Stack Pane"
:closable false
:content stack-pane}
{:fx/type :tab
:text "Tile Pane"
:closable false
:content tile-pane}
{:fx/type :tab
:text "VBox"
:closable false
:content v-box}
{:fx/type :tab
:text "Button Bar"
:closable false
:content button-bar}
{:fx/type :tab
:text "SplitPane"
:closable false
:content split-pane}]}}}))

View file

@ -1,31 +1,56 @@
(ns mw-desktop.core (ns mw-desktop.core
(:require [clojure.java.io :refer [file]] (:require [clojure.string :refer [join]]
[clojure.tools.cli :refer [parse-opts]]) [clojure.tools.cli :refer [parse-opts]]
;; [mw-desktop.fxui :refer [ui]]
[mw-desktop.io :refer [load-ruleset! load-world!]])
(:gen-class)) (:gen-class))
(def defaults
"Defaults for command line arguments."
{:ruleset "rulesets/basic.txt"
:tileset "tilesets/world/"
:world "heightmaps/small_hill.png"})
(def cli-options (def cli-options
[["-f FILEPATH" "--file-path FILEPATH" [["-h" "--help"]
"Set the path to the directory for reading and writing Lisp files." ["-r FILEPATH" "--ruleset FILEPATH" "The ruleset to load"]
:validate [#(and (.exists (file %)) ["-t FILEPATH" "--tileset FILEPATH" "The tileset to load"]
(.isDirectory (file %)) ["-w FILEPATH" "--world FILEPATH" "Choose the world to load"]])
(.canRead (file %))
(.canWrite (file %))) (defn error-msg [errors]
"File path must exist and must be a directory."]] (str "The following errors occurred while parsing your command:\n\n"
["-h" "--help"] (join \newline errors)))
["-p PROMPT" "--prompt PROMPT" "Set the REPL prompt to PROMPT"
:default "Sprecan::"] (defn validate-args
["-r SYSOUTFILE" "--read SYSOUTFILE" "Read Lisp system from file SYSOUTFILE" "Validate command line arguments. Either return a map indicating the program
:validate [#(and should exit (with an error message, and optional ok status), or a map
(.exists (file %)) indicating the action the program should take and the options provided."
(.canRead (file %))) [args]
"Could not find sysout file"]] (let [{:keys [options arguments errors summary]} (parse-opts args cli-options)]
["-s" "--strict" "Strictly interpret the Lisp 1.5 language, without extensions."] (cond
["-t" "--time" "Time evaluations."] (:help options) ; help => exit OK with usage summary
["-x" "--testing" "Disable the jline reader - useful when piping input."]]) {:exit-message (:summary args) :ok? true}
errors ; errors => exit with description of errors
{:exit-message (error-msg errors)}
;; custom validation on arguments
(and (= 1 (count arguments))
(#{"start" "stop" "status"} (first arguments)))
{:action (first arguments) :options options}
:else ; failed custom validation => exit with usage summary
{:exit-message (:summary args)})))
(defn -main (defn -main
"Parse options, print the banner, read the init file if any, and enter the "Parse options, print the banner, read the init file if any, and enter the
read/eval/print loop." read/eval/print loop."
[& opts] [& opts]
(let [args (parse-opts opts cli-options)] (let [args (parse-opts opts cli-options)
(println "Hello, World!"))) options (merge defaults (:options args))]
(load-ruleset! (:ruleset options))
(load-tileset! (:tileset options))
(load-world! (:world options))
(when (:help args) (println (:summary args)) (System/exit 0))
;;(ui options)
(println options)
))

View file

@ -1,252 +0,0 @@
;; this file is cribbed entirely from
;; https://github.com/cljfx/cljfx/blob/master/examples/e12_interactive_development.clj
;; This file is supposed to be explored from the REPL, evaluating forms one
;; by one from top to bottom.
(ns e12-interactive-development
(:require [cljfx.api :as fx]))
;; I want to build an interactive chart that shows how bouncing object falls
;; on the ground. I want to be able to edit gravity and friction to see how
;; it affects object's behavior, so I will put it into state:
(def *state
(atom {:gravity 10
:friction 0.4}))
;; I want to have map event handlers extensible during runtime to avoid full app
;; restarts. One way is using vars instead of functions to get that kind of
;; behavior, but I'll go with another way: multi-methods.
(defmulti event-handler :event/type)
;; Now we'll create our app with dummy root view
(defn root-view [{{:keys [gravity friction]} :state}]
{:fx/type :stage
:showing true
:scene {:fx/type :scene
:root {:fx/type :h-box
:children [{:fx/type :label
:text (str "g = " gravity ", f = " friction)}]}}})
(def renderer
(fx/create-renderer
:middleware (fx/wrap-map-desc (fn [state]
{:fx/type root-view
:state state}))
:opts {:fx.opt/map-event-handler event-handler}))
(fx/mount-renderer *state renderer)
;; At this point, really tiny window appears that displays current gravity and
;; friction. We want to have an ability to change these values, so let's create
;; some slider views for them:
(defn slider-view [{:keys [min max value]}]
{:fx/type :slider
:min min
:max max
:value value})
;; Now we will update our root view to display these sliders:
(defn root-view [{{:keys [gravity friction]} :state}]
{:fx/type :stage
:showing true
:scene {:fx/type :scene
:root {:fx/type :h-box
:children [{:fx/type slider-view
:min 0
:max 100
:value gravity}
{:fx/type slider-view
:min 0
:max 1
:value friction}]}}})
;; Now we updated our root function, but window didn't change. It happens
;; because cljfx has no way to know if definition of some component functions is
;; changed. But we can ask renderer to refresh itself by calling it without any
;; arguments:
(renderer)
;; Now small label got replaced with 2 sliders. Problem is, there are no labels
;; on them, so users can't really see what these sliders mean, so let's fix it:
(defn slider-view [{:keys [min max value label]}]
{:fx/type :v-box
:children [{:fx/type :label
:text label}
{:fx/type :slider
:min min
:max max
:value value
:major-tick-unit max
:show-tick-labels true}]})
(defn root-view [{{:keys [gravity friction]} :state}]
{:fx/type :stage
:showing true
:scene {:fx/type :scene
:root {:fx/type :h-box
:spacing 10
:children [{:fx/type slider-view
:min 0
:max 100
:value gravity
:label "Gravity"}
{:fx/type slider-view
:min 0
:max 1
:label "Friction"
:value friction}]}}})
(renderer)
;; Great, time to add a chart that uses gravity and friction, but first let's
;; try to display something dummy to make sure it works
(defn chart-view [{:keys [gravity friction]}]
{:fx/type :line-chart
:x-axis {:fx/type :number-axis
:label "Time"}
:y-axis {:fx/type :number-axis
:label "Y"}
:data [{:fx/type :xy-chart-series
:name "Position by time"
:data (for [t (range 100)]
{:fx/type :xy-chart-data
:x-value t
:y-value t})}]})
(defn root-view [{{:keys [gravity friction]} :state}]
{:fx/type :stage
:showing true
:scene {:fx/type :scene
:root {:fx/type :v-box
:spacing 20
:children [{:fx/type chart-view
:gravity gravity
:friction friction}
{:fx/type :h-box
:spacing 10
:alignment :center
:children [{:fx/type slider-view
:min 0
:max 100
:value gravity
:label "Gravity"}
{:fx/type slider-view
:min 0
:max 1
:label "Friction"
:value friction}]}]}}})
(renderer)
;; Now chart is added to a window. Everything looks fine, time to do some
;; simulation:
(defn simulate-step [{:keys [velocity y]} gravity friction]
(let [new-velocity (* (- velocity gravity) (- 1 friction))
new-y (+ y new-velocity)]
(if (neg? new-y)
{:velocity (- new-velocity) :y 0}
{:velocity new-velocity :y new-y})))
(defn chart-view [{:keys [gravity friction]}]
{:fx/type :line-chart
:x-axis {:fx/type :number-axis
:label "Time"}
:y-axis {:fx/type :number-axis
:label "Y"}
:data [{:fx/type :xy-chart-series
:name "Position by time"
:data (->> {:velocity 0 :y 100}
(iterate #(simulate-step % gravity friction))
(take 100)
(map-indexed (fn [index {:keys [y]}]
{:fx/type :xy-chart-data
:x-value index
:y-value y})))}]})
(renderer)
(defmethod event-handler ::set-friction [e]
(swap! *state assoc :friction (:fx/event e)))
(defmethod event-handler ::set-gravity [e]
(swap! *state assoc :gravity (:fx/event e)))
(defn slider-view [{:keys [min max value label event]}] ;; add event as arg
{:fx/type :v-box
:children [{:fx/type :label
:text label}
{:fx/type :slider
:min min
:max max
:value value
:on-value-changed {:event/type event} ;; fire it on value
:major-tick-unit max
:show-tick-labels true}]})
(defn root-view [{{:keys [gravity friction]} :state}]
{:fx/type :stage
:showing true
:scene {:fx/type :scene
:root {:fx/type :v-box
:spacing 20
:children [{:fx/type chart-view
:gravity gravity
:friction friction}
{:fx/type :h-box
:spacing 10
:alignment :center
:children [{:fx/type slider-view
:min 0
:max 100
:value gravity
:label "Gravity"
:event ::set-gravity} ;; provide events
{:fx/type slider-view
:min 0
:max 1
:label "Friction"
:value friction
:event ::set-friction}]}]}}})
(renderer)
(swap! *state assoc :gravity 1)
(defn root-view [{{:keys [gravity friction]} :state}]
{:fx/type :stage
:showing true
:scene {:fx/type :scene
:root {:fx/type :v-box
:spacing 20
:children [{:fx/type chart-view
:gravity gravity
:friction friction}
{:fx/type :h-box
:spacing 10
:alignment :center
:children [{:fx/type slider-view
:min 0
:max 5 ;; 100 -> 5
:value gravity
:label "Gravity"
:event ::set-gravity}
{:fx/type slider-view
:min 0
:max 1
:label "Friction"
:value friction
:event ::set-friction}]}]}}})
(renderer)

View file

@ -3,12 +3,7 @@
[clojure.core.cache :refer [lru-cache-factory]] [clojure.core.cache :refer [lru-cache-factory]]
[clojure.java.io :refer [resource]] [clojure.java.io :refer [resource]]
[clojure.string :refer [join lower-case starts-with?]] [clojure.string :refer [join lower-case starts-with?]]
[mw-desktop.state :refer [get-state state update-state!]]) [mw-desktop.state :refer [get-state state update-state!]]))
(:import [java.awt Desktop]
[java.io File]
[java.net URI]
[org.commonmark.node Node]
[org.commonmark.parser Parser]))
;; OK, the basic idea here is we have a window divided vertically ;; OK, the basic idea here is we have a window divided vertically
;; into two panes. The user can drag the division between the panes ;; into two panes. The user can drag the division between the panes
@ -56,6 +51,7 @@
;; 'piechart count group by state' ;; 'piechart count group by state'
;; ;;
;; In which case you probably have one graph page per rule. ;; In which case you probably have one graph page per rule.
(defn- tile-image [{:keys [url]}] (defn- tile-image [{:keys [url]}]
{:fx/type :image-view {:fx/type :image-view
:image {:url url :image {:url url
@ -83,8 +79,7 @@
:children (map (fn [cell]{:fx/type tile-image :children (map (fn [cell]{:fx/type tile-image
:tile-pane/alignment :bottom-center :tile-pane/alignment :bottom-center
:url (resource (format "%s/%s.png" tileset (:state cell)))}) :url (resource (format "%s/%s.png" tileset (:state cell)))})
(flatten world)) (flatten world))}))
}))
(defn root-view [{{:keys [world rules]} :state}] (defn root-view [{{:keys [world rules]} :state}]
{:fx/type :stage {:fx/type :stage
@ -94,6 +89,15 @@
:items [{:fx.type :scroll-pane :items [{:fx.type :scroll-pane
:content {:fx/type world-view}}]}}}) :content {:fx/type world-view}}]}}})
(defmulti handle-event :event/type)
(defmethod handle-event :default [e]
(prn e))
(defmethod handle-event ::type-text [{:keys [fx/event fx/context]}]
{:context (fx/swap-context context assoc :typed-text event)})
(defmulti event-handler (defmulti event-handler
"Multi-method event handler cribbed from e12-interactive-development" "Multi-method event handler cribbed from e12-interactive-development"
:event/type) :event/type)
@ -107,3 +111,12 @@
:opts {:fx.opt/map-event-handler event-handler})) :opts {:fx.opt/map-event-handler event-handler}))
(fx/mount-renderer state renderer) (fx/mount-renderer state renderer)
;; (defn ui
;; [_config]
;; (update-state! :ui
;; (fx/create-app state
;; :event-handler event-handler
;; :desc-fn (fn [state]
;; {:fx/type root-view
;; :state state}))))

98
src/mw_desktop/io.clj Normal file
View file

@ -0,0 +1,98 @@
(ns mw-desktop.io
"Handle loading (and, ultimately, saving) files for the MicroWorld desktop
app."
(:require [clojure.java.io :refer [as-file file input-stream resource]]
[clojure.string :refer [split starts-with?]]
[mw-desktop.state :refer [update-state!]]
[mw-engine.heightmap :refer [apply-heightmap]]
[mw-engine.world :refer [world?]]
[mw-parser.declarative :refer [compile]]
[pantomime.mime :refer [mime-type-of]])
(:import [java.net URL]))
(defn identify-resource
"Identify whether `path` represents a file, a resource (preferring the file
if both exist) or URL, identify the MIME type of the content, and return a
map with keys `:stream` whose value is an open stream on the content and
`type` whose value is the MIME type of the content."
[path]
(let [f (.exists (file path))
r (resource path)
u (when-not (or f r) (URL. path))
p' (cond (and f r) (file path)
r r
u u
:else path)]
{:path p'
:stream (input-stream p')
:type (mime-type-of p')}))
(defn load-ruleset!
"Load a ruleset from `path`, which may be either a file path or a resource
path, and should indicate a text file of valid MicroWorld rules.
Where a file and resource with this `path` both exist, the file is
preferred. Updates global state."
[path]
(update-state!
:rules
(doall (compile (slurp (:stream (identify-resource path)))))))
(defn assemble-tile-set
"Return a map of image files in the directory at this `dir-path`, keyed by
keywords formed from their basename without extension."
[dir-path]
(let [tiles (file-seq (as-file dir-path))]
(into {}
(map
#(vector
(keyword
(first
(split (.getName %) #"\.")))
%)
(filter #(starts-with? (mime-type-of %) "image")
(remove #(.isDirectory %) tiles))))))
(defn load-tileset!
"Load a tileset from `path`, which may be either a file path or a resource
path, and should indicate a directory containing same-size image files.
Where a file and resource with this `path` both exist, the file is
preferred. Updates global state."
[path]
(let [{p' :path} (identify-resource path)
dir-path (as-file p')]
(if (.isDirectory dir-path)
(update-state! :tileset (assemble-tile-set dir-path))
(throw (ex-info "Tileset should be a directory of image files"
{:path path
:expanded dir-path}))))
(update-state!
:tileset
(doall (compile (slurp (:stream (identify-resource path)))))))
(defn load-world!
"Load a world from `path`, which may be either a file path or a resource
path, and may indicate either a world file (EDN) or a heightmap (image).
Where a file and resource with this `path` both exist, the file is
preferred. Updates global state."
[path]
(let [data (identify-resource path)
{type :type stream :stream} data
world (try (if (starts-with? type "image/")
(apply-heightmap stream)
(read-string (slurp stream)))
(catch Exception any
(throw
(ex-info
(format
"Failed to read `%s` as either EDN or heightmap."
path)
(merge data {:path path})
any))))]
(if (world? world) (do (update-state! :world world) world)
(throw (ex-info "Invalid world file?"
(merge data {:path path
:data world}))))))

View file

@ -1,9 +1,7 @@
;; lightly adapted from ;; lightly adapted from
;; https://github.com/cljfx/cljfx/blob/master/examples/e20_markdown_editor.clj ;; https://github.com/cljfx/cljfx/blob/master/examples/e20_markdown_editor.clj
(ns mw-desktop.e20-markdown-editor (ns mw-desktop.markdown
(:require [cljfx.api :as fx] (:require [cljfx.api :refer [sub-ctx sub-val swap-context]]
[clojure.core.cache :refer [lru-cache-factory]]
[clojure.java.io :refer [resource]]
[clojure.string :refer [join lower-case starts-with?]]) [clojure.string :refer [join lower-case starts-with?]])
(:import [java.awt Desktop] (:import [java.awt Desktop]
[java.io File] [java.io File]
@ -11,14 +9,6 @@
[org.commonmark.node Node] [org.commonmark.node Node]
[org.commonmark.parser Parser])) [org.commonmark.parser Parser]))
;; does not work any more :(
#_(SvgImageLoaderFactory/install (PrimitiveDimensionProvider.))
(def *context
(atom
(fx/create-context {:typed-text (slurp (resource "doc/grammar.md"))}
#(lru-cache-factory % :threshold 4096))))
(defn commonmark->clj [^Node node] (defn commonmark->clj [^Node node]
(let [tag (->> node (let [tag (->> node
.getClass .getClass
@ -49,7 +39,7 @@
(defn node-sub [context] (defn node-sub [context]
(-> (Parser/builder) (-> (Parser/builder)
.build .build
(.parse (fx/sub-val context :typed-text)) (.parse (sub-val context :typed-text))
commonmark->clj)) commonmark->clj))
(defmulti handle-event :event/type) (defmulti handle-event :event/type)
@ -58,7 +48,7 @@
(prn e)) (prn e))
(defmethod handle-event ::type-text [{:keys [fx/event fx/context]}] (defmethod handle-event ::type-text [{:keys [fx/event fx/context]}]
{:context (fx/swap-context context assoc :typed-text event)}) {:context (swap-context context assoc :typed-text event)})
(defmulti md->fx :tag) (defmulti md->fx :tag)
@ -235,38 +225,8 @@
{:fx/type md-view {:fx/type md-view
:node node})}]}) :node node})}]})
(defn note-input [{:keys [fx/context]}] (defn markdown-view [{:keys [fx/context]}]
{:fx/type :text-area
:style-class "input"
:text (fx/sub-val context :typed-text)
:on-text-changed {:event/type ::type-text :fx/sync true}})
(defn note-preview [{:keys [fx/context]}]
{:fx/type :scroll-pane {:fx/type :scroll-pane
:fit-to-width true :fit-to-width true
:content {:fx/type md-view :content {:fx/type md-view
:node (fx/sub-ctx context node-sub)}}) :node (sub-ctx context node-sub)}})
(def app
(fx/create-app *context
:event-handler handle-event
:desc-fn (fn [_]
{:fx/type :stage
:showing true
:width 960
:height 540
:scene {:fx/type :scene
:stylesheets #{"doc/markdown.css"}
:root {:fx/type :grid-pane
:padding 10
:hgap 10
:column-constraints [{:fx/type :column-constraints
:percent-width 100/2}
{:fx/type :column-constraints
:percent-width 100/2}]
:row-constraints [{:fx/type :row-constraints
:percent-height 100}]
:children [{:fx/type note-input
:grid-pane/column 0}
{:fx/type note-preview
:grid-pane/column 1}]}}})))

View file

@ -1,23 +1,24 @@
(ns mw-desktop.state (ns mw-desktop.state
"Global state of the application." "Global state of the application."
(:require [mw-engine.utils :refer [member?]] (:require [mw-engine.world :refer [world?]]
[mw-engine.world :refer [world?]]) [taoensso.timbre :refer [info]])
(:import [clojure.lang Keyword])) (:import [clojure.lang Keyword]))
(def valid-states (def valid-statuses
#{:init :halted :halt-requested :running}) #{:init :halted :halt-requested :running})
(def state (def state
"Global state of the application." "Global state of the application."
(atom {:state :init})) (atom {:status :init}))
(defn get-state [^Keyword key] (defn get-state [^Keyword key]
(@state key)) (@state key))
(defn await-state (defn await-status
"Pause the current process until the global status is in the state `state-value`." "Pause the current process until the global status is in the state `status-value`."
[state-value] [status-value]
(while (not= (@state :state) state-value) (while (not= (@state :status) status-value)
(info "Awaiting status %s" status-value)
(Thread/sleep 10000))) (Thread/sleep 10000)))
(defn update-state! (defn update-state!
@ -34,15 +35,16 @@
(when (and (:world deltas) (not (world? (:world deltas)))) (when (and (:world deltas) (not (world? (:world deltas))))
(throw (ex-info "Attempt to set an invalid world" (throw (ex-info "Attempt to set an invalid world"
{:deltas deltas {:deltas deltas
:state @state}))) :status @state})))
;; you can't change either the world or the rules while the engine is computing ;; you can't change either the world or the rules while the engine is
;; a new status for the world. ;; computing a new state for the world.
(when-not (= (@state :state) :init) (when (and (#{:world :rules} (keys deltas))
(when (or (member? (keys deltas) :world) (#{:running :halt-requested} (@state :status)))
(member? (keys deltas) :rules)) (await-status :halted))
(await-state :halted)) )
(swap! state merge deltas) (swap! state merge deltas)
(when (and (= (@state :state) :init) ;; if we've got both a world and rules, and we're in state :init, we
(:world state) ;; advance to state :halted
(:rules state)) (when (and (= (@state :status) :init)
(swap! state merge {:state :halted})))) (:world @state)
(:rules @state))
(swap! state merge {:status :halted}))))

View file

@ -1,157 +0,0 @@
(ns mw-desktop.swing-ui
(:require [clojure.java.io :refer [resource]]
[clojure.string :refer [join]]
[markdown.core :refer [md-to-html-string]]
[mw-desktop.state :refer [get-state update-state!]]
[seesaw.core :refer [border-panel editor-pane frame
left-right-split menu menu-item menubar native! pack!
scrollable separator show! tabbed-panel table text]])
(:import [org.htmlcleaner CleanerProperties HtmlCleaner SimpleHtmlSerializer]))
;; This is probably a dead end. Its performance is terrible; the fxui version looks
;; much more promising.
;; OK, the basic idea here is we have a window divided vertically
;; into two panes. The user can drag the division between the panes
;; left and right. In the left pane is always the world; in the right, a
;; number of pages can be displayed.
;;
;; 1. Documentation;
;; 2. The rule editor;
;; 3. The log;
;; 4. Data on what states are in use (and how many of each);
;; 5. Some way to get data on other properties (for the mutual aid model, we
;; want to see how much food in total there is in the world, how much the
;; richest centile has, how much the poorest, and how that's changing over
;; time; but whether I have the skill to make that something the user can
;; configure is another matter).
;;
;; There is a File menu with options to:
;;
;; 1. Save the world as an EDN file;
;; 2. Load the world from an EDN file;
;; 3. Create a world from a height map;
;; 4. Load a rules file;
;; 5. Save a rules file;
;; 4. Load (? or register?) a tile set (probably as a jar file?);
;;
;; There is a World menu with options to:
;;
;; 1. Start the world running;
;; 2. Pause/Stop the run;
;; 3. Select a tile set;
;;
;; There is a View menu with options to change the display in the right
;; hand pane.
;;
;; 1. The rule editor;
;; 2. The documentation;
;; 3. Stats displays (but this needs more thought and experimentation)
;;
;; One thought is I might define new rule language to create graphs and charts.
;;
;; 'timeseries total food where state is house group by decile'
;; 'timeseries total food where state is house, fertility where state is pasture or crop or fallow'
;; 'graph fertility by altitude'
;; 'barchart fertility by state'
;; 'piechart count group by state'
;;
;; In which case you probably have one graph page per rule.
(update-state! :world-view (table :model [:columns [{:key :name, :text "Name"} :likes]
:rows '[["Bobby" "Laura Palmer"]
["Agent Cooper" "Cherry Pie"]
{:likes "Laura Palmer" :name "James"}
{:name "Big Ed" :likes "Norma Jennings"}]])
:rule-editor (text :editable? true
:id :rule-editor
:multi-line? true
:text ";; This is where you will write your rules.")
:error-panel (text :editable? false
:id :error-panel
:foreground "maroon"
:multi-line? true
:text ";; Errors will be shown here."))
(defn markdown->html
"`md-to-html-string` returns an HTML fragment that `editor-pane` chokes on.
This is an attempt to do better. It sort-of works -- produces nice clean
HTML -- but the performance of `editor-pane` is still unacceptably poor"
[md-text]
(let [props (CleanerProperties.)]
(.setOmitDoctypeDeclaration props false)
(.setOmitDeprecatedTags props true)
(.setOmitUnknownTags props true)
(.setOmitXmlDeclaration props true)
(.getAsString (SimpleHtmlSerializer. props)
(.clean (HtmlCleaner. props) (md-to-html-string md-text)))))
(defn make-multi-view
"Make the right hand multi-view panel."
[]
(tabbed-panel
:tabs [{:title "Rules"
:content (border-panel
:center (scrollable
(border-panel
:center (get-state :rule-editor)
:west (text :columns 4
:editable? false
:foreground "cornflowerblue"
:id :line-numbers
:multi-line? true
:text (join "\n"
(map str (range 1 1000)))
:wrap-lines? false)))
:south (scrollable
(get-state :error-panel)))}
{:title "Grammar"
:content (scrollable
;; the performance of laying out HTML in an editor-pane
;; is painful! RTF is better but not good, and unreliable.
(editor-pane :editable? false
;; :multi-line? true
:content-type "text/plain"
:text (slurp
(resource "doc/grammar.md"))
;; :wrap-lines? true
))}]))
(update-state! :multi-view (make-multi-view))
(defn create-app-window
"Create the app window."
[]
(native!)
(update-state! :app-window
(pack!
(frame :title "MicroWorld"
;; :size [600 :by 600]
;; :on-close :exit
:menubar (menubar
:items
[(menu :text "World" :items
[(menu-item :text "Run World" :enabled? false)
(menu-item :text "Halt Run" :enabled? false)
(separator)
(menu :text "Create World..." :items
[(menu-item :text "From Heightmap...")
(menu-item :text "From Coordinates...")])
(menu-item :text "Load World File...")
(menu-item :text "Save World File As...")
(separator)
(menu-item :text "Import Tile Set...")])
(menu :text "Rules" :items
[(menu-item :text "New Rule Set")
(menu-item :text "Open Rule Set...")
(menu-item :text "Save Rule Set")
(menu-item :text "Save Rule Set As...")
(separator)
(menu-item :text "Compile Rules")])
(menu :text "Help" :items
[(menu-item :text "About MicroWorld")])])
:content (left-right-split (scrollable (get-state :world-view))
(scrollable (get-state :multi-view))
:divider-location 8/10)))))
(defn show-app-window [] (show! (get-state :app-window)))