OK, we have a working documentation browser.

That's a small win, but *something* works.
This commit is contained in:
Simon Brooke 2023-07-30 16:24:47 +01:00
parent 272de653d8
commit 826c675e8a
7 changed files with 406 additions and 45 deletions

View file

@ -2,8 +2,8 @@
-fx-line-spacing: 4; -fx-line-spacing: 4;
-fx-font-family: Ubuntu; -fx-font-family: Ubuntu;
-fx-font-size: 14; -fx-font-size: 14;
-fx-text-fill: #ddd; -fx-text-fill: black;
-fx-fill: #ddd; -fx-fill: black;
} }
.root { .root {

View file

@ -54,7 +54,7 @@ all cells have x more than -1.
If you are having problems because one of your rules isn't working, look to If you are having problems because one of your rules isn't working, look to
see whether there is another rule above it which is 'blocking' it. see whether there is another rule above it which is 'blocking' it.
### <a name="grammar"></a>Grammar ### Grammar
#### Comments #### Comments
@ -77,7 +77,7 @@ In rules, _conditions_ is one of:
+ _condition_ and _conditions_ + _condition_ and _conditions_
+ _condition_ or _conditions_ + _condition_ or _conditions_
Note that 'and' takes precedence over or, so Note that `and` takes precedence over `or`, so
conditionA and conditionB or conditionC and conditionD conditionA and conditionB or conditionC and conditionD
@ -214,3 +214,5 @@ Copyright © 2014 [Simon Brooke](mailto:simon@journeyman.cc)
Distributed under the terms of the Distributed under the terms of the
[GNU General Public License v2](http://www.gnu.org/licenses/gpl-2.0.html) [GNU General Public License v2](http://www.gnu.org/licenses/gpl-2.0.html)
-----

View file

@ -1,9 +1,9 @@
(ns mw-desktop.fxui (ns mw-desktop.fxui
(:require [cljfx.api :as fx] (:require [cljfx.api :as fx]
[clojure.core.cache :refer [lru-cache-factory]] [mw-desktop.state :refer [state]]
[clojure.java.io :refer [resource]] [mw-desktop.ui-components.documentation-browser :refer [doc-browser]]
[clojure.string :refer [join lower-case starts-with?]] [mw-desktop.ui-components.rule-editor :refer [rule-editor]]
[mw-desktop.state :refer [get-state state update-state!]])) [mw-desktop.ui-components.world-view :refer [world-view]]))
;; 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
@ -52,43 +52,35 @@
;; ;;
;; 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 root-view [{{:keys []} :state}]
{:fx/type :image-view
:image {:url url
:requested-width 20
:preserve-ratio true
:background-loading true}})
(defn world-view [{:keys [world tileset]}]
;; assumes that by the time we get here, a tileset is a clojure map
;; in which the keys are the names of the tiles, without file extension, as
;; keywords (i.e. they're states, from the point of view of the world), and
;; in which the values are just java images (bitmaps), or else maps which
;; wrap java images with some other related data for example dimensions.
(let [th (or (:height (first tileset)) 20)
tw (or (:width (first tileset)) 20)
cols (count (first world))
rows (count world)]
{:fx/type :tile-pane
:hgap 0
:pref-columns cols
:pref-rows rows
:pref-tile-height th
:pref-tile-width tw
:vgap 0
:children (map (fn [cell]{:fx/type tile-image
:tile-pane/alignment :bottom-center
:url (resource (format "%s/%s.png" tileset (:state cell)))})
(flatten world))}))
(defn root-view [{{:keys [world rules]} :state}]
{:fx/type :stage {:fx/type :stage
:showing true :showing true
:title "MicroWorld"
:scene {:fx/type :scene :scene {:fx/type :scene
:stylesheets #{"doc/markdown.css"}
:root {:fx/type :split-pane :root {:fx/type :split-pane
:items [{:fx.type :scroll-pane :items [{:fx/type :stack-pane
:content {:fx/type world-view}}]}}}) :children [{:fx/type :scroll-pane
:content {:fx/type world-view}}
{:fx/type :label
:stack-pane/alignment :top-right
:stack-pane/margin 5
:max-width 300
;; :text status
}]}
{:fx/type :tab-pane
:tabs [;; {:fx/type :tab
;; :text "Rule Editor"
;; :closable false
;; :content rule-editor
;; :parse-errors (or parse-errors "No errors found")
;; :rules-file (or rules-file "Unnamed file")
;; :rules-src (or rules-src ";; enter your rules here")
;; }
{:fx/type :tab
:text "Help"
:closable false
:content doc-browser}]}]}}})
(defmulti handle-event :event/type) (defmulti handle-event :event/type)

View file

@ -34,8 +34,9 @@
Where a file and resource with this `path` both exist, the file is Where a file and resource with this `path` both exist, the file is
preferred. Updates global state. Returns the ruleset loaded." preferred. Updates global state. Returns the ruleset loaded."
[path] [path]
(let [rules (doall (compile (slurp (:stream (identify-resource path)))))] (let [src (slurp (:stream (identify-resource path)))
(update-state! :rules rules) rules (doall (compile src))]
(update-state! :rules rules :rules-file path :rules-src src)
rules)) rules))
(defn assemble-tile-set (defn assemble-tile-set
@ -93,7 +94,7 @@
path) path)
(merge data {:path path}) (merge data {:path path})
any))))] any))))]
(if (world? world) (do (update-state! :world world) world) (if (world? world) (do (update-state! :world world :world-file path) world)
(throw (ex-info "Invalid world file?" (throw (ex-info "Invalid world file?"
(merge data {:path path (merge data {:path path
:data world})))))) :data world}))))))

View file

@ -0,0 +1,260 @@
(ns mw-desktop.ui-components.documentation-browser
"A tab in which documentation can be presented. Cribbed pretty much in
entirety from the cljfx example `e20-markdown-editor`."
(:require [cljfx.api :as fx]
[clojure.core.cache :refer [lru-cache-factory]]
[clojure.java.io :refer [resource]]
[clojure.string :as str]
[mw-desktop.state :refer [state]])
(:import [java.awt Desktop]
[java.io File]
[java.net URI]
[org.commonmark.node Node]
[org.commonmark.parser Parser]))
(defn commonmark->clj [^Node node]
(let [tag (->> node
.getClass
.getSimpleName
(re-seq #"[A-Z][a-z]+")
(map str/lower-case)
(str/join "-")
keyword)
all-attrs (->> node
bean
(map (fn [[k v]]
[(->> k
name
(re-seq #"[A-Z]?[a-z]+")
(map str/lower-case)
(str/join "-")
keyword)
v]))
(into {}))]
{:tag tag
:attrs (dissoc all-attrs :next :previous :class :first-child :last-child :parent)
:children (->> node
.getFirstChild
(iterate #(.getNext ^Node %))
(take-while some?)
(mapv commonmark->clj))}))
(defn node-sub [context]
(-> (Parser/builder)
.build
(.parse (fx/sub-val context :typed-text))
commonmark->clj))
(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 md->fx :tag)
(defn md-view [{:keys [node]}]
(md->fx node))
(defmethod md->fx :heading [{children :children {:keys [level]} :attrs}]
{:fx/type :text-flow
:style-class ["heading" (str "level-" level)]
:children (for [node children]
{:fx/type md-view
:node node})})
(defmethod md->fx :paragraph [{children :children}]
{:fx/type :text-flow
:style-class "paragraph"
:children (for [node children]
{:fx/type md-view
:node node})})
(defmethod md->fx :text [{{:keys [literal]} :attrs}]
{:fx/type :text
:cache true
:cache-hint :speed
:text literal})
(defmethod md->fx :code [{{:keys [literal]} :attrs}]
{:fx/type :label
:cache true
:cache-hint :speed
:style-class "code"
:text literal})
(defmethod md->fx :fenced-code-block [{{:keys [literal]} :attrs}]
{:fx/type :v-box
:padding {:top 9}
:children [{:fx/type :scroll-pane
:style-class ["scroll-pane" "code-block"]
:fit-to-width true
:content {:fx/type :label
:cache true
:cache-hint :speed
:max-width ##Inf
:min-width :use-pref-size
:text literal}}]})
(defmethod md->fx :indented-code-block [{{:keys [literal]} :attrs}]
{:fx/type :v-box
:padding {:top 9}
:children [{:fx/type :scroll-pane
:style-class ["scroll-pane" "code-block"]
:fit-to-width true
:content {:fx/type :label
:cache true
:cache-hint :speed
:max-width ##Inf
:min-width :use-pref-size
:text literal}}]})
(defmethod md->fx :link [{{:keys [^String destination]} :attrs children :children}]
(let [link {:fx/type :hyperlink
:on-action (fn [_]
(future
(try
(if (str/starts-with? destination "http")
(.browse (Desktop/getDesktop) (URI. destination))
(.open (Desktop/getDesktop) (File. destination)))
(catch Exception e
(.printStackTrace e)))))}]
(if (and (= 1 (count children))
(= :text (:tag (first children))))
(assoc link :text (-> children first :attrs :literal))
(assoc link :graphic {:fx/type :h-box
:children (for [node children]
{:fx/type md-view
:node node})}))))
(defmethod md->fx :strong-emphasis [{:keys [children]}]
(if (and (= 1 (count children))
(= :text (:tag (first children))))
{:fx/type :text
:cache true
:cache-hint :speed
:style-class "strong-emphasis"
:text (-> children first :attrs :literal)}
{:fx/type :h-box
:cache true
:style-class "strong-emphasis"
:children (for [node children]
{:fx/type md-view
:node node})}))
(defmethod md->fx :emphasis [{:keys [children]}]
(if (and (= 1 (count children))
(= :text (:tag (first children))))
{:fx/type :text
:cache true
:cache-hint :speed
:style-class "emphasis"
:text (-> children first :attrs :literal)}
{:fx/type :h-box
:style-class "emphasis"
:children (for [node children]
{:fx/type md-view
:node node})}))
(defmethod md->fx :soft-line-break [_]
{:fx/type :text
:text " "})
(defmethod md->fx :document [{:keys [children]}]
{:fx/type :v-box
:style-class "document"
:children (for [node children]
{:fx/type md-view
:node node})})
(defmethod md->fx :image [{{:keys [destination]} :attrs}]
{:fx/type :image-view
:image {:url (if (str/starts-with? destination "http")
destination
(str "file:" destination))
:background-loading true}})
(defmethod md->fx :bullet-list [{{:keys [bullet-marker]} :attrs children :children}]
{:fx/type :v-box
:style-class "md-list"
:children (for [node children]
{:fx/type :h-box
:alignment :baseline-left
:spacing 4
:children [{:fx/type :label
:min-width :use-pref-size
:cache true
:cache-hint :speed
:text (str bullet-marker)}
{:fx/type md-view
:node node}]})})
(defmethod md->fx :ordered-list [{{:keys [delimiter start-number]} :attrs
children :children}]
{:fx/type :v-box
:style-class "md-list"
:children (map (fn [child number]
{:fx/type :h-box
:alignment :baseline-left
:spacing 4
:children [{:fx/type :label
:cache true
:cache-hint :speed
:min-width :use-pref-size
:text (str number delimiter)}
(assoc (md->fx child)
:h-box/hgrow :always)]})
children
(range start-number ##Inf))})
(defmethod md->fx :list-item [{:keys [children]}]
{:fx/type :v-box
:children (for [node children]
{:fx/type md-view
:node node})})
(defmethod md->fx :default [{:keys [tag attrs children]}]
{:fx/type :v-box
:children [{:fx/type :label
:cache true
:cache-hint :speed
:style {:-fx-background-color :red}
:text (str tag " " attrs)}
{:fx/type :v-box
:padding {:left 10}
:children (for [node children]
{:fx/type md-view
:node node})}]})
(def *context
(atom
(fx/create-context
{:typed-text (subs (slurp (resource "doc/mw-parser.md")) 0 10000)}
#(lru-cache-factory % :threshold 498))))
(defn doc-browser
[{:keys [fx/context]}]
{:fx/type :scroll-pane
:fit-to-width true
:content {:fx/type md-view
:node (fx/sub-ctx context node-sub)}})
(defmulti handle-event :event/type)
(defmethod handle-event :default [e]
(prn e))
(def app
"Test purposes only"
(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 doc-browser}}})))

View file

@ -0,0 +1,34 @@
(ns mw-desktop.ui-components.rule-editor
(:require [cljfx.api :as fx]
[clojure.string :refer [join]]
[mw-desktop.state :refer :all]))
(defn rule-editor
"Rule editor comprises a scrolling container, with within it,
1. name of loaded rule file (non editable, visually distinguished) at the
top;
2. line numbers in a column on the left;
3. editable rules source text in the middle;
4. parser/compiler errors in a panel as the bottom."
[{{:keys [rules-file rules-src parse-errors]} :state}]
{:fx/type :scroll-pane
:content {:fx/type :border-pane
:top {:fx/type :label
:border-pane/alignment :center
:border-pane/margin 2
:text rules-file}
:left {:fx/type :label
:border-pane/margin 2
:text (join "\n"
(map #(format "%5d" %)
(range 1 (inc (count (re-seq #"\n" rules-src))))))}
:center {:fx/type :text-area
:style-class "input"
:text (fx/sub-val rules-src :typed-text)
:on-text-changed {:event/type ::type-text :fx/sync true}}
:bottom {:fx/type :label
:border-pane/margin 2
:text (or parse-errors "error messages will appear here.")}}
}
)

View file

@ -0,0 +1,72 @@
(ns mw-desktop.ui-components.world-view
"View of the world"
(:require [cljfx.api :as fx]
[clojure.java.io :refer [as-url resource]]
[mw-desktop.io :refer [load-world!]]
[mw-desktop.state :refer [state]]))
;; World view is essentially a grid of tiles, one per cell. In the long term
;; I want a context menu, allowing the user to
;;
;; 1. inspect cell properties;
;; 2. change non-protected properties, including state;
;; 3. browse cell history in a user-friendly presentation.
;;
;; In the short term, just being able to scroll around the grid is enough.
(defn- tile-image [{:keys [url]}]
{:fx/type :image-view
:image {:url url
:requested-width 20
:preserve-ratio true
:background-loading true}})
(defn assemble-tiles
[world tiles]
(map (fn [cell]
{:fx/type tile-image
:tile-pane/alignment :bottom-center
:url (as-url (or (tiles (:state cell))
(resource "tilesets/world/error.png")))})
(flatten world)))
(defn world-view
[{{:keys [world tileset]} :state}]
;; assumes that by the time we get here, a tileset is a clojure map
;; in which the keys are the names of the tiles, without file extension, as
;; keywords (i.e. they're states, from the point of view of the world), and
;; in which the values are URLs which point to image files.
(let [th (or (:height (first tileset)) 20)
tw (or (:width (first tileset)) 20)
cols 48 ;; (count (first world))
rows 47;; (count world)
children (assemble-tiles world tileset)]
{:fx/type :tile-pane
:hgap 0
:pref-columns cols
:pref-rows rows
:pref-tile-height th
:pref-tile-width tw
:vgap 0
:children children}))
;;; From this point on we're just constructing a test harness to launch the
;;; component in isolation.
(load-world! "init-state/world.edn")
(defmulti handle-event :event/type)
(defmethod handle-event :default [e]
(prn e))
(def app
"Test purposes only"
(fx/create-app state
:event-handler handle-event
:desc-fn (fn [_]
{:fx/type :stage
:showing true
:width 960
:height 540
:scene {:fx/type :scene
:root {:fx/type world-view}}})))