packages feed

FULE (empty) → 0.3.0

raw patch · 33 files changed

+2926/−0 lines, 33 filesdep +FULEdep +basedep +containerssetup-changed

Dependencies added: FULE, base, containers, deepseq, mtl, transformers

Files

+ CHANGELOG.md view
@@ -0,0 +1,38 @@+# Changelog for `FULE`++All notable changes to this project will be documented in this file.++The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),+and this project adheres to the+[Haskell Package Versioning Policy](https://pvp.haskell.org/).++## Unreleased++## 0.3.0+ - Made `Sized`'s `sized` function's size a single argument+ - Made the `Sized` container properly override the inherent size of content.+ - Made Window size a single argument+ - Corrected bounds on dependencies+ - Updated documentation+ - Reorganized (and split) modules++## 0.2.0.3 - 2023-11-16+ - Tweaked documentation for release to Hackage++## 0.2.0.2 - 2023-10-27+ - Fixed constraints+ - Updated documentation++## 0.2.0.1 - 2023-10-27+ - Fully corrected the construction of the transform matrix in the layout `build` function+ - Updated the theory documentation++## 0.2.0 - 2023-10-20+ - Altered construction of the transform matrix in the layout `build` function to fix some issues+ - Updated the theory documentation to describe the new transform matrix+ - Now applying the full transform matrix instead of just the propagated matrix to the constraint matrices in the layout `build` function+ - Changed the grid container API to accept a tuple rather than two separate arguments for specifying the number of rows and columns the grid should have.++## 0.1.0 - 2023-09-28+Initial release.+
+ FULE.cabal view
@@ -0,0 +1,86 @@+cabal-version: 1.12++-- This file has been generated from package.yaml by hpack version 0.36.0.+--+-- see: https://github.com/sol/hpack++name:           FULE+version:        0.3.0+synopsis:       Functional UI Layout Engine+description:    Please see the README on GitHub at <https://github.com/pschnapp/FULE#readme>+category:       GUI+homepage:       https://github.com/pschnapp/FULE#readme+bug-reports:    https://github.com/pschnapp/FULE/issues+author:         Paul Schnapp+maintainer:     paul.schnapp@gmail.com+copyright:      2022, 2023 Paul Schnapp+license:        BSD3+license-file:   LICENSE+build-type:     Simple+extra-source-files:+    README.md+    CHANGELOG.md+    doc/Theory.md+    doc/Usage.md++source-repository head+  type: git+  location: https://github.com/pschnapp/FULE++library+  exposed-modules:+      FULE+      FULE.Component+      FULE.Container+      FULE.Container.Arrayed+      FULE.Container.Clipped+      FULE.Container.Config+      FULE.Container.Config.Orientation+      FULE.Container.Config.Padding+      FULE.Container.Config.SizedContent+      FULE.Container.Divided+      FULE.Container.Grid+      FULE.Container.Item+      FULE.Container.Layered+      FULE.Container.Padded+      FULE.Container.Positioned+      FULE.Container.Sized+      FULE.Container.Unreckoned+      FULE.Container.Window+      FULE.Internal.Sparse+      FULE.Internal.Util+      FULE.Layout+      FULE.LayoutOp+  other-modules:+      Paths_FULE+  hs-source-dirs:+      src+  ghc-options: -Wredundant-constraints+  build-depends:+      base >=4.7 && <5+    , containers >=0.6.6 && <0.7+    , deepseq >=1.4.8 && <1.5+    , mtl >=2.2.2 && <2.3+    , transformers >=0.5.6 && <0.6+  default-language: Haskell2010++test-suite FULE-test+  type: exitcode-stdio-1.0+  main-is: Spec.hs+  other-modules:+      Divided+      Grid+      Layout+      Paths_FULE+  hs-source-dirs:+      examples+      test+  ghc-options: -Wredundant-constraints -threaded -rtsopts -with-rtsopts=-N+  build-depends:+      FULE+    , base >=4.7 && <5+    , containers >=0.6.6 && <0.7+    , deepseq >=1.4.8 && <1.5+    , mtl >=2.2.2 && <2.3+    , transformers >=0.5.6 && <0.6+  default-language: Haskell2010
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright Paul Schnapp (c) 2022, 2023++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++    * Redistributions of source code must retain the above copyright+      notice, this list of conditions and the following disclaimer.++    * Redistributions in binary form must reproduce the above+      copyright notice, this list of conditions and the following+      disclaimer in the documentation and/or other materials provided+      with the distribution.++    * Neither the name of Author name here nor the names of other+      contributors may be used to endorse or promote products derived+      from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,27 @@+# FULE++The Functional UI Layout Engine (FULE - pronounced "fuel") is an experimental Haskell library for positioning UI elements on-screen. The layout will react dynamically to changes in window-size or manipulations by the user, but it is up to you to wire these events from the GUI framework to the FULE layout and to draw the components themselves on the screen.++Check out the [usage documentation](https://github.com/pschnapp/FULE/blob/main/doc/Usage.md) and [examples](https://github.com/pschnapp/FULE/blob/main/examples) for more information, or if curious you can dive into [the theory behind it](https://github.com/pschnapp/FULE/blob/main/doc/Theory.md).++The [osabe project](https://bitbucket.org/osabe-app/code/) can be referenced as an example use of the library -- it is for this appliction that FULE was written in the first place.++If you have any questions or issues feel free to contact me. I'd also be interested to hear about any uses of this library.++## Features+ - Pre-defined containers (more to come!)+ - Easy centering of content+ - Dynamic, responsive layout+ - Flat data-structures+ - Monad-friendly API++## Roadmap++### Planning+ - A `Quartered` container+ - A `Table` container++### Wishlist+ - Removable/replaceable components -- updatable `Layout`+ - An `Oversized` container (for scrolling)+ - Have `Positioned` take effect within an `Unreckoned` container
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ doc/Theory.md view
@@ -0,0 +1,578 @@+# Introduction++GUIs are commonly defined and stored as a tree of objects. Updating the GUI means changing elements of this tree.++Updating trees where there's mutable data is easy, but updating nested immutable data-structures can be onerous and inefficient (though conveniences such as zippers and lenses can make it easier).++While GUIs in FULE are initially defined using a tree of function calls, the result is a flat layout data-structure and a list of UI components (along with meta-information) that should be easier to work with in an immutable language.++In this document we'll go over the math of how this works to create a responsive layout, which will require elementary knowledge of linear algebra (i.e. matrix multiplication).++# Guides++The main entity of layout construction in FULE is called a _guide_. Guides represent the boundaries between components, the edges of windows, or invisible things like center-lines for centering content.++Each guide has a value associated with it, and these values are kept in a vector (in the mathematical sense).++For example, a bare window has four guides associated with it: the _top_ and _left_ guides both have a value of zero, representing the origin of the window, and the _right_ and _bottom_ guides have values like e.g. 800 and 600, respecitively, representing the width and height of the window in pixels.++The layout-vector for this example is one with four rows, one for each guide:++$$+\left[+  \begin{matrix}+  0\\+  0\\+  800\\+  600+  \end{matrix}+\right]+$$++If we were to divide the window into two parts content-wise, a top and a bottom, with the bottom portion being 100 pixels in height, we would add another value to the layout-vector to represent the dividing guide, giving it a value of 500 -- one-hundred pixels above the guide for the bottom of the window:++$$+\left[+  \begin{matrix}+  0\\+  0\\+  800\\+  600\\+  500+  \end{matrix}+\right]+$$++When one wants to obtain the boundaries of a visual component, one just queries the layout-vector for the required guides' values.++# Dependencies++When the value of a guide in the vector changes, e.g. in response to a window resize, we'll often want other guides to be repositioned relative to them as well, to maintain the integrity of the layout.++FULE encodes this sort of relationship using _dependency matrices_, of which there are two. We will discuss each one in turn.++## Plastic Dependencies++The plastic dependency matrix specifies a set of rigid, direct relationships between guides, causing changes to one guide to propagate to the others plastically linked to it.++Continuing with our split-window example from above: say we wanted the bottom portion of the window to always be 100 pixels in height, even after the window was resized. Any changes to the value of the guide for the bottom of the window then would need to be applied to the value for the dividing guide as well, to maintain a difference of 100 pixels between them.++The plastic dependency matrix encoding this linkage would look like this:++$$+\left[+  \begin{matrix}+  1 & 0 & 0 & 0 & 0\\+  0 & 1 & 0 & 0 & 0\\+  0 & 0 & 1 & 0 & 0\\+  0 & 0 & 0 & 1 & 0\\+  0 & 0 & 0 & 1 & 1+  \end{matrix}+\right]+$$++This is essentially an identity matrix, but note the extra $1$ in the bottom row: this non-zero entry links the division guide (represented by the bottom row) to the bottom window guide (represented by the next-from-bottom row) so that when an update occurs to the window guide, it will be applied to the division guide as well.++If the window were resized to be 25 pixels shorter, the propagation of this change to the division guide would happen like so:++$$+\left[+  \begin{matrix}+  1 & 0 & 0 & 0 & 0\\+  0 & 1 & 0 & 0 & 0\\+  0 & 0 & 1 & 0 & 0\\+  0 & 0 & 0 & 1 & 0\\+  0 & 0 & 0 & 1 & 1+  \end{matrix}+\right]+\left[+  \begin{matrix}+  0\\+  0\\+  0\\+  -25\\+  0+  \end{matrix}+\right] =+\left[+  \begin{matrix}+  0\\+  0\\+  0\\+  -25\\+  -25+  \end{matrix}+\right]+$$++And the update to the layout vector would then happen like this:++$$+\left[+  \begin{matrix}+  1 & 0 & 0 & 0 & 0\\+  0 & 1 & 0 & 0 & 0\\+  0 & 0 & 1 & 0 & 0\\+  0 & 0 & 0 & 1 & 0\\+  0 & 0 & 0 & 1 & 1+  \end{matrix}+\right]+\left[+  \begin{matrix}+  0\\+  0\\+  0\\+  -25\\+  0+  \end{matrix}+\right]+++\left[+  \begin{matrix}+  0\\+  0\\+  800\\+  600\\+  500+  \end{matrix}+\right] =+\left[+  \begin{matrix}+  0\\+  0\\+  800\\+  575\\+  475+  \end{matrix}+\right]+$$++### Symmetries++Note that the change-propagation in this example only goes one-way: since the entry across the diagonal from the extra $1$ is a $0$, changes to the division guide will not be propagated to the bottom window guide, only the other way around -- their dependency relationship is _asymmetric_.++To demonstrate a _symmetric_ relationship, let's add yet another guide to the layout: this time we'll add one just 10 pixels above the division guide to create the top and bottom of a _resize bar_ which a user could drag to change the sizes of the two larger areas of the window simultaneously.++Our layout-vector now looks like this:++$$+\left[+  \begin{matrix}+  0\\+  0\\+  800\\+  600\\+  500\\+  490+  \end{matrix}+\right]+$$++For the behavior of this resize bar to make sense, any change to either one of the resize bar's guides must be applied to the other, otherwise the bar would expand and contract in size.++The plasticity matrix therefore should link them together with a $1$ on both sides of the diagonal in the row and column for each bar guide:++$$+\left[+  \begin{matrix}+  1 & 0 & 0 & 0 & 0 & 0\\+  0 & 1 & 0 & 0 & 0 & 0\\+  0 & 0 & 1 & 0 & 0 & 0\\+  0 & 0 & 0 & 1 & 0 & 0\\+  0 & 0 & 0 & 1 & 1 & 1\\+  0 & 0 & 0 & 0 & 1 & 1+  \end{matrix}+\right]+$$++Now a direct update to either one of the guides will affect the other:++$$+\left[+  \begin{matrix}+  1 & 0 & 0 & 0 & 0 & 0\\+  0 & 1 & 0 & 0 & 0 & 0\\+  0 & 0 & 1 & 0 & 0 & 0\\+  0 & 0 & 0 & 1 & 0 & 0\\+  0 & 0 & 0 & 1 & 1 & 1\\+  0 & 0 & 0 & 0 & 1 & 1+  \end{matrix}+\right]+\left[+  \begin{matrix}+  0\\+  0\\+  0\\+  0\\+  -15\\+  0+  \end{matrix}+\right] =+\left[+  \begin{matrix}+  0\\+  0\\+  0\\+  0\\+  -15\\+  -15+  \end{matrix}+\right]+$$++$$+\left[+  \begin{matrix}+  1 & 0 & 0 & 0 & 0 & 0\\+  0 & 1 & 0 & 0 & 0 & 0\\+  0 & 0 & 1 & 0 & 0 & 0\\+  0 & 0 & 0 & 1 & 0 & 0\\+  0 & 0 & 0 & 1 & 1 & 1\\+  0 & 0 & 0 & 0 & 1 & 1+  \end{matrix}+\right]+\left[+  \begin{matrix}+  0\\+  0\\+  0\\+  0\\+  0\\+  -15+  \end{matrix}+\right] =+\left[+  \begin{matrix}+  0\\+  0\\+  0\\+  0\\+  -15\\+  -15+  \end{matrix}+\right]+$$++The relationship of these two guides is thus symmetric.++### Propagation++Relationships between the guides specified in this way allows us to propagate changes from one guide to another automatically, but only for direct linkages; transitive links won't be realized the way things are currently set up because there's nothing linking the linkages.++Say the user resized the window, like before, with the latest plasticity matrix:++$$+\left[+  \begin{matrix}+  1 & 0 & 0 & 0 & 0 & 0\\+  0 & 1 & 0 & 0 & 0 & 0\\+  0 & 0 & 1 & 0 & 0 & 0\\+  0 & 0 & 0 & 1 & 0 & 0\\+  0 & 0 & 0 & 1 & 1 & 1\\+  0 & 0 & 0 & 0 & 1 & 1+  \end{matrix}+\right]+\left[+  \begin{matrix}+  0\\+  0\\+  0\\+  -25\\+  0\\+  0+  \end{matrix}+\right] =+\left[+  \begin{matrix}+  0\\+  0\\+  0\\+  -25\\+  -25\\+  0+  \end{matrix}+\right]+$$++The link between the bottom window guide and the division guide causes the change to propagate to the division guide as before, preserving their 100 pixel difference, but the change does not continue on to affect the other guide of the resize bar. It should, however, because the other resize bar guide is in a relationship with the division guide and should move the same amount that it does.++This defficiency is due to the fact that the plasticity matrix entry which would link the window bottom to the second resize bar guide -- bottom row, fourth column -- is a $0$. We need some way to link these relationships together, which would turn this entry into a $1$.++If we look at both of the propagations discussed thus far, we have one propagation from the bottom window guide to the division guide, caused by an application of the plasticity matrix, and a propagation from the division guide to the other resize bar guide, also caused by an application of the matrix.++Each application results in one propagation -- from window to division and from division to bar. What, then, if we were to apply the pasticity matrix _twice_ -- would that create a transitive link?++Applying the matrix twice is equivalent to squaring it, then applying it, so let's see what happens when we square it:++$$+\left[+  \begin{matrix}+  1 & 0 & 0 & 0 & 0 & 0\\+  0 & 1 & 0 & 0 & 0 & 0\\+  0 & 0 & 1 & 0 & 0 & 0\\+  0 & 0 & 0 & 1 & 0 & 0\\+  0 & 0 & 0 & 1 & 1 & 1\\+  0 & 0 & 0 & 0 & 1 & 1+  \end{matrix}+\right] ^{2} =+\left[+  \begin{matrix}+  1 & 0 & 0 & 0 & 0 & 0\\+  0 & 1 & 0 & 0 & 0 & 0\\+  0 & 0 & 1 & 0 & 0 & 0\\+  0 & 0 & 0 & 1 & 0 & 0\\+  0 & 0 & 0 & 2 & 2 & 2\\+  0 & 0 & 0 & 1 & 2 & 2\\+  \end{matrix}+\right]+$$++The entry to link the guides that we'd wanted to be linked _is_ a $1$ in the squared matrix, like we'd desired, but other entries have been changed from a $1$ to a $2$, which we didn't want. We can remediate this by setting all non-zero entries of the matrix to $1$ after the square.++Successive applications of the plasticity matrix serve to propagate the linkage of the guides, so a general propagation algorithm would involve applying apply the matrix $n-1$ times for an $n \times n$ matrix (and setting things to $1$ after), just to make sure we cover all the linkages.++Alternatively, we could iteratively square the matrix (and set things to $1$) until there were no more changes upon iteration, and that should take care of linking everything together.++We'll call this iterative propagation operation $prop_{P}$:++$$+prop_{P}\left(+\left[+  \begin{matrix}+  1 & 0 & 0 & 0 & 0 & 0\\+  0 & 1 & 0 & 0 & 0 & 0\\+  0 & 0 & 1 & 0 & 0 & 0\\+  0 & 0 & 0 & 1 & 0 & 0\\+  0 & 0 & 0 & 1 & 1 & 1\\+  0 & 0 & 0 & 0 & 1 & 1+  \end{matrix}+\right]+\right) =+\left[+  \begin{matrix}+  1 & 0 & 0 & 0 & 0 & 0\\+  0 & 1 & 0 & 0 & 0 & 0\\+  0 & 0 & 1 & 0 & 0 & 0\\+  0 & 0 & 0 & 1 & 0 & 0\\+  0 & 0 & 0 & 1 & 1 & 1\\+  0 & 0 & 0 & 1 & 1 & 1\\+  \end{matrix}+\right]+$$++The update procedure now becomes:++$$+prop_{P}\left(+\left[+  \begin{matrix}+  1 & 0 & 0 & 0 & 0 & 0\\+  0 & 1 & 0 & 0 & 0 & 0\\+  0 & 0 & 1 & 0 & 0 & 0\\+  0 & 0 & 0 & 1 & 0 & 0\\+  0 & 0 & 0 & 1 & 1 & 1\\+  0 & 0 & 0 & 0 & 1 & 1+  \end{matrix}+\right]+\right)+\left[+  \begin{matrix}+  0\\+  0\\+  0\\+  -25\\+  0\\+  0+  \end{matrix}+\right]+++\left[+  \begin{matrix}+  0\\+  0\\+  800\\+  600\\+  500\\+  490+  \end{matrix}+\right] =+\left[+  \begin{matrix}+  0\\+  0\\+  800\\+  575\\+  475\\+  465+  \end{matrix}+\right]+$$++which updates all the guides we'd wanted to change.++**In-depth note:** internally $prop_{P}$ has been implemented with an operator I'm calling $\star$, which is like matrix multiplication but whereas in regular multiplication the piece-wise products of the arguments' respective rows and columns are _summed_, in $\star$ the non-zero elements of the piece-wise products are _multiplied_ -- this saves us having to reset the values to $1$ afterwards. The iterative squaring using $\star$ is done until the matrix becomes [idempotent](https://en.wikipedia.org/wiki/Idempotent_matrix) under the operation.++## Elastic Dependencies++Plastic dependencies give us rigid links between guides; _elastic_ dependencies give us _stretchy_ links.++Whereas plastic dependencies let us specify an offset between guides in **pixels**, elastic dependencies let us specify offsets in **percents**.++Suppose we wanted to divide a window into two vertical halves and have each half always occupy 50% of the window. There's no set pixel value we can use to have this division always remain in the center; we have to make it move relative to the two window sides when an update occurs, splitting the difference of their movements.++If we recycle our 800x600 window for this new example, the layout vector for the window will have a vertical guide (at 400 pixels) between the two vertical window sides:++$$+\left[+  \begin{matrix}+  0\\+  0\\+  800\\+  600\\+  400\\+  \end{matrix}+\right]+$$++And the elasticity matrix to split the difference between the two sides' movements for this guide when an update takes place will be:++$$+\left[+  \begin{matrix}+  1 & 0 & 0 & 0 & 0\\+  0 & 1 & 0 & 0 & 0\\+  0 & 0 & 1 & 0 & 0\\+  0 & 0 & 0 & 1 & 0\\+  0 & 0.5 & 0.5 & 0 & 1\\+  \end{matrix}+\right]+$$++The two $0.5$ entries in this matrix represent the amount of influence each of the window-side guides has on the division guide; since it's halfway between them they each exert half of the influence on it.++If a user were to expand the right side of the window by 50 pixels then we'd have an update process of:++$$+\left[+  \begin{matrix}+  1 & 0 & 0 & 0 & 0\\+  0 & 1 & 0 & 0 & 0\\+  0 & 0 & 1 & 0 & 0\\+  0 & 0 & 0 & 1 & 0\\+  0 & 0.5 & 0.5 & 0 & 1\\+  \end{matrix}+\right]+\left[+  \begin{matrix}+  0\\+  0\\+  50\\+  0\\+  0\\+  \end{matrix}+\right]+++\left[+  \begin{matrix}+  0\\+  0\\+  800\\+  600\\+  400\\+  \end{matrix}+\right] =+\left[+  \begin{matrix}+  0\\+  0\\+  850\\+  600\\+  425\\+  \end{matrix}+\right]+$$++which keeps the division guide in the center of the window.++Now suppose we were to alter this deisgn to have the vertical portions occupy 25% of the window on the left and 75% on the right. How would this change the elasticity matrix?++The $0.5$ entries representing the 50%s would have to change, but possibly not how you think.++Remember that these values represent the influence the window-side guides have on the division guide. If we change the division guide to be closer to one of the sides, then that side will have more influence; moving the guide to be 25% of the way from one side means that that side's guide will have 75% of the influence on the division guide.++The values for these entries in the elasticity matrix then are one minus the percent distance the dependent guide is from the influencing guide, so the new elasticity matrix would be:++$$+\left[+  \begin{matrix}+  1 & 0 & 0 & 0 & 0\\+  0 & 1 & 0 & 0 & 0\\+  0 & 0 & 1 & 0 & 0\\+  0 & 0 & 0 & 1 & 0\\+  0 & (1-0.25) & (1-0.75) & 0 & 1\\+  \end{matrix}+\right] =+\left[+  \begin{matrix}+  1 & 0 & 0 & 0 & 0\\+  0 & 1 & 0 & 0 & 0\\+  0 & 0 & 1 & 0 & 0\\+  0 & 0 & 0 & 1 & 0\\+  0 & 0.75 & 0.25 & 0 & 1\\+  \end{matrix}+\right]+$$++### Propagation++Like the plastic dependencies, the elastic dependencies must be propagated because we can have transitive linkages between them -- one elastic guide can depend on other elastic guides that depend on still other elastic guides that depend finally on plastic guides, e.g..++The propagation operation for the elasticity matrix must be different than that for the plasticity matrix because we want entries in the result to remain less than $1$ so they will still be elastic.++After propagation the resulting matrix should have entries for the elastic guides in columns for the non-elastic guides on which they depend, transitively or intransitively, since that's what ultimately will produce a change to the elastic guides during an update.++Thanks to some divine guidance during experimentation I was led to the following procedure for elastic propagation:++Let $E$ be an $n \times n$ elasticity matrix, but [hollow](https://en.wikipedia.org/wiki/Hollow_matrix) (sans identity elements); the propagation procedure $prop_E$ then is:++$$+prop_E(E) = \sum_{i=1}^{n} E^i+$$++Because $E$ appears to be [nilpotent](https://en.wikipedia.org/wiki/Nilpotent_matrix) we can stop the summation early, saving us some computation cycles.++This propagation operation can leave non-zero entries for some of the elastic guides that we don't necessarily want, but they don't appear to be negatively affecting anything so they are being left as-is.++# Update Cycle++We have two matrices that need to be applied to produce an update to the layout, and we also have propagation to be concerned with.++To forgo some explanation, the following is the current working update procedure as determined by providence, reason, and experimentation:++Given a plasticity matrix $P$ (with identity entries), an hollow elasticity matrix $E$ (without identity entries), an update vector $U$, and the layout-guide value vector $L_n$, the procedure to get the next vector of layout-guide values $L_{n+1}$ is:++$$+\begin{align}+P_{P} & = prop_{P}(P) \\+P_{E} & = prop_{E}(E) \\+P_{H} & = P_{P} - I \\+T & = P_{P} + P_{E} + P_{H}P_{E} + P_{E}P_{H} + P_{H}P_{E}P_{H} \\+L_{n+1} & = T U + L_n+\end{align}+$$++where $I$ is the identity matrix.++The $P_{H}$ terms in $T$ serve to mix the propagations of $E$ and $P$ together since we need them to interact in the layout for it to respond properly to changes.++Since $P$ and $E$ don't change each time an update $U$ is applied we can pre-compute the combined transformation matrix $T$ just once for efficiency's sake and use it for every update cycle; any changes to the composition of the layout (and thus to $P$ or $E$) will require rebuilding $T$ though.++# Guide Constraints++FULE also offers a way to constrain the movement of one guide relative to another, but as this feature is a pretty straight-forward augmentation of the update procedure above I'll not cover the theory here; you should have a good basis to understand what's going on should you wish to dive into the code.++# Analysis++Rather than having a resize event that propagates throughout a GUI tree to notify all the components it affects of a size change, we keep the layout information in a flat data-structure and can pre-compute this propagation only once beforehand.++Depending on how you're managing your layout you may need to reconstruct the bounds of the components from the guides every frame, but that shouldn't be too much of a hassle with a flat list of components.
+ doc/Usage.md view
@@ -0,0 +1,108 @@+# Introduction++FULE is a layout engine for calculating the positions of visual elements on screen and adjusting these positions in response to input. While it does not draw the the elements or facilitate interactions with them -- these are left up to you -- it does try to give you a leg up in your effort to do so!++Creating a layout with FULE can be done at a high level utilizing its predefined containers, at a low level with the `LayoutDesign` and `Layout` types using guides, or at a mix of both levels. There are some simple examples in the [examples](../examples/) directory if you'd like to view some working code.++This document goes over FULE's essential concepts and discusses how to integrate FULE into your project.++ - [Essential Concepts](#essential-concepts)+ - [Imports](#imports)+ - [Layout Creation](#layout-creation)+   - [Components](#components)+   - [Minimal Layout](#minimal-layout)+ - [Layout Usage](#layout-usage)++# Essential Concepts++The main entity of layout construction in FULE is called a _guide_. Guides represent the boundaries between components, the edges of windows, or invisible things like center-lines for centering content. When you interact with a `Layout`, you will be retrieving the positions of guides or moving them around.++Guides can be positioned absolutely, statically relative to other guides, or suspended elastically between other guides.++At a higher level are _containers_. Containers use guides under the hood to create higher-level constructs such as grids or divided areas. When you create a `Layout` you will probably use continers.++That's pretty-much it as far as usage concepts go; you can dive into [the theory](Theory.md) if you'd like to look a little deeper.++# Imports++You only need a single import to use FULE:++```haskell+import FULE+```++# Layout Creation++## Components++The first thing you'll probably want to do in your code is define your visual components -- interactive widgets or static elements -- and provide an instance of FULE's `Component` typeclass for each of them. `Component` tells FULE what the sizing requirements are for your components.++`Component` is a monad-friendly typeclass should you need to interact with stateful data to retrieve sizing information for your components.++Your components will either need to be all of the same type or to have some sort of [wrapper for heterogenous types](https://wiki.haskell.org/Heterogenous_collections) around them in order for FULE to use them.++A convenience implementation of `Component` has been provided for you for any type which specifices no height or width requirements. When you define your own instance(s) you should override the default by using the `{-# OVERLAPS #-}` or `{-# OVERLAPPING #-}` pragmas on it (or them).++## Minimal Layout++Next you can create your layout.++Layouts are created at a high level using any of several _containers_. The outer-most container for your layout must be a `Window`; within the `Window` you can add your visual components and other containers. ++Most containers require configuration arguments suitable for their particular functions; `Window` must be provided:+ - The dimensions of the GUI window that the layout will be displayed in+ - A function to create an (invisible) UI component that you should use to adjust the size of the `Layout` in response to changes in the GUI window's size -- a callback in other words++For the requirements other containers have, consult their documentation on Hackage (not available yet). For some working examples, see the [examples](../examples/) directory.++Once your layout has been defined, you'll build it by passing the `Window` to one of the `layout` or `layoutM` functions[^1]. The result of this will be a tuple of type `(Layout, [ComponentInfo k])`, where `k` is your component type.++The list of `ComponentInfo`s contains your visual components along with meta-information about their situation within the layout, including their display `Bounds`, which can also contain info for clipping, and `RenderGroup`, which can be used for dealing with z-overlaps and rendering updates.++[^1]: The -`M` variants of functions (e.g. `layoutM`) or types (e.g. `GridM`) work in a generic monad so you can do stuff like extract the dimensions of a component using `IO` during the layout process. If you don't need a monad or want to keep things pure you can use the non-`M` variants which work in the `Identity` monad behind the scenes.++# Layout Usage++Now that you have a layout to use, you can wire it to the rest of your application.++User-input should be piped in to your components in the `ComponentInfo`s; an instance of `Functor` has been provided for this wrapper-type so you can easily modify your components within it.++Updates for the `Layout` should be part of the output your components generate. Updates can be applied to the `Layout` using the `reactToChange` or `reactToChanges` functions, giving them movement deltas for the guides that have moved. (Guide values can be retrieved from the `Layout` using the `getGuide` or `getGuides` functions.)++To draw your components, and likely to provide input for them as well, you'll need to know their bounding rectangles within the `Layout`.++The `Bounds` within the `ComponentInfo` for each component contains a set of guide IDs from which a bounding rectangle can be constructed for the component, like so:++```haskell+import FULE++data Rectangle+  = Rect+    { xOf :: Int+    , yOf :: Int+    , widthOf :: Int+    , heightOf :: Int+    }++toRectangle :: Bounds -> Layout -> Rectangle+toRectangle bounds layout =+  Rect left top (right - left) (bottom - top)+  where+    [top, left, right, bottom] = boundingGuidesFor layout bounds+```++In the event that you're concerned about clipping content and are using the `Clipped` container, you can get the clipping rectangles from the `Bounds` as well:++```haskell+toRectangles :: Bounds -> Layout -> [Rectangle]+toRectangles bounds layout =+  case clippingOf bounds of+    Nothing -> [toRectangle bounds layout]+    Just cl -> toRectangle bounds layout : toRectangles cl layout+```++The rectangle at the head of the returned list is the bounds for the component; all the rectangles in the list should be intersected to find the clipped display area. (Don't forget to consider clipping when reacting to input!)++# Conclusion++That should get you started, do let me know if there are any questions or issues!
+ examples/Divided.hs view
@@ -0,0 +1,64 @@+-- This module contains two examples of Divided containers (using `sizedTop`):+--  - one statically divided+--  - one dynamically divided+--+-- Note the type-annotation on the `Dynamics` for each divided container:+-- this is required since the type-checker cannot disambiguate `Widget`+-- as the type-parameter to `Dynamics` by itself.+-- Using the qualifiers here requires the ScopedTypeVariables language-extension.+-- You could also declare these values outside of the function bodies and+-- not need to use the language-extension.+--+{-# LANGUAGE ScopedTypeVariables #-}++module Divided where++import FULE+++data Widget+  = ResizeControl+    { widthOf :: Int+    , heightOf :: Int+    -- These guides should be used to resize the `Layout` in response to+    -- the window being resized, using a delta.+    -- Note that you will have to wire this to the GUI framework yourself.+    , widthGuideOf :: GuideID+    , heightGuideOf :: GuideID+    }+  | UnsizedContent+  | SizedContent+  | DividerBar+    -- A Horizontal Orientation would call for vertical movement,+    -- so should be paired with watching for changes in the y-axis;+    -- likewise with Vertical and the x-axis.+    { orientationOf :: Orientation+    , movementGuideOf :: GuideID+    }++getWindowSize :: m (Int, Int)+getWindowSize = undefined -- for you to implement++staticDivided :: (Monad m) => m (Layout, [ComponentInfo Widget])+staticDivided = do+  (width, height) <- getWindowSize+  let staticSize :: Dynamics Widget = static+  return $ layout+    (window (width, height) (ResizeControl width height)+      (sizedTop sizedToContents staticSize+        SizedContent+        UnsizedContent+        ))++dynamicDivided :: (Monad m) => m (Layout, [ComponentInfo Widget])+dynamicDivided = do+  (width, height) <- getWindowSize+  let dynamicSize :: Dynamics Widget = dynamic DividerBar 10+  return $ layout+    (window (width, height) (ResizeControl width height)+      (sizedTop (sizedTo 100) dynamicSize+        -- Depending on the content, you may wish to use `clipped` to prevent+        -- it from displaying out of bounds when the container is resized.+        (clipped SizedContent)+        (clipped UnsizedContent)+        ))
+ examples/Grid.hs view
@@ -0,0 +1,59 @@+-- This module contains example usages of `Grid`, `GridM`, `Item`, and `ItemM`.+--+{-# LANGUAGE ScopedTypeVariables #-}++module Grid where++import FULE+++data Widget+  = ResizeControl+    { widthOf :: Int+    , heightOf :: Int+    -- These guides should be used to resize the `Layout` in response to+    -- the window being resized, using a delta.+    -- Note that you will have to wire this to the GUI framework yourself.+    , widthGuideOf :: GuideID+    , heightGuideOf :: GuideID+    }+  | Content String+++exampleGrid :: Int -> Int -> (Layout, [ComponentInfo Widget])+exampleGrid width height = layout+  (window (width, height) (ResizeControl width height)+    (grid (2, 3)+      -- Annoyingly a type-annotation is required for the list of items which+      -- must be contained within an extra set of parentheses.+      ([item (Content "A")+      , item (Content "B")+      , item (Content "C")+      , item (Content "D")+      , item (Content "E")+      , item (Content "F")+      ]::[Item Widget])))+++getWindowSize :: m (Int, Int)+getWindowSize = undefined -- for you to implement++-- When using `ItemM` you'll need to specify a `forall` for your monadic+-- type-variable in the type-signature and use the `ScopedTypeVariables`+-- language-extension to get the type-checker to use the same variable for the+-- type-signature and the type-annotation of your `ItemM` list in the body.+exampleGridM :: forall m . (Monad m) => m (Layout, [ComponentInfo Widget])+exampleGridM = do+  (width, height) <- getWindowSize+  layoutM+    (window (width, height) (ResizeControl width height)+      (grid (2, 3)+        -- Annoyingly a type-annotation is required for the list of items which+        -- must be contained within an extra set of parentheses.+        ([item (Content "A")+        , item (Content "B")+        , item (Content "C")+        , item (Content "D")+        , item (Content "E")+        , item (Content "F")+        ]::[ItemM m Widget])))
+ examples/Layout.hs view
@@ -0,0 +1,46 @@+-- This module contains an example using `Layout` (and `LayoutDesign`).+--+-- `Layout` is a low-level construct of this library; you may wish to use one of+-- the higher-level Containers if you're not doing anything super custom, or you+-- can use the higher-level Containers then augment the result using `Layout`+-- afterward.+--+module Layout where++import Control.Arrow -- `second`+import Control.Monad.State+import FULE+++makeLayout :: Int -> Int -> ([Bounds], Layout)+makeLayout width height =+  second build -- we have to `build` the `LayoutDesign`+  $ runState (designLayout width height) emptyLayoutDesign++-- make a layout for a divided window with a resize bar+designLayout :: Int -> Int -> State LayoutDesign [Bounds]+designLayout width height = do+  --+  -- window+  windowTop <- state . addGuide $ Absolute 0+  windowLeft <- state . addGuide $ Absolute 0+  windowRight <- state . addGuide $ Absolute width+  windowBottom <- state . addGuide $ Absolute height+  --+  -- resize bar+  resizeBarBottom <- state . addGuide $ Relative (-100) windowBottom Asymmetric+  resizeBarTop <- state . addGuide $ Relative (-10) resizeBarBottom Symmetric+  -- constraints keep the resize bar within the bounds of the window when moving+  modify . addGuideConstraint $ resizeBarBottom `LTE` windowBottom+  modify . addGuideConstraint $ resizeBarTop `GTE` windowTop+  --+  -- make the bounds for the various areas+  let window =+        Bounds windowTop windowLeft windowRight windowBottom Nothing+  let resizeBar =+        Bounds resizeBarTop windowLeft windowRight resizeBarBottom Nothing+  let unsizedContent = -- top portion of the screen+        Bounds windowTop windowLeft windowRight resizeBarTop Nothing+  let sizedContent = -- bottom portion of the screen+        Bounds resizeBarBottom windowLeft windowRight windowBottom Nothing+  return [window, unsizedContent, resizeBar, sizedContent]
+ src/FULE.hs view
@@ -0,0 +1,44 @@+-- |+-- Module      : FULE+-- Description : The full library (@import@ this!)+-- Copyright   : (c) Paul Schnapp, 2023+-- License     : BSD3+-- Maintainer  : Paul Schnapp <paul.schnapp@gmail.com>+--+-- This is the full library, just @import@ this module.+module FULE+ ( module FULE.Component+ , module FULE.Container+ , module FULE.Container.Arrayed+ , module FULE.Container.Clipped+ , module FULE.Container.Config+ , module FULE.Container.Divided+ , module FULE.Container.Grid+ , module FULE.Container.Item+ , module FULE.Container.Layered+ , module FULE.Container.Padded+ , module FULE.Container.Positioned+ , module FULE.Container.Sized+ , module FULE.Container.Unreckoned+ , module FULE.Container.Window+ , module FULE.Layout+ , module FULE.LayoutOp+ ) where++import FULE.Component+import FULE.Container+import FULE.Container.Arrayed+import FULE.Container.Clipped+import FULE.Container.Config+import FULE.Container.Divided+import FULE.Container.Grid+import FULE.Container.Item+import FULE.Container.Layered+import FULE.Container.Padded+import FULE.Container.Positioned+import FULE.Container.Sized+import FULE.Container.Unreckoned+import FULE.Container.Window+import FULE.Layout+import FULE.LayoutOp+
+ src/FULE/Component.hs view
@@ -0,0 +1,114 @@+{-# LANGUAGE DeriveFunctor #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}++-- |+-- Module      : FULE.Component+-- Description : The @Component@ typeclass+-- Copyright   : (c) Paul Schnapp, 2023+-- License     : BSD3+-- Maintainer  : Paul Schnapp <paul.schnapp@gmail.com>+--+-- A typeclass which any visual component made by you should implement;+-- related datatypes.+module FULE.Component+ ( Component(..)+ , ComponentInfo(..)+ , RenderGroup+ , Bounds(..)+ , HasBoundingGuides(..)+ , boundingGuidesInCSSOrderFor+ ) where++import Control.DeepSeq++import FULE.Layout+++-- | A typeclass for specifying the display requirements of a visual component.+--+--   A default implementation has been provided meaning you may wish to have+--   your instances use the @{-# OVERLAPS #-}@ or @{-# OVERLAPPING #-}@ pragmas.+--+--   You'll need to have the @MultiParamTypeClasses@ language extension enabled+--   to implement this.+class (Monad m) => Component k m where+  -- | The /width/ the component requires on-screen, if any.+  --   The default implementation returns @Nothing@.+  requiredWidth :: k -> m (Maybe Int)+  requiredWidth _ = return Nothing+  -- | The /height/ the compnent requires on-screen, if any.+  --   The default implementation returns @Nothing@.+  requiredHeight :: k -> m (Maybe Int)+  requiredHeight _ = return Nothing++-- | A convenience instance for components that have no width or height+--   requirements.+instance {-# OVERLAPPABLE #-} (Monad m) => Component k m where+++-- | Meta info about a component along with the component itself.+data ComponentInfo k+  = ComponentInfo+    { boundsOf :: Bounds -- ^ The bounding rectangle of the component.+    , componentOf :: k -- ^ The component itself.+    , renderGroupOf :: RenderGroup -- ^ The render group of the component.+    }+  deriving (Functor, Show)++instance (NFData k) => NFData (ComponentInfo k) where+  rnf i@(ComponentInfo { boundsOf = b, componentOf = k, renderGroupOf = g }) =+    seq i . deepseq b . deepseq k . deepseq g $ ()+++-- | A convenience type-wrapper representing the rendering group a component+--   is associated with. Rendering groups are tracked when multiple components+--   overlap (in the z-axis) or are part of containers which overlap.+--   (They are not used internally but are tracked as a convenience for the+--   consumer.)+type RenderGroup = Maybe Int+++-- | A collection of Guides representing the bounding rectangle of a visual+--   component.+data Bounds+  = Bounds+    { topOf :: GuideID+    -- ^ The Guide representing the /top/ edge of the bounding rectangle.+    , leftOf :: GuideID+    -- ^ The Guide representing the /left/ edge of the bounding rectangle.+    , rightOf :: GuideID+    -- ^ The Guide representing the /right/ edge of the bounding rectangle.+    , bottomOf :: GuideID+    -- ^ The Guide representing the /bottom/ edge of the bounding rectangle.+    , clippingOf :: Maybe Bounds+    -- ^ Another @Bounds@ which may cause this one to clip.+    }+  deriving (Read, Show)++instance NFData Bounds where+  rnf a@(Bounds t l r b c) =+    seq a . deepseq t . deepseq l . deepseq r . deepseq b . deepseq c $ ()+++-- | A typeclass for retrieving Guides representing a bounding rectangle.+class HasBoundingGuides a where+  -- | Retrieves the bounding Guides for a type in the order:+  --   /top/, /left/, /right/, /bottom/.+  boundingGuidesFor :: Layout -> a -> [Int]++instance HasBoundingGuides Bounds where+  boundingGuidesFor layout (Bounds t l r b _) =+    getGuides [t, l, r, b] layout++instance HasBoundingGuides (ComponentInfo k) where+  boundingGuidesFor layout component =+    boundingGuidesFor layout (boundsOf component)++-- | Retrieves the bounding Guides for a type in CSS-order:+--   /top/, /right/, /bottom/, /left/.+boundingGuidesInCSSOrderFor :: (HasBoundingGuides a) => Layout -> a -> [Int]+boundingGuidesInCSSOrderFor layout component =+  let [t, l, r, b] = boundingGuidesFor layout component+  in [t, r, b, l]+
+ src/FULE/Container.hs view
@@ -0,0 +1,45 @@+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MonoLocalBinds #-}+{-# LANGUAGE MultiParamTypeClasses #-}++-- |+-- Module      : FULE.Container+-- Description : The @Container@ typeclass.+-- Copyright   : (c) Paul Schnapp, 2023+-- License     : BSD3+-- Maintainer  : Paul Schnapp <paul.schnapp@gmail.com>+--+-- A typeclass for creating containers of visual content.+module FULE.Container+ ( Container(..)+ ) where++import Control.Monad.Writer+import Data.Proxy++import FULE.Component+import FULE.LayoutOp+++-- sadly the `Proxy` has to be used for the heterogenous collections to work+--+-- | A typeclass for laying-out a container of type @c@ that holds content of+--   type @k@.+--+--   You'll need to have the @FlexibleInstances@ and @MultiParamTypeClasses@+--   language extensions enabled to use this and you may wish to have your+--   instance use the @{-# OVERLAPS #-}@ or @{-# OVERLAPPING #-}@ pragmas.+class (Monad m) => Container c k m where+  -- | Get the minimum /width/ required for display of the container @c@.+  minWidth :: c -> Proxy k -> m (Maybe Int)+  -- | Get the minimum /height/ required for display of the container @c@.+  minHeight :: c -> Proxy k -> m (Maybe Int)+  -- | Add the container @c@ and its contents @k@ to a 'FULE.Layout.LayoutDesign'.+  addToLayout :: c -> Proxy k -> Bounds -> RenderGroup -> LayoutOp k m ()++instance {-# OVERLAPPABLE #-} (Component k m) => Container k k m where+  minWidth k _ = requiredWidth k+  minHeight k _ = requiredHeight k+  addToLayout k _ bounds renderGroup = tell [ComponentInfo bounds k renderGroup]+
+ src/FULE/Container/Arrayed.hs view
@@ -0,0 +1,132 @@+{-# LANGUAGE BlockArguments #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}++-- |+-- Module      : FULE.Container.Arrayed+-- Description : The @Arrayed@ Container.+-- Copyright   : (c) Paul Schnapp, 2023+-- License     : BSD3+-- Maintainer  : Paul Schnapp <paul.schnapp@gmail.com>+--+-- A 'FULE.Container.Container' to display items one after another, either+-- vertically or horizontally. Items are spaced according to their inherent sizes.+--+-- You may also wish to consider a 'FULE.Container.Grid.Grid'.+module FULE.Container.Arrayed+ ( ArrayedM+ , Arrayed+ , arrayedHoriz+ , arrayedVert+ ) where++import Control.Monad+import Control.Monad.Trans.Class+import Data.Foldable+import Data.Functor.Identity+import Data.Maybe+import Data.Proxy++import FULE.Component+import FULE.Container+import FULE.Container.Config+import FULE.Container.Item+import FULE.Internal.Util+import FULE.Layout+import FULE.LayoutOp+++-- | An array (horizontal or vertical) of visual 'FULE.Container.Item.ItemM's+--   in a layout. Each item will occupy a different amount of space in the array;+--   if you wish each item to be evenly spaced, use a 'FULE.Container.Grid.Grid'+--   instead.+data ArrayedM m k+  = Arrayed+    { horizPaddingOf :: Int+    , vertPaddingOf :: Int+    , orientationOf :: Orientation+    , itemsOf :: [ItemM m k]+    }++-- | Like 'ArrayedM' but run in the 'Data.Functor.Identity.Identity' monad.+type Arrayed = ArrayedM Identity++-- NOTE: no padding is added when there are no items to display+instance (Monad m) => Container (ArrayedM m k) k m where+  minWidth (Arrayed h v o is) proxy = case o of+    Horizontal -> do+      let padding = (length is + 1) * h+      fmap (+ padding) . getTotalSize <$> mapM (`minWidth` proxy) is+    Vertical -> do+      let padding = 2 * h+      fmap (+ padding) . getMaxSize <$> mapM (`minWidth` proxy) is+  minHeight (Arrayed h v o is) proxy = case o of+    Horizontal -> do+      let padding = 2 * v+      fmap (+ padding) . getMaxSize <$> mapM (`minHeight` proxy) is+    Vertical -> do+      let padding = (length is + 1) * v+      fmap (+ padding) . getTotalSize <$> mapM (`minHeight` proxy) is+  addToLayout (Arrayed h v o is) proxy bounds renderGroup = unless (null is) do+    let (refBoundingGuide, getRefBoundingGuide) = case o of+          Horizontal -> (leftOf bounds, rightOf)+          Vertical -> (topOf bounds, bottomOf)+    let clipping = clippingOf bounds+    alignmentGuide <- case o of+      Horizontal -> addGuideToLayout $ Relative v (topOf bounds) Asymmetric+      Vertical -> addGuideToLayout $ Relative h (leftOf bounds) Asymmetric+    loopingWith refBoundingGuide is $ \refBoundingGuide i -> do+      bounds <- makeBounds h v o alignmentGuide refBoundingGuide i proxy clipping+      addToLayout i proxy bounds renderGroup+      return (getRefBoundingGuide bounds)++loopingWith :: (Foldable t, Monad m) => b -> t a -> (b -> a -> m b) -> m ()+loopingWith b as f = void (foldlM f b as)++makeBounds+  :: (Monad m)+  => Int -> Int -> Orientation -> GuideID -> GuideID -> ItemM m k -> Proxy k+  -> Maybe Bounds+  -> LayoutOp k m Bounds+makeBounds horiz vert dir alignmentGuide refBoundingGuide i proxy clipping = do+  width <- fmap (fromMaybe 0) . lift . lift $ minWidth i proxy+  height <- fmap (fromMaybe 0) . lift . lift $ minHeight i proxy+  case dir of+    Horizontal -> do+      boundingGuide <-+        if horiz /= 0+        then addGuideToLayout $ Relative horiz refBoundingGuide Asymmetric+        else return refBoundingGuide+      right <- addGuideToLayout $ Relative width boundingGuide Asymmetric+      bottom <- addGuideToLayout $ Relative height alignmentGuide Asymmetric+      return (Bounds alignmentGuide boundingGuide right bottom clipping)+    Vertical -> do+      boundingGuide <-+        if vert /= 0+        then addGuideToLayout $ Relative vert refBoundingGuide Asymmetric+        else return refBoundingGuide+      right <- addGuideToLayout $ Relative width alignmentGuide Asymmetric+      bottom <- addGuideToLayout $ Relative height boundingGuide Asymmetric+      return (Bounds boundingGuide alignmentGuide right bottom clipping)++-- | Array 'FULE.Container.Item.ItemM's horizontally with the specified padding.+--+--   Padding is added between the elements and around the perimeter of the array;+--   the horizontal padding is added once between elements, and the same padding+--   is used before and after the array -- thus the intra-element padding is not+--   double the outside padding.+arrayedHoriz :: Padding -> [ItemM m k] -> ArrayedM m k+arrayedHoriz padding = arrayed padding Horizontal++-- | Array 'FULE.Container.Item.ItemM's vertically with the specified padding.+--+--   Padding is added between the elements and around the perimeter of the array;+--   the vertical padding is added once between elements, and the same padding+--   is used before and after the array -- thus the intra-element padding is not+--   double the outside padding.+arrayedVert :: Padding -> [ItemM m k] -> ArrayedM m k+arrayedVert padding = arrayed padding Vertical++arrayed :: Padding -> Orientation -> [ItemM m k] -> ArrayedM m k+arrayed (horiz, vert) = Arrayed (max 0 horiz) (max 0 vert)+
+ src/FULE/Container/Clipped.hs view
@@ -0,0 +1,37 @@+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}++-- |+-- Module      : FULE.Container.Clipped+-- Description : The @Clipped@ Container.+-- Copyright   : (c) Paul Schnapp, 2023+-- License     : BSD3+-- Maintainer  : Paul Schnapp <paul.schnapp@gmail.com>+--+-- A 'FULE.Container.Container' to specify that content overflow should be clipped.+module FULE.Container.Clipped+ ( Clipped+ , clipped+ ) where++import FULE.Component+import FULE.Container+++-- | A container the content of which should be clipped on overflow.+--   Clipping bounds are specified as part of 'FULE.Component.Bounds' of+--   contained 'FULE.Component.Component's.+--+--   It is up to you the consumer to implement the actual clipping of content.+newtype Clipped c = Clipped c++instance (Container c k m) => Container (Clipped c) k m where+  minWidth (Clipped c) = minWidth c+  minHeight (Clipped c) = minHeight c+  addToLayout (Clipped c) proxy bounds =+    addToLayout c proxy bounds{ clippingOf = Just bounds }++-- | Create a container which clips any overflow.+clipped :: c -> Clipped c+clipped = Clipped+
+ src/FULE/Container/Config.hs view
@@ -0,0 +1,18 @@+-- |+-- Module      : FULE.Container.Config+-- Description : Common configuration types.+-- Copyright   : (c) Paul Schnapp, 2023+-- License     : BSD3+-- Maintainer  : Paul Schnapp <paul.schnapp@gmail.com>+--+-- Configuration types that are common across containers.+module FULE.Container.Config+ ( module FULE.Container.Config.Orientation+ , module FULE.Container.Config.Padding+ , module FULE.Container.Config.SizedContent+ ) where++import FULE.Container.Config.Orientation+import FULE.Container.Config.Padding+import FULE.Container.Config.SizedContent+
+ src/FULE/Container/Config/Orientation.hs view
@@ -0,0 +1,20 @@+-- |+-- Module      : FULE.Container.Config.Orientation+-- Description : Visual orientation config.+-- Copyright   : (c) Paul Schnapp, 2023+-- License     : BSD3+-- Maintainer  : Paul Schnapp <paul.schnapp@gmail.com>+--+-- The @Orientation@ datatype is used to specify the visual orientation of an+-- element.+module FULE.Container.Config.Orientation where++import Control.DeepSeq+++-- | Layout orientation, used in multiple 'FULE.Container.Container's.+data Orientation = Horizontal | Vertical++instance NFData Orientation where+  rnf o = seq o ()+
+ src/FULE/Container/Config/Padding.hs view
@@ -0,0 +1,25 @@+-- |+-- Module      : FULE.Container.Config.Padding+-- Description : Padding config.+-- Copyright   : (c) Paul Schnapp, 2023+-- License     : BSD3+-- Maintainer  : Paul Schnapp <paul.schnapp@gmail.com>+--+-- Datatype and functions for specifying visual padding.+module FULE.Container.Config.Padding where+++-- | Visual padding around an element.+type Padding = (Int, Int)++-- | Padding to use; see parameters for details.+padding+  :: Int -- ^ Horizontal padding to use.+  -> Int -- ^ Vertical padding to use.+  -> Padding+padding horiz vert = (horiz, vert)++-- | Don't use any padding.+noPadding :: Padding+noPadding = (0, 0)+
+ src/FULE/Container/Config/SizedContent.hs view
@@ -0,0 +1,23 @@+-- |+-- Module      : FULE.Container.Config.SizedContent+-- Description : Content size config.+-- Copyright   : (c) Paul Schnapp, 2023+-- License     : BSD3+-- Maintainer  : Paul Schnapp <paul.schnapp@gmail.com>+--+-- Datatype and functions for specifying the size of content.+module FULE.Container.Config.SizedContent where+++-- | The size that the sized portion of a container should have;+--   see the 'FULE.Container.Divided.Divided' container for an example use.+type SizedContentSize a = Maybe a++-- | Use a set size for the sized portion of a container.+sizedTo :: a -> SizedContentSize a+sizedTo = Just++-- | Use the inherent size of the content for the sized portion of a container.+sizedToContents :: SizedContentSize a+sizedToContents = Nothing+
+ src/FULE/Container/Divided.hs view
@@ -0,0 +1,242 @@+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}++-- |+-- Module      : FULE.Container.Divided+-- Description : The @Divided@ Container.+-- Copyright   : (c) Paul Schnapp, 2023+-- License     : BSD3+-- Maintainer  : Paul Schnapp <paul.schnapp@gmail.com>+--+-- A 'FULE.Container.Container' that is divided in half, horizontally or vertically.+module FULE.Container.Divided+ ( Divided+ , BarControlGen+ , Dynamics+ , dynamic+ , static+ , sizedTop+ , sizedLeft+ , sizedRight+ , sizedBottom+ ) where++import Control.Applicative+import Control.Monad.Trans.Class+import Data.Maybe+import Data.Proxy++import FULE.Component+import FULE.Container+import FULE.Container.Config+import FULE.Internal.Util+import FULE.Layout+import FULE.LayoutOp+++data SizedSide = SizedTop | SizedLeft | SizedRight | SizedBottom+++-- | Type of a function to produce a 'FULE.Component.Component' for controlling+--   the resize bar, thus leaving the mechanics and presentation up to you.+type BarControlGen b+  = Orientation+  -- ^ The orientation of the resize bar; a @Horizontal@ orientation would call+  --   for vertical movement, so should be paired with watching for changes in+  --   the @y@ axis, and likewise with @Vertical@ and the @x@ axis.+  -> GuideID+  -- ^ A Guide associated with the resize bar that should be updated with a+  --   delta when the bar is moved.+  -> b -- ^ The bar component to be added to the layout.++-- | A specification of whether the sized portion of the container should be+--   resizable and how the resize bar, if any, should be sized and controlled.+data Dynamics b+  = Dynamic+    { barGenOf :: BarControlGen b+    , barSizeOf :: Int+    }+  | Static++barSizeFor :: Dynamics b -> Maybe Int+barSizeFor (Dynamic _ s) = Just s+barSizeFor Static = Nothing++-- | Use a dynamic sizing, with a resize bar, for the sized portion of the+--   container.+dynamic+  :: BarControlGen b+  -- ^ A function to generate a @Component@ to use for the resize bar.+  -> Int+  -- ^ The thickness of the resize bar @Component@ in the direction of travel.+  -> Dynamics b+dynamic genBar size = Dynamic genBar (max 0 size)++-- | Use a static size for the sized portion of the container.+static :: Dynamics b+static = Static+++-- | A container divided (horizontally or vertically) into two parts with one+--   of the parts having a set size (height or width) and the other part+--   resizing dynamically.+--+--   When configured to be resizable, the resize bar's size is not included in+--   the size of the sized content during layout but is treated as an additional+--   size to be considered.+data Divided s b u+  = Divided+    { sizedSideOf :: SizedSide+    , sizeOf :: SizedContentSize Int+    , dynamicsOf :: Dynamics b+    , sizedContentOf :: s+    , unconstrainedContentOf :: u+    }++instance (Container s b m, Container u b m) => Container (Divided s b u) b m where+  minWidth divided proxy = do+    sizedWidth <- minWidth (sizedContentOf divided) proxy+    let barSize = barSizeFor (dynamicsOf divided)+    let sizedWidth' = getTotalSize [sizeOf divided <|> sizedWidth, barSize]+    case sizedSideOf divided of+      SizedLeft  -> return sizedWidth'+      SizedRight -> return sizedWidth'+      _          -> return sizedWidth+  minHeight divided proxy = do+    sizedHeight <- minHeight (sizedContentOf divided) proxy+    let barSize = barSizeFor (dynamicsOf divided)+    let sizedHeight' = getTotalSize [sizeOf divided <|> sizedHeight, barSize]+    case sizedSideOf divided of+      SizedTop    -> return sizedHeight'+      SizedBottom -> return sizedHeight'+      _           -> return sizedHeight+  addToLayout divided proxy bounds renderGroup =+    case sizedSideOf divided of+      SizedTop -> makeDivided divided proxy bounds renderGroup+        DivisionConfig+        { setUnconInnerOf = \g b -> b { topOf = g }+        , getUnconOuterOf = bottomOf+        , setSizedInnerOf = \g b -> b { bottomOf = g }+        , getSizedOuterOf = topOf+        , multiplierOf = 1+        , orientationOf = Horizontal+        }+      SizedLeft -> makeDivided divided proxy bounds renderGroup+        DivisionConfig+        { setUnconInnerOf = \g b -> b { leftOf = g }+        , getUnconOuterOf = rightOf+        , setSizedInnerOf = \g b -> b { rightOf = g }+        , getSizedOuterOf = leftOf+        , multiplierOf = 1+        , orientationOf = Vertical+        }+      SizedRight -> makeDivided divided proxy bounds renderGroup+        DivisionConfig+        { setUnconInnerOf = \g b -> b { rightOf = g }+        , getUnconOuterOf = leftOf+        , setSizedInnerOf = \g b -> b { leftOf = g }+        , getSizedOuterOf = rightOf+        , multiplierOf = -1+        , orientationOf = Vertical+        }+      SizedBottom -> makeDivided divided proxy bounds renderGroup+        DivisionConfig+        { setUnconInnerOf = \g b -> b { bottomOf = g }+        , getUnconOuterOf = topOf+        , setSizedInnerOf = \g b -> b { topOf = g }+        , getSizedOuterOf = bottomOf+        , multiplierOf = -1+        , orientationOf = Horizontal+        }+++data DivisionConfig+  = DivisionConfig+    { setUnconInnerOf :: GuideID -> Bounds -> Bounds+    , getUnconOuterOf :: Bounds -> GuideID+    , setSizedInnerOf :: GuideID -> Bounds -> Bounds+    , getSizedOuterOf :: Bounds -> GuideID+    , multiplierOf :: Int+    , orientationOf :: Orientation+    }++makeDivided+ :: (Container s b m, Container u b m)+ => Divided s b u -> Proxy b -> Bounds -> RenderGroup -> DivisionConfig -> LayoutOp b m ()+makeDivided divided proxy bounds renderGroup config = do+  -- sized+  dim <- case orientation of+    -- a Horizontal `orientation` means we're split horizontally so should get the height+    -- and likewise for Vertical and width+    Horizontal -> lift . lift $ minHeight sized proxy+    Vertical -> lift . lift $ minWidth sized proxy+  let size' = m * fromMaybe 0 (size <|> dim)+  sizedInner <- addGuideToLayout $ Relative size' (getSizedOuter bounds) Asymmetric+  addToLayout sized proxy (setSizedInner sizedInner bounds) renderGroup+  -- bar+  unconstrainedInner <- case dynamics of+    Dynamic genBar barSize -> do+      barUncon <- addGuideToLayout $ Relative (m * barSize) sizedInner Symmetric+      -- yes the 'sized' and 'unconstrained' are supposed to be mixed here:+      let barBounds = setSizedInner barUncon . setUnconInner sizedInner $ bounds+      addToLayout (genBar orientation sizedInner) proxy barBounds renderGroup+      addGuideConstraintToLayout $ barUncon `unconConstraint` getUnconOuter bounds+      addGuideConstraintToLayout $ sizedInner `sizedConstraint` getSizedOuter bounds+      return barUncon+    Static -> return sizedInner+  -- unconstrained+  addToLayout unconstrained proxy (setUnconInner unconstrainedInner bounds) renderGroup+  where+    Divided+      { sizeOf = size+      , dynamicsOf = dynamics+      , sizedContentOf = sized+      , unconstrainedContentOf = unconstrained+      } = divided+    DivisionConfig+      { setUnconInnerOf = setUnconInner+      , getUnconOuterOf = getUnconOuter+      , setSizedInnerOf = setSizedInner+      , getSizedOuterOf = getSizedOuter+      , multiplierOf = m+      , orientationOf = orientation+      } = config+    (unconConstraint, sizedConstraint) = if m == 1 then (LTE, GTE) else (GTE, LTE)+++-- | Create a 'Divided' container with the top portion having a particular size.+sizedTop+  :: SizedContentSize Int -- ^ The size of the sized content.+  -> Dynamics b -- ^ The dynamics of the @Divided@ container.+  -> s -- ^ The sized content.+  -> u -- ^ The dynamic content.+  -> Divided s b u+sizedTop = Divided SizedTop . fmap (max 0)++-- | Create a 'Divided' container with the left portion having a particular size.+sizedLeft+  :: SizedContentSize Int -- ^ The size of the sized content.+  -> Dynamics b -- ^ The dynamics of the @Divided@ container.+  -> s -- ^ The sized content.+  -> u -- ^ The dynamic content.+  -> Divided s b u+sizedLeft = Divided SizedLeft . fmap (max 0)++-- | Create a 'Divided' container with the right portion having a particular size.+sizedRight+  :: SizedContentSize Int -- ^ The size of the sized content.+  -> Dynamics b -- ^ The dynamics of the @Divided@ container.+  -> s -- ^ The sized content.+  -> u -- ^ The dynamic content.+  -> Divided s b u+sizedRight = Divided SizedRight . fmap (max 0)++-- | Create a 'Divided' container with the bottom portion having a particular size.+sizedBottom+  :: SizedContentSize Int -- ^ The size of the sized content.+  -> Dynamics b -- ^ The dynamics of the @Divided@ container.+  -> s -- ^ The sized content.+  -> u -- ^ The dynamic content.+  -> Divided s b u+sizedBottom = Divided SizedBottom . fmap (max 0)+
+ src/FULE/Container/Grid.hs view
@@ -0,0 +1,78 @@+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}++-- |+-- Module      : FULE.Container.Grid+-- Description : The @Grid@ Container.+-- Copyright   : (c) Paul Schnapp, 2023+-- License     : BSD3+-- Maintainer  : Paul Schnapp <paul.schnapp@gmail.com>+--+-- A two-dimensional grid of items, evenly spaced.+--+-- You may also wish to consider the 'FULE.Container.Arrayed' Container.+module FULE.Container.Grid+ ( GridM+ , Grid+ , grid+ ) where++import Data.Functor.Identity++import FULE.Component+import FULE.Container+import FULE.Container.Item+import FULE.Internal.Util+import FULE.Layout+import FULE.LayoutOp+++-- | A two-dimensional grid of visual 'FULE.Container.Item.ItemM's, evenly spaced.+data GridM m k+  = Grid+    { rowCountOf :: Int+    , columnCountOf :: Int+    , itemsOf :: [ItemM m k]+    }++-- | Like 'GridM' but run in the 'Data.Functor.Identity.Identity' monad.+type Grid = GridM Identity++instance (Monad m) => Container (GridM m k) k m where+  minWidth (Grid _ c is) p = fmap (* c) . getMaxSize <$> mapM (`minWidth` p) is+  minHeight (Grid r _ is) p = fmap (* r) . getMaxSize <$> mapM (`minHeight` p) is+  addToLayout (Grid r c is) proxy bounds renderGroup = do+    let addBetween f1 f2 p =+          addGuideToLayout $ Between (f1 bounds, p) (f2 bounds, 1-p)+    elasHorizs <- mapM (addBetween topOf bottomOf) (percents r)+    elasVerts <- mapM (addBetween leftOf rightOf) (percents c)+    let tops = topOf bounds : elasHorizs+    let lefts = leftOf bounds : elasVerts+    let rights = elasVerts ++ [rightOf bounds]+    let bottoms = elasHorizs ++ [bottomOf bounds]+    let boundsForItems =+          [Bounds t l r b (clippingOf bounds)+          | (t, b) <- zip tops bottoms+          , (l, r) <- zip lefts rights+          ]+    mapM_ (\(i, b) -> addToLayout i proxy b renderGroup) (zip is boundsForItems)++percents :: Int -> [Double]+percents n = fmap (\i -> fromIntegral i / fromIntegral n) [1..n-1]++-- | Create a 'GridM' of 'FULE.Container.Item.ItemM's.+grid+  :: (Int, Int) -- ^ The number of rows and columns the 'GridM' should have.+  -> [ItemM m k]+  -- ^ The 'FULE.Container.Item.ItemM's to put in the 'GridM'.+  --+  --   Placement of the 'FULE.Container.Item.ItemM's will start with the+  --   top-left position of the grid and proceed to the right, wrapping+  --   around to the next row when the end of the previous row has been reached.+  --+  --   If the number of elements in this list does not meet or exceeds the number+  --   of grid locations available, then up-to the number of grid locations will+  --   be filled, but no more than that.+  -> GridM m k+grid (rows, cols) = Grid (max 0 rows) (max 0 cols)+
+ src/FULE/Container/Item.hs view
@@ -0,0 +1,68 @@+{-# LANGUAGE ExistentialQuantification #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}++-- |+-- Module      : FULE.Container.Item+-- Description : Wrapper for heterogenous content.+-- Copyright   : (c) Paul Schnapp, 2023+-- License     : BSD3+-- Maintainer  : Paul Schnapp <paul.schnapp@gmail.com>+--+-- A wrapper for heterogenous content to be used by other+-- `FULE.Container.Container's.+module FULE.Container.Item+ ( ItemM+ , Item+ , item+ ) where++import Data.Functor.Identity++import FULE.Container+++-- | A container for heterogenous items. This container lets items of different+--   types be used in the same aggregating container, like+--   'FULE.Container.Arrayed.ArrayedM', 'FULE.Container.Grid.GridM', or+--   'FULE.Container.Layered.LayeredM'.+--+--   When using @ItemM@ you'll likely need to:+--   +--   * Use the [@ScopedTypeVariables@](https://wiki.haskell.org/Scoped_type_variables)+--     language extension and explicitly specify a @forall m@ in your+--     function declaration (if a type-variable is being used)+--   * Explicitly specify the type of your list of @ItemM@ in the call to the+--     aggregating container and wrap the list in parentheses+--+--   For example:+--+-- > {-# LANGUAGE ScopedTypeVariables #-}+-- >+-- > import FULE+-- >+-- > ...+-- >+-- > someFn :: forall m => m (ArrayedM m Widget)+-- > someFn = return $+-- >   arrayedHoriz noPadding+-- >     ([item someWidget+-- >     , item someContainer+-- >     , item someOtherWidget+-- >     ]::[ItemM m Widget])+--+--   [Reference: Heterogenous Collections](https://wiki.haskell.org/Heterogenous_collections)+data ItemM m k = forall c . Container c k m => Item c++-- | Like 'ItemM' but run in the 'Data.Functor.Identity.Identity' monad.+type Item = ItemM Identity++instance (Monad m) => Container (ItemM m k) k m where+  minWidth (Item c) = minWidth c+  minHeight (Item c) = minHeight c+  addToLayout (Item c) = addToLayout c++-- | Create an 'ItemM' with a heterogenous item.+item :: (Container c k m) => c -> ItemM m k+item = Item+
+ src/FULE/Container/Layered.hs view
@@ -0,0 +1,49 @@+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}++-- |+-- Module      : FULE.Container.Layered+-- Description : The @Layered@ Container.+-- Copyright   : (c) Paul Schnapp, 2023+-- License     : BSD3+-- Maintainer  : Paul Schnapp <paul.schnapp@gmail.com>+--+-- A 'FULE.Container.Container' to layer multiple items within the same+-- 'FULE.Component.Bounds'.+module FULE.Container.Layered+ ( LayeredM+ , Layered+ , layered+ ) where++import Data.Functor.Identity+import Data.Maybe++import FULE.Container+import FULE.Container.Item+import FULE.Internal.Util+import FULE.LayoutOp+++-- | This container layers multiple visual 'FULE.Container.Item.ItemM's within+--   the same bounding rectangle, one on top of the other. Z-ordering is not+--   really taken into account since that depends on how you are using the+--   layout output, but the @ItemM@s will appear in the output of+--   'FULE.Container.runLayoutOp' in the same order in which they're passed to+--   the 'layered' function.+newtype LayeredM m k = Layered [ItemM m k]++-- | Like 'LayeredM' but run in the 'Data.Functor.Identity.Identity' monad.+type Layered = LayeredM Identity++instance (Monad m) => Container (LayeredM m k) k m where+  minWidth (Layered is) p = getMaxSize <$> mapM (`minWidth` p) is+  minHeight (Layered is) p = getMaxSize <$> mapM (`minHeight` p) is+  addToLayout (Layered is) proxy bounds renderGroup = do+    renderGroup' <- Just <$> maybe nextRenderGroup pure renderGroup+    mapM_ (\i -> addToLayout i proxy bounds renderGroup') is++-- | Layer 'FULE.Container.Item.ItemM's within the same bounding rectangle.+layered :: [ItemM m k] -> LayeredM m k+layered = Layered+
+ src/FULE/Container/Padded.hs view
@@ -0,0 +1,47 @@+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}++-- |+-- Module      : FULE.Container.Padded+-- Description : The @Padded@ Container.+-- Copyright   : (c) Paul Schnapp, 2023+-- License     : BSD3+-- Maintainer  : Paul Schnapp <paul.schnapp@gmail.com>+--+-- A 'FULE.Container.Container' to add padding around content.+module FULE.Container.Padded+ ( Padded+ , padded+ ) where++import FULE.Component+import FULE.Container+import FULE.Container.Config+import FULE.Layout+import FULE.LayoutOp+++-- | A container with padding around the content.+data Padded c+  = Padded+    { horizPaddingOf :: Int+    , vertPaddingOf :: Int+    , contentsOf :: c+    }++instance (Container c k m) => Container (Padded c) k m where+  minWidth (Padded h _ c) proxy = fmap (+ 2 * h) <$> minWidth c proxy+  minHeight (Padded _ v c) proxy = fmap (+ 2 * v) <$> minHeight c proxy+  addToLayout (Padded h v c) proxy bounds renderGroup = do+    let Bounds t l r b cl = bounds+    t' <- if v == 0 then return t else addGuideToLayout $ Relative v t Asymmetric+    l' <- if h == 0 then return l else addGuideToLayout $ Relative h l Asymmetric+    r' <- if h == 0 then return r else addGuideToLayout $ Relative (-h) r Asymmetric+    b' <- if v == 0 then return b else addGuideToLayout $ Relative (-v) b Asymmetric+    let bounds' = Bounds t' l' r' b' cl+    addToLayout c proxy bounds' renderGroup++-- | Create a container with padding around the content.+padded :: Padding -> c -> Padded c+padded (horiz, vert) = Padded (max 0 horiz) (max 0 vert)+
+ src/FULE/Container/Positioned.hs view
@@ -0,0 +1,182 @@+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}++-- |+-- Module      : FULE.Container.Positioned+-- Description : The @Positioned@ Container.+-- Copyright   : (c) Paul Schnapp, 2023+-- License     : BSD3+-- Maintainer  : Paul Schnapp <paul.schnapp@gmail.com>+--+-- A 'FULE.Container.Container' to position content relative to its parent+-- container, including in the center.+module FULE.Container.Positioned+ ( Positioned+ , topLeft+ , topMiddle+ , topRight+ , middleLeft+ , centered+ , middleRight+ , bottomLeft+ , bottomMiddle+ , bottomRight+ ) where++import Control.Monad.Trans.Class+import Data.Proxy++import FULE.Component+import FULE.Container+import FULE.Layout+import FULE.LayoutOp+++data Position+  = TopLeft+  | TopMiddle+  | TopRight+  | MiddleLeft+  | MiddleMiddle+  | MiddleRight+  | BottomLeft+  | BottomMiddle+  | BottomRight++-- | A container for positioning content within a larger container in one of+--   nine positions relative to the parent. Relative positioning will adjust+--   to keep up with changes to the size of the parent container.+data Positioned c = Positioned Position c++instance (Container c k m) => Container (Positioned c) k m where+  minWidth (Positioned _ c) = minWidth c+  minHeight (Positioned _ c) = minHeight c+  addToLayout (Positioned position c) proxy bounds renderGroup = do+    reqWidth <- lift . lift $ minWidth c proxy+    reqHeight <- lift . lift $ minHeight c proxy+    let clipping = clippingOf bounds+    bounds' <- case position of+      TopLeft -> do+        vert <- adhereTop bounds reqHeight+        horiz <- adhereLeft bounds reqWidth+        return (makeBounds vert horiz clipping)+      TopMiddle -> do+        vert <- adhereTop bounds reqHeight+        horiz <- adhereMiddleHoriz bounds reqWidth+        return (makeBounds vert horiz clipping)+      TopRight -> do+        vert <- adhereTop bounds reqHeight+        horiz <- adhereRight bounds reqWidth+        return (makeBounds vert horiz clipping)+      MiddleLeft -> do+        vert <- adhereMiddleVert bounds reqHeight+        horiz <- adhereLeft bounds reqWidth+        return (makeBounds vert horiz clipping)+      MiddleMiddle -> do+        vert <- adhereMiddleVert bounds reqHeight+        horiz <- adhereMiddleHoriz bounds reqWidth+        return (makeBounds vert horiz clipping)+      MiddleRight -> do+        vert <- adhereMiddleVert bounds reqHeight+        horiz <- adhereRight bounds reqWidth+        return (makeBounds vert horiz clipping)+      BottomLeft -> do+        vert <- adhereBottom bounds reqHeight+        horiz <- adhereLeft bounds reqWidth+        return (makeBounds vert horiz clipping)+      BottomMiddle -> do+        vert <- adhereBottom bounds reqHeight+        horiz <- adhereMiddleHoriz bounds reqWidth+        return (makeBounds vert horiz clipping)+      BottomRight -> do+        vert <- adhereBottom bounds reqHeight+        horiz <- adhereRight bounds reqWidth+        return (makeBounds vert horiz clipping)+    addToLayout c proxy bounds' renderGroup++adhereTop :: (Monad m) => Bounds -> Maybe Int -> LayoutOp k m (GuideID, GuideID)+adhereTop bounds Nothing = return (topOf bounds, bottomOf bounds)+adhereTop (Bounds { topOf = top }) (Just h) = do+  bottom <- addGuideToLayout $ Relative h top Asymmetric+  return (top, bottom)++adhereMiddleVert :: (Monad m) => Bounds -> Maybe Int -> LayoutOp k m (GuideID, GuideID)+adhereMiddleVert bounds Nothing = return (topOf bounds, bottomOf bounds)+adhereMiddleVert bounds (Just h) = do+  horiz <- addGuideToLayout $ Between (topOf bounds, 0.5) (bottomOf bounds, 0.5)+  top <- addGuideToLayout $ Relative (-1 * (h `div` 2)) horiz Asymmetric+  bottom <- addGuideToLayout $ Relative h top Symmetric+  return (top, bottom)++adhereBottom :: (Monad m) => Bounds -> Maybe Int -> LayoutOp k m (GuideID, GuideID)+adhereBottom bounds Nothing = return (topOf bounds, bottomOf bounds)+adhereBototm (Bounds { bottomOf = bottom }) (Just h) = do+  top <- addGuideToLayout $ Relative (-1 * h) bottom Asymmetric+  return (top, bottom)++adhereLeft :: (Monad m) => Bounds -> Maybe Int -> LayoutOp k m (GuideID, GuideID)+adhereLeft bounds Nothing = return (leftOf bounds, rightOf bounds)+adhereLeft (Bounds { leftOf = left }) (Just w) = do+  right <- addGuideToLayout $ Relative w left Asymmetric+  return (left, right)++adhereMiddleHoriz :: (Monad m) => Bounds -> Maybe Int -> LayoutOp k m (GuideID, GuideID)+adhereMiddleHoriz bounds Nothing = return (leftOf bounds, rightOf bounds)+adhereMiddleHoriz bounds (Just w) = do+  vert <- addGuideToLayout $ Between (leftOf bounds, 0.5) (rightOf bounds, 0.5)+  left <- addGuideToLayout $ Relative (-1 * (w `div` 2)) vert Asymmetric+  right <- addGuideToLayout $ Relative w left Symmetric+  return (left, right)++adhereRight :: (Monad m) => Bounds -> Maybe Int -> LayoutOp k m (GuideID, GuideID)+adhereRight bounds Nothing = return (leftOf bounds, rightOf bounds)+adhereRight (Bounds { rightOf = right }) (Just w) = do+  left <- addGuideToLayout $ Relative (-1 * w) right Asymmetric+  return (left, right)++makeBounds :: (GuideID, GuideID) -> (GuideID, GuideID) -> Maybe Bounds -> Bounds+makeBounds (top, bottom) (left, right) clipping =+  Bounds+  { topOf = top+  , leftOf = left+  , rightOf = right+  , bottomOf = bottom+  , clippingOf = clipping+  }++-- | Position content in the /top-left/ corner of its parent container.+topLeft :: c -> Positioned c+topLeft = Positioned TopLeft++-- | Position content in the middle of the /top/ side of its parent container.+topMiddle :: c -> Positioned c+topMiddle = Positioned TopMiddle++-- | Position content in the /top-right/ corner of its parent container.+topRight :: c -> Positioned c+topRight = Positioned TopRight++-- | Position content in the middle of the /left/ side of its parent container.+middleLeft :: c -> Positioned c+middleLeft = Positioned MiddleLeft++-- | Position content in the very center of its parent container.+centered :: c -> Positioned c+centered = Positioned MiddleMiddle++-- | Position content in the middle of the /right/ side of its parent container.+middleRight :: c -> Positioned c+middleRight = Positioned MiddleRight++-- | Position content in the /bottom-left/ corner of its parent container.+bottomLeft :: c -> Positioned c+bottomLeft = Positioned BottomLeft++-- | Position content in the middle of the /bottom/ side of its parent container.+bottomMiddle :: c -> Positioned c+bottomMiddle = Positioned BottomMiddle++-- | Position content in the /bottom-right/ corner of its parent container.+bottomRight :: c -> Positioned c+bottomRight = Positioned BottomRight+
+ src/FULE/Container/Sized.hs view
@@ -0,0 +1,61 @@+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}++-- |+-- Module      : FULE.Container.Sized+-- Description : The @Sized@ Container.+-- Copyright   : (c) Paul Schnapp, 2023+-- License     : BSD3+-- Maintainer  : Paul Schnapp <paul.schnapp@gmail.com>+--+-- A 'FULE.Conatiner.Container' to specify or override the (inherent) size of+-- content.+--+-- To /remove/ the size of content from consideration during the layout process+-- see the 'FULE.Container.Unreckoned.Unreckoned' container.+module FULE.Container.Sized+ ( Sized+ , sizedHoriz+ , sizedVert+ , sized+ ) where++import Control.Applicative++import FULE.Container+++-- | A container which specifies or overrides the size of content in the layout.+data Sized c+  = Sized+    { widthOf :: Maybe Int+    , heightOf :: Maybe Int+    , contentsOf :: c+    }++instance (Container c k m) => Container (Sized c) k m where+  minWidth (Sized w _ c) p = (w <|>) <$> minWidth c p+  minHeight (Sized _ h c) p = (h <|>) <$> minHeight c p+  addToLayout (Sized _ _ c) = addToLayout c++-- | Add or override the horizontal size of the content.+sizedHoriz+  :: Int -- ^ The width the content should have.+  -> c -- ^ The content.+  -> Sized c+sizedHoriz width = Sized (Just $ max 0 width) Nothing++-- | Add or override the vertical size of the content.+sizedVert+  :: Int -- ^ The height the content should have.+  -> c -- ^ The content.+  -> Sized c+sizedVert height = Sized Nothing (Just $ max 0 height)++-- | Add or override the size of the content in both dimensions.+sized+  :: (Int, Int) -- ^ The width and height the content should have.+  -> c -- ^ The content.+  -> Sized c+sized (width, height) = Sized (Just $ max 0 width) (Just $ max 0 height)+
+ src/FULE/Container/Unreckoned.hs view
@@ -0,0 +1,71 @@+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}++-- |+-- Module      : FULE.Container.Unreckoned+-- Description : The @Unreckoned@ Container.+-- Copyright   : (c) Paul Schnapp, 2023+-- License     : BSD3+-- Maintainer  : Paul Schnapp <paul.schnapp@gmail.com>+--+-- A 'FULE.Container.Container' to remove dimension info from content when it's+-- considered during the layout process.+--+-- To /add/ size to content, see the 'FULE.Container.Sized.Sized' container.+module FULE.Container.Unreckoned+ ( Unreckoned+ , unreckonedHoriz+ , unreckonedVert+ , unreckoned+ ) where++import Control.Monad.Trans.Class+import Data.Maybe++import FULE.Component+import FULE.Container+import FULE.Container.Config+import FULE.Layout+import FULE.LayoutOp+++-- | A container complementary to 'FULE.Container.Sized.Sized': whereas @Sized@+--   specifies a size for content which may or may not already have one,+--   @Unreckoned@ /removes/ size associated with content as it is reckoned+--   in the layout. This allows for content to overflow its bounds as far as the+--   layout is concerned.+--+--   The 'FULE.Component.Bounds' in the 'FULE.Component.ComponentInfo' for a+--   'FULE.Component.Component' which is @Unreckoned@ will match the overflowing+--   size of the @Component@ even though the size was not taken into account+--   during the layout process itself.+data Unreckoned c = Unreckoned (Maybe Orientation) c++instance (Container c k m) => Container (Unreckoned c) k m where+  minWidth (Unreckoned o c) proxy = case o of+    Just Vertical -> minWidth c proxy+    _ -> return Nothing+  minHeight (Unreckoned o c) proxy = case o of+    Just Horizontal -> minHeight c proxy+    _ -> return Nothing+  addToLayout (Unreckoned _ c) proxy bounds renderGroup = do+    reqWidth <- lift . lift $ minWidth c proxy+    reqHeight <- lift . lift $ minHeight c proxy+    let Bounds t l r b cl = bounds+    right <- addGuideToLayout $ Relative (fromMaybe 0 reqWidth) l Asymmetric+    bottom <- addGuideToLayout $ Relative (fromMaybe 0 reqHeight) t Asymmetric+    let bounds = Bounds t l right bottom cl+    addToLayout c proxy bounds renderGroup++-- | Elide the horizontal size of the content.+unreckonedHoriz :: c -> Unreckoned c+unreckonedHoriz = Unreckoned (Just Horizontal)++-- | Elide the verital size of the content.+unreckonedVert :: c -> Unreckoned c+unreckonedVert = Unreckoned (Just Vertical)++-- | Elide all sizes associated with the content.+unreckoned :: c -> Unreckoned c+unreckoned = Unreckoned Nothing+
+ src/FULE/Container/Window.hs view
@@ -0,0 +1,86 @@+{-# LANGUAGE FlexibleContexts #-}++-- |+-- Module      : FULE.Container.Window+-- Description : The @Window@ Container and layout construction functions.+-- Copyright   : (c) Paul Schnapp, 2023+-- License     : BSD3+-- Maintainer  : Paul Schnapp <paul.schnapp@gmail.com>+--+-- This module contains @Window@, the base 'FULE.Container.Conatiner' for all+-- layouts, and the @layout@ and @layoutM@ functions for building a+-- 'Fule.Laout.Layout' from it.+module FULE.Container.Window+ ( Window+ , WindowAdjustorGen+ , window+ , layoutM+ , layout+ ) where++import Control.Arrow+import Data.Functor.Identity+import Data.Proxy++import FULE.Component+import FULE.Container+import FULE.Layout+import FULE.LayoutOp+++-- | Type of a function to produce a 'FULE.Component.Component' to adjust+--   the 'FULE.Layout.Layout' in response to a change in the size of the window+--   in the encompassing GUI framework. The @Component@ should use the Guides+--   passed as arguments to this function to update the @Layout@.+type WindowAdjustorGen k+  =  GuideID+  -- ^ The Guide to use to adjust the /width/ of the 'FULE.Layout' in response+  --   to a change in the window size. Adjustments should be made using the delta+  --   of the old and new sizes.+  -> GuideID+  -- ^ The Guide to use to adjust the /height/ of the 'FULE.Layout' in response+  --   to a change in the window size. Adjustments should be made using the delta+  --   of the old and new sizes.+  -> k++-- | The base container of any (non-custom) 'FULE.Layout.Layout' representing+--   the window in the encompassing GUI framework. It is the only container that+--   can be used with the 'layout' and 'layoutM' functions to build a @Layout@.+data Window c k+  = Window+    { widthOf :: Int+    , heightOf :: Int+    , controlGenOf :: WindowAdjustorGen k+    , contentsOf :: c+    }++-- | Create a 'Window'.+window+  :: (Int, Int) -- ^ The width and height of the window.+  -> WindowAdjustorGen k+  -- ^ A function to construct a 'FULE.Component.Component' for reacting to+  --   changes in the size of the window in the encompassing GUI framework.+  -> c -- ^ The content of the window.+  -> Window c k+window (width, height) = Window (max 0 width) (max 0 height)+++-- | Build a layout for a 'Window' in the specified monad @m@.+layoutM :: (Container c k m) => Window c k -> m (Layout, [ComponentInfo k])+layoutM = (first build <$>) . runLayoutOp . makeLayoutOp++-- | Build a layout for a 'Window' in the 'Data.Functor.Identity.Identity' monad.+layout :: (Container c k Identity) => Window c k -> (Layout, [ComponentInfo k])+layout = first build . runIdentity . runLayoutOp . makeLayoutOp++makeLayoutOp :: (Container c k m) => Window c k -> LayoutOp k m ()+makeLayoutOp (Window w h gen c) = do+  top <- addGuideToLayout $ Absolute 0+  left <- addGuideToLayout $ Absolute 0+  right <- addGuideToLayout $ Absolute w+  bottom <- addGuideToLayout $ Absolute h+  let bounds = Bounds top left right bottom Nothing+  let proxy = Proxy :: Proxy k+  addToLayout (gen right bottom) proxy bounds Nothing+  addToLayout c proxy bounds Nothing+
+ src/FULE/Internal/Sparse.hs view
@@ -0,0 +1,102 @@+module FULE.Internal.Sparse+ ( Matrix+ , Pos+ , empty+ , eye+ , matrix+ , dims+ , expandTo+ , get+ , set+ , del+ , count+ , add+ , sub+ , mul+ , star+ , filter+ ) where++import Prelude hiding (filter)+import Control.DeepSeq+import Data.List hiding (filter)+import qualified Data.Map.Strict as Map+++type Pos = (Int, Int)++maxp (rl, cl) (rr, cr) = (max rl rr, max cl cr)+++data Matrix a+  = M+    { dimensionsOf :: (Int, Int)+    , matrixOf :: Map.Map Pos a+    }++instance (NFData a) => NFData (Matrix a) where+  rnf (M (r, c) m) = seq r . seq c . deepseq m $ ()++instance (Num a, Show a) => Show (Matrix a) where+  show (M (0, 0) _) = "[]"+  show m@(M (r, c) _) = concat ["[ ", mat, "\n]"]+    where+      row rx = intercalate ", " $ map (\cx -> show $ get (rx, cx) m) [1..c]+      mat = intercalate "\n; " $ map row [1..r]++instance Functor Matrix where+  fmap f (M d m) = M d $ Map.map f m++empty :: Matrix a+empty = M (0, 0) Map.empty++eye :: (Num a) => Int -> Matrix a+eye dim = matrix (dim, dim) [((r,c), 1) | (r, c) <- zip [1..dim] [1..dim]]++matrix :: (Int, Int) -> [(Pos, a)] -> Matrix a+matrix dims entries = M dims (Map.fromList entries)++dims :: Matrix a -> (Int, Int)+dims = dimensionsOf++expandTo :: (Int, Int) -> Matrix a -> Matrix a+expandTo d (M _ m) = M d m++get :: Num a => Pos -> Matrix a -> a+get pos m = Map.findWithDefault 0 pos $ matrixOf m++set :: (Eq a, Num a) => Pos -> a -> Matrix a -> Matrix a+set pos v (M d m) = M (maxp pos d) m'+  where m' = if v == 0 then Map.delete pos m else Map.insert pos v m++del :: Pos -> Matrix a -> Matrix a+del pos m = m { matrixOf = Map.delete pos $ matrixOf m }++-- remove row/col (both?)++count :: Matrix a -> Int+count = Map.size . matrixOf++add :: (Num a) => Matrix a -> Matrix a -> Matrix a+add (M dl ml) (M dr mr) = M (maxp dl dr) $ Map.unionWith (+) ml mr++sub :: (Num a) => Matrix a -> Matrix a -> Matrix a+sub (M dl ml) (M dr mr) = M (maxp dl dr) $ Map.unionWith (-) ml mr++mul :: (Num a) => Matrix a -> Matrix a -> Matrix a+mul (M (r, _) ml) (M (_, c) mr) =+  -- not the most efficient algorithm but very concise+  M (r, c) $ Map.fromListWith (+) $+  [((rl,cr), vl*vr) | ((rl,cl), vl) <- ps ml, ((rr,cr), vr) <- ps mr, cl==rr]+  where ps = Map.toList++star :: (Num a) => Matrix a -> Matrix a -> Matrix a+star (M (r, _) ml) (M (_, c) mr) =+  -- not the most efficient algorithm but very concise+  M (r, c) $ Map.fromListWith (*) $+  [((rl,cr), vl*vr) | ((rl,cl), vl) <- ps ml, ((rr,cr), vr) <- ps mr, cl==rr]+  where ps = Map.toList++filter :: (a -> Bool) -> Matrix a -> Matrix a+filter f (M d m) = M d $ Map.filter f m+
+ src/FULE/Internal/Util.hs view
@@ -0,0 +1,17 @@+module FULE.Internal.Util where++import Data.Maybe+++collapseTo :: ([a] -> b) -> [Maybe a] -> Maybe b+collapseTo f ms =+  case catMaybes ms of+    [] -> Nothing+    ls -> Just (f ls)++getMaxSize :: [Maybe Int] -> Maybe Int+getMaxSize = collapseTo maximum++getTotalSize :: [Maybe Int] -> Maybe Int+getTotalSize = collapseTo sum+
+ src/FULE/Layout.hs view
@@ -0,0 +1,357 @@+-- |+-- Module      : FULE.Layout+-- Description : Low-level layout functionality.+-- Copyright   : (c) Paul Schnapp, 2023+-- License     : BSD3+-- Maintainer  : Paul Schnapp <paul.schnapp@gmail.com>+--+-- This is the basic, low-level layout functionality.+--+-- You'll start by creating a 'FULE.Layout.LayoutDesign' and then make a usable+-- 'FULE.Layout.Layout' from it by 'FULE.Layout.build'ing it.+module FULE.Layout+ ( LayoutDesign+ , emptyLayoutDesign+ --+ , GuideID+ , PlasticDependencyType(..)+ , GuideSpecification(..)+ , addGuide+ --+ , GuideConstraint(..)+ , addGuideConstraint+ --+ , Layout+ , build+ , design+ , getGuide+ , getGuides+ , reactToChange+ , reactToChanges+ ) where++import Control.DeepSeq++import FULE.Internal.Sparse as Matrix+++--------------------------------+-- LayoutDesign+--------------------------------++-- | A 'Layout' that is still under construction.+--   Use the 'build' function to turn a @LayoutDesign@ into an elivened @Layout@.+data LayoutDesign+  = LayoutDesign+    { designPlasticityOf :: Matrix Double+    , designElasticityOf :: Matrix Double+    , designLTEConstraintsOf :: Matrix Double+    , designGTEConstraintsOf :: Matrix Double+    , designGuidesOf :: Matrix Double+    }++instance NFData LayoutDesign where+  rnf (LayoutDesign p e lte gte g) =+    deepseq p . deepseq e . deepseq lte . deepseq gte . deepseq g $ ()++-- | Create a new 'LayoutDesign'.+emptyLayoutDesign :: LayoutDesign+emptyLayoutDesign = LayoutDesign empty empty empty empty empty+++-- | An identifier for a Guide in a 'Layout' or 'LayoutDesign'.+newtype GuideID = G Int+  deriving (Eq, Ord, Read, Show)++instance NFData GuideID where+  rnf g@(G i) = seq g . deepseq i $ ()+++-- | The type of a plastic dependency between two Guides.+data PlasticDependencyType+  = Asymmetric+  -- ^ Specifies that changes to the dependent Guide do not affect the reference+  --   Guide, but changes to the reference propagate to the dependent Guide.+  | Symmetric+  -- ^ Specifies that changes to either Guide are applied to the other as well.+  deriving (Eq, Show)++-- | The specification of a Guide to be added to a 'LayoutDesign'.+--   A Guide may be added:+--+--   * at an absolute position within the design+--   * relative to a reference Guide within the design with a plastic+--     dependencey upon the reference+--   * relative to two reference Guides within the design with an elastic+--     dependency upon both+--+--   See each constructor and its fields for more information.+data GuideSpecification+  = Absolute -- ^ Add a new Guide at an absolute position within the @Layout@.+    { positionOf :: Int+    -- ^ The position the new Guide should have in the @Layout@.+    --   Note this could be either an @x@ or @y@ position, the axis doesn't+    --   matter for the specification.+    }+  | Relative -- ^ Add a new Guide with a plastic dependence on a reference Guide.+    { offsetOf :: Int+    -- ^ The offset from the reference Guide the new dependent Guide should have.+    , dependencyOf :: GuideID+    -- ^ The ID of the reference Guide.+    , dependencyTypeOf :: PlasticDependencyType+    -- ^ The type of dependency the dependent Guide should have on the reference+    --   Guide.+    }+  | Between+    -- ^ Add a new Guide between two other Guides with an elastic dependency on them:+    --   Whenever one of the reference Guides moves the dependent Guide will be moved+    --   to remain positioned relatively between them.+    --+    --   The @Double@ arguments of the pairs below should sum to equal @1.0@;+    --   this will not be checked.+      (GuideID, Double)+      -- ^ A reference Guide and how close the dependent Guide should be to it+      --   relative to the other reference, as a percentage.+      (GuideID, Double)+      -- ^ Another reference Guide and how close the dependent Guide should be+      --   to it relative to the first reference, as a percentage.+++-- | Add a new Guide to a 'LayoutDesign' according to the given 'GuideSpecification'.+--+--   Returns an ID for the new Guide along with an updated 'LayoutDesign'.+addGuide :: GuideSpecification -> LayoutDesign -> (GuideID, LayoutDesign)+addGuide (Absolute pos) = addAbsolute pos+addGuide (Relative offset gid dep) = addRelative offset gid dep +addGuide (Between r1 r2) = addBetween r1 r2++type LayoutDesignOp = LayoutDesign -> (GuideID, LayoutDesign)++addAbsolute :: Int -> LayoutDesignOp+addAbsolute position design =+  ( G gid+  , LayoutDesign+    { designPlasticityOf = set (gid, gid) 1 (designPlasticityOf design)+    , designElasticityOf = expandTo (gid, gid) (designElasticityOf design)+    , designLTEConstraintsOf = expandTo (gid, gid) (designLTEConstraintsOf design)+    , designGTEConstraintsOf = expandTo (gid, gid) (designGTEConstraintsOf design)+    , designGuidesOf = set (gid, 1) (fromIntegral position) (designGuidesOf design)+    }+  )+  where+    gid = nextGuideNumberFor design++addRelative :: Int -> GuideID -> PlasticDependencyType -> LayoutDesignOp+addRelative offset (G ref) dep design@(LayoutDesign { designGuidesOf = guides }) =+  ( G gid+  , LayoutDesign+    { designPlasticityOf =+        set (gid, gid) 1 . set (gid, ref) 1 . symRelat $ designPlasticityOf design+    , designElasticityOf = expandTo (gid, gid) (designElasticityOf design)+    , designLTEConstraintsOf = expandTo (gid, gid) (designLTEConstraintsOf design)+    , designGTEConstraintsOf = expandTo (gid, gid) (designGTEConstraintsOf design)+    , designGuidesOf = set (gid, 1) pos guides+    }+  )+  where+    gid = nextGuideNumberFor design+    symRelat = case dep of+      Asymmetric -> id+      Symmetric  -> set (ref, gid) 1+    pos = fromIntegral offset + get (ref, 1) guides++addBetween :: (GuideID, Double) -> (GuideID, Double) -> LayoutDesignOp+addBetween (G ref1, pct1) (G ref2, pct2) design@(LayoutDesign { designGuidesOf = guides }) =+  ( G gid+  , LayoutDesign+    { designPlasticityOf = set (gid, gid) 1 (designPlasticityOf design)+    , designElasticityOf =+        -- yes the indicies are supposed to mismatch in this+        set (gid, ref1) pct2 . set (gid, ref2) pct1 $+        expandTo (gid, gid) (designElasticityOf design)+    , designLTEConstraintsOf = expandTo (gid, gid) (designLTEConstraintsOf design)+    , designGTEConstraintsOf = expandTo (gid, gid) (designGTEConstraintsOf design)+    , designGuidesOf = set (gid, 1) pos guides+    }+  )+  where+    gid = nextGuideNumberFor design+    -- yes the indicies are supposed to mismatch in this+    pos = pct2 * get (ref1, 1) guides + pct1 * get (ref2, 1) guides++nextGuideNumberFor :: LayoutDesign -> Int+nextGuideNumberFor (LayoutDesign { designGuidesOf = guides }) =+  (+1) . fst $ dims guides+++--------------------------------+-- Guide Constraints+--------------------------------++-- | The type of constraint one Guide should have relative to another.+data GuideConstraint+  = LTE+    -- ^ Constrain a Guide to be always less-than or equal-to another.+    { constrainedOf :: GuideID+    -- ^ The Guide to constrain the movement of.+    , referenceOf :: GuideID+    -- ^ The reference Guide to constrain movement relative to.+    }+  | GTE+    -- ^ Constrain a Guide to be always greater-than or equal-to another.+    { constrainedOf :: GuideID+    -- ^ The Guide to constrain the movement of.+    , referenceOf :: GuideID+    -- ^ The reference Guide to constrain movement relative to.+    }+  deriving (Eq, Show)++-- | Constrain the movement of one Guide relative to another. (Still slightly+--    experimental.)+--+--   __Important Notes:__+--+--   * Never constrain a Guide against itself+--   * A Guide should be used /only once/ as the constrainee (first argument)+--     for a given constraint-type+--   * The above conditions will not be checked!+--   * If a guide depends on multiple other guides that are simultaneously+--     affected by constraints, things may go a bit wonky, just sayin'.+addGuideConstraint :: GuideConstraint -> LayoutDesign -> LayoutDesign+addGuideConstraint constraint design =+  case constraint of+    LTE (G forGuide) (G ofGuide) ->+      design+      { designLTEConstraintsOf =+          set (forGuide, forGuide) 1+          . set (forGuide, ofGuide) (-1)+          $ designLTEConstraintsOf design+      }+    GTE (G forGuide) (G ofGuide) ->+      design+      { designGTEConstraintsOf =+          set (forGuide, forGuide) 1+          . set (forGuide, ofGuide) (-1)+          $ designGTEConstraintsOf design+      }+++--------------------------------+-- Layout+--------------------------------++-- | A 'LayoutDesign' that has been enlivened and can have its Guides queried or+--   moved.+data Layout+  = Layout+    { layoutDesignOf :: LayoutDesign+    , layoutLTEConstraintsOf :: Matrix Double+    , layoutGTEConstraintsOf :: Matrix Double+    , layoutTransformationOf :: Matrix Double+    , layoutGuidesOf :: Matrix Double+    }++instance NFData Layout where+  rnf (Layout d lte gte tx g) =+    deepseq d . deepseq lte . deepseq gte . deepseq tx . deepseq g $ ()++instance Show Layout where+  show l = concat+    [ "\n"+    , show (layoutTransformationOf l)+    , "\n\n"+    , show (layoutGuidesOf l)+    , "\n"+    ]++propPlas :: (Num a) => Matrix a -> Matrix a+propPlas m =+  let m' = m `star` m+  -- Note: could possibly encounter a cycle and not know it, but this matrix+  -- should be idempotent so this condition should be ok.+  in if count m' == count m+  then m'+  else propPlas m'+++propElas :: (Num a) => Matrix a -> Matrix a+propElas m = go m m+  where+    go s p =+      let p' = m `mul` p+      in if count p' == 0+      then s+      else go (s `add` p') p'++-- | Create an enlivened 'Layout' from a 'LayoutDesign'.+build :: LayoutDesign -> Layout+build design =+  Layout+  { layoutDesignOf = design+  , layoutLTEConstraintsOf = lte+  , layoutGTEConstraintsOf = gte+  , layoutTransformationOf = transform+  , layoutGuidesOf = dg+  }+  where+    LayoutDesign+      { designPlasticityOf = plas+      , designElasticityOf = elas+      , designLTEConstraintsOf = lte+      , designGTEConstraintsOf = gte+      , designGuidesOf = dg+      } = design+    pp = propPlas plas+    pe = propElas elas+    ph = pp `sub` (eye . fst . dims $ plas)+    transform = pp `add` pe+      `add` (ph `mul` pe)+      `add` (pe `mul` ph)+      `add` (ph `mul` pe `mul` ph)++-- | Transform a 'Layout' back into a 'LayoutDesign'.+design :: Layout -> LayoutDesign+design layout =+  (layoutDesignOf layout) { designGuidesOf = layoutGuidesOf layout }++-- | Get the position of a Guide within a 'Layout'.+getGuide :: GuideID -> Layout -> Int+getGuide (G gid) = floor . get (gid, 1) . layoutGuidesOf++-- | Get the position of multiple Guides within a 'Layout'.+getGuides :: [GuideID] -> Layout -> [Int]+getGuides gs layout = map (`getGuide` layout) gs++-- | Move a Guide within a 'Layout'.+reactToChange+  :: GuideID -- ^ The Guide to move.+  -> Int -- ^ The movement to apply to the Guide -- a delta.+  -> Layout -> Layout+reactToChange (G gid) amt =+  doReactToChanges [((gid, 1), fromIntegral amt)]++-- | Move multiple Guides within a 'Layout'.+reactToChanges+  :: [(GuideID, Int)]+  -- ^ A list of Guides with movements (deltas) to apply to them.+  -> Layout -> Layout+reactToChanges pairs =+  let convert (G gid, amt) = ((gid, 1), fromIntegral amt)+  in doReactToChanges (map convert pairs)++doReactToChanges :: [(Pos, Double)] -> Layout -> Layout+doReactToChanges entries layout =+  layout { layoutGuidesOf = adjusted }+  where+    Layout+      { layoutLTEConstraintsOf = lte+      , layoutGTEConstraintsOf = gte+      , layoutTransformationOf = t+      , layoutGuidesOf = g+      } = layout+    changes = matrix (dims g) entries+    changed = t `mul` changes `add` g+    adjusted = changed+      `sub` (t `mul` Matrix.filter (> 0) (lte `mul` changed))+      `sub` (t `mul` Matrix.filter (< 0) (gte `mul` changed))+
+ src/FULE/LayoutOp.hs view
@@ -0,0 +1,68 @@+-- |+-- Module      : FULE.LayoutOp+-- Description : @LayoutDesign@ creation helpers.+-- Copyright   : (c) Paul Schnapp, 2023+-- License     : BSD3+-- Maintainer  : Paul Schnapp <paul.schnapp@gmail.com>+--+-- Operations for constructing a 'FULE.Layout.LayoutDesign'.+module FULE.LayoutOp+ ( LayoutOp+ , LayoutOpState+ , runLayoutOp+ , addGuideToLayout+ , addGuideConstraintToLayout+ , nextRenderGroup+ ) where++import Control.Monad.Trans.State+import Control.Monad.Writer++import FULE.Component+import FULE.Layout+++-- | Internal.+data LayoutOpState+  = LOS+    { builderOf :: LayoutDesign+    , currentRenderGroupOf :: Int+    }++-- | An operation that will produce a 'FULE.Layout.LayoutDesign' and a list of+--   components of type @k@ in the monad @m@.+type LayoutOp k m = StateT LayoutOpState (WriterT [ComponentInfo k] m)++-- | Run a 'LayoutOp' to create a 'FULE.Layout.LayoutDesign' and a list of+--   components of type @k@.+runLayoutOp :: (Monad m) => LayoutOp k m () -> m (LayoutDesign, [ComponentInfo k])+runLayoutOp = (toOutput <$>) . runWriterT . (`execStateT` LOS emptyLayoutDesign 0)+  where toOutput (LOS builder _, components) = (builder, components)++-- | Add a Guide to the 'FULE.Layout.LayoutDesign'.+addGuideToLayout :: (Monad m) => GuideSpecification -> LayoutOp k m GuideID+addGuideToLayout r = do+  state <- get+  let (guideID, builder) = addGuide r (builderOf state)+  put state { builderOf = builder }+  return guideID++-- | Add a Guide constraint to the 'FULE.Layout.LayoutDesign'.+addGuideConstraintToLayout+  :: (Monad m)+  => GuideConstraint -> LayoutOp k m ()+addGuideConstraintToLayout constraint = do+  state <- get+  let builder = addGuideConstraint constraint (builderOf state)+  put state { builderOf = builder }++-- | Get the next available render group from the 'LayoutOp' state and advance+--   to the next one internally.+nextRenderGroup :: (Monad m) => LayoutOp k m Int+nextRenderGroup = do+  state <- get+  let renderGroup = currentRenderGroupOf state+  put state { currentRenderGroupOf = renderGroup + 1 }+  return renderGroup++
+ test/Spec.hs view
@@ -0,0 +1,2 @@+main :: IO ()+main = putStrLn "Test suite not yet implemented"