lue-bird / elm-scroll / Scroll

📜 Items rolled up on both sides of a focus

→ good fit for dynamic choice selection: tabs, playlist, timeline... ↑ examples

Scroll can even focus a gap Down or Up from every item

Not what you were looking for? Check out alternatives


type Scroll item focusGapTag possiblyOrNever
    = Scroll ({ before : Emptiable (Stacked item) Possibly, focus : Emptiable item possiblyOrNever, after : Emptiable (Stacked item) Possibly })

📜 Items rolled up on both sides of a focus

→ good fit for dynamic choice selection: tabs, playlist, ...

Scroll can even focus a gap Down and Up every item:

in arguments

empty : Scroll item_ FocusGap Possibly

in types

type alias Model =
    RecordWithoutConstructorFunction
        { choice : Scroll Option FocusGap Never
        }

(where RecordWithoutConstructorFunction stops the compiler from creating a constructor function for Model)


type FocusGap
    = FocusGap Basics.Never

A word in every Scroll type:

position


type Location
    = AtSide Linear.Direction Basics.Int
    | AtFocus

Position in a Scroll relative to its focus

import Linear exposing (Direction(..))
import Emptiable exposing (Emptiable, filled)
import Stack exposing (topBelow)

Scroll.one 0
    |> Scroll.sideAlter Up
        (\_ -> topBelow 1 [ 2, 3 ])
    |> Scroll.to (Scroll.AtSide Up 2)
    |> Emptiable.map Scroll.focusFill
--> filled 3
--: Emptiable (Stacked number_) Possibly

nearest : Linear.Direction -> Location

The Location directly Down|Up the focus

import Emptiable exposing (Emptiable, filled)
import Stack exposing (onTopLay, topBelow)
import Scroll exposing (Scroll, FocusGap)
import Linear exposing (Direction(..))

Scroll.one "hello"
    |> Scroll.sideAlter Up
        (\_ -> topBelow "scrollable" [ "world" ])
    |> Scroll.toEnd Up
    |> Scroll.to (Down |> Scroll.nearest)
    |> Emptiable.map Scroll.focusFill
--> filled "scrollable"
--: Emptiable (Scroll String FocusGap Never) Possibly

Scroll.empty
    |> Scroll.sideAlter Down
        (\_ -> topBelow "world" [ "scrollable" ])
    |> Scroll.to (Down |> Scroll.nearest)
--> Scroll.one "world"
-->     |> Scroll.sideAlter Down
-->         (\_ -> Stack.one "scrollable")
-->     |> filled
--: Emptiable (Scroll String FocusGap Never) Possibly

Scroll.empty
    |> Scroll.sideAlter Up
        (onTopLay "foo")
    |> Scroll.to (Up |> Scroll.nearest)
--> filled (Scroll.one "foo")
--: Emptiable (Scroll String FocusGap Never) Possibly

nearest =
    \side ->
        Scroll.AtSide side 0

create

empty : Scroll item_ FocusGap Possibly

An empty Scroll on a gap with nothing before and after it. It's the loneliest of all Scrolls

<>
import Emptiable

Scroll.empty |> Scroll.toStack
--> Emptiable.empty

one : element -> Scroll element FocusGap never_

A Scroll with a single focussed item in it, nothing Down and Up it

🍊  ->  <🍊>
import Stack

Scroll.one "wat" |> Scroll.focusFill
--> "wat"

Scroll.one "wat" |> Scroll.toStack
--> Stack.one "wat"

fuzz : Fuzzer item -> Fuzzer (Scroll item FocusGap Possibly)

Scroll item FocusGap Possibly Fuzzer with a given item Fuzzer.

Each side's generated length will be <= 32, The focus is either filled or not

import Linear exposing (Direction(..))
import Stack exposing (topBelow)
import Fuzz

Scroll.fuzz (Fuzz.intRange 0 9) |> Fuzz.examples 2
--> [ Scroll.one 6
-->     |> Scroll.sideAlter Down (\_ -> topBelow 0 [ 1, 6, 0, 5, 1, 7, 8, 8, 0, 5, 4, 9, 4, 9, 3, 7, 0, 3, 6, 4 ])
-->     |> Scroll.sideAlter Up (\_ -> topBelow 5 [ 9 ])
--> , Scroll.empty
-->     |> Scroll.sideAlter Down (\_ -> topBelow 3 [ 9, 7, 5, 0, 3 ])
-->     |> Scroll.sideAlter Up (\_ -> topBelow 8 [ 4, 5, 6, 9, 6, 0, 6, 4, 3, 8, 0, 5, 4 ])
--> ]

To always fuzz the focus as filled → focusFilledFuzz

focusFilledFuzz : Fuzzer item -> Fuzzer (Scroll item FocusGap never_)

Scroll item FocusGap never_ Fuzzer with a given item Fuzzer.

Each side's generated length will be <= 32, The focus is always filled

import Linear exposing (Direction(..))
import Stack exposing (topBelow)
import Fuzz

Scroll.focusFilledFuzz (Fuzz.intRange 0 9)
    |> Fuzz.examples 2
--> [ Scroll.one 5
-->     |> Scroll.sideAlter Down (\_ -> topBelow 6 [ 7, 7, 5, 4, 1, 3, 9, 4, 0, 8, 8, 8, 5, 7, 9, 9 ])
-->     |> Scroll.sideAlter Up (\_ -> topBelow 5 [ 9 ])
--> , Scroll.one 5
-->     |> Scroll.sideAlter Down (\_ -> topBelow 9 [ 9, 6, 4, 4, 9, 8, 0, 5, 1, 2, 2, 5, 9, 9, 3, 0 ])
-->     |> Scroll.sideAlter Up (\_ -> topBelow 2 [ 7, 8 ])
--> ]

To only sometimes fuzz the focus as filled → fuzz

scan

focusFill : Scroll item FocusGap Basics.Never -> item

The focused item

🍍 🍓 <🍊> 🍉 🍇  ->  🍊
import Linear exposing (Direction(..))
import Stack exposing (topBelow)

Scroll.one "hi there" |> Scroll.focusFill
--> "hi there"

Scroll.one 1
    |> Scroll.sideAlter Up
        (\_ -> topBelow 2 [ 3, 4 ])
    |> Scroll.toEnd Up
    |> Scroll.focusFill
--> 4

Short for

import Emptiable

Scroll.focusFill =
    Scroll.focus >> Emptiable.fill

focus : Scroll item FocusGap possiblyOrNever -> Emptiable item possiblyOrNever

The focused item or gap

🍍 🍓 <🍊> 🍉 🍇  ->  🍊
🍍 🍓 <> 🍉 🍇  ->  _
import Emptiable exposing (filled, fill)

Scroll.empty |> Scroll.focus
--> Emptiable.empty

Scroll.one "hi there" |> Scroll.focus |> fill
--> "hi there"

focusFill is short for focus |> fill

side : Linear.Direction -> Scroll item FocusGap possiblyOrNever_ -> Emptiable (Stacked item) Possibly

The stack to one side of the focus

Down

🍍←🍓) <🍊> 🍉 🍇

Up

🍍 🍓 <🍊> (🍉→🍇
import Emptiable exposing (Emptiable)
import Stack exposing (Stacked, topBelow)
import Linear exposing (Direction(..))

Scroll.one 0
    |> Scroll.sideAlter Up
        (\_ -> topBelow 1 [ 2, 3 ])
    |> Scroll.to (Up |> Scroll.nearest)
    |> Emptiable.mapFlat (Scroll.to (Up |> Scroll.nearest))
    |> Emptiable.mapFlat (Scroll.side Down)
--> topBelow 1 [ 0 ]
--: Emptiable (Stacked number_) Possibly

Scroll.one 0
    |> Scroll.sideAlter Up
        (\_ -> topBelow 1 [ 2, 3 ])
    |> Scroll.to (Up |> Scroll.nearest)
    |> Emptiable.mapFlat (Scroll.side Up)
--> topBelow 2 [ 3 ]
--: Emptiable (Stacked number_) Possibly

length : Scroll item_ FocusGap possiblyOrNever_ -> Basics.Int

Counting all contained items

import Stack exposing (topBelow)
import Linear exposing (Direction(..))

Scroll.one 0
    |> Scroll.sideAlter Down
        (\_ -> topBelow -1 [ -2 ])
    |> Scroll.sideAlter Up
        (\_ -> topBelow 1 [ 2, 3 ])
    |> Scroll.length
--> 6

Scroll.empty
    |> Scroll.sideAlter Down
        (\_ -> topBelow -1 [ -2 ])
    |> Scroll.sideAlter Up
        (\_ -> topBelow 1 [ 2, 3 ])
    |> Scroll.length
--> 5

O(n) like Scroll.length

move the focus

to : Location -> Scroll item FocusGap possiblyOrNever_ -> Emptiable (Scroll item FocusGap never_) Possibly

Try to move the focus to the item at a given Scroll.Location

Scroll.to (Down |> Scroll.nearest)

🍊 <🍉> 🍇  ->  <🍊> 🍉 🍇
import Linear exposing (Direction(..))
import Emptiable exposing (Emptiable, filled)
import Scroll exposing (Scroll, FocusGap)

Scroll.empty |> Scroll.to (Down |> Scroll.nearest)
--> Emptiable.empty

Scroll.one "hello"
    |> Scroll.sideAlter Up
        (\_ -> topBelow "scrollable" [ "world" ])
    |> Scroll.toEnd Up
    |> Scroll.to (Down |> Scroll.nearest)
    |> Emptiable.map Scroll.focusFill
--> filled "scrollable"
--: Emptiable (Scroll String FocusGap Never) Possibly

This also works from within gaps:

🍊 🍉 <> 🍇  ->  🍊 <🍉> 🍇
import Linear exposing (Direction(..))
import Emptiable exposing (Emptiable, filled)
import Stack exposing (onTopLay)
import Scroll exposing (Scroll, FocusGap)

Scroll.empty
    |> Scroll.sideAlter Down
        (onTopLay "foo")
    |> Scroll.to (Down |> Scroll.nearest)
--> filled (Scroll.one "foo")
--: Emptiable (Scroll String FocusGap Never) Possibly

Scroll.to (Up |> Scroll.nearest)

<🍊> 🍉 🍇  ->  🍊 <🍉> 🍇
import Linear exposing (Direction(..))
import Emptiable exposing (Emptiable, filled)
import Scroll exposing (Scroll, FocusGap)

Scroll.one 0
    |> Scroll.sideAlter Up
        (\_ -> topBelow 1 [ 2, 3 ])
    |> Scroll.to (Up |> Scroll.nearest)
    |> Emptiable.map Scroll.focusFill
--> filled 1
--: Emptiable number_ Possibly

This also works from within gaps:

🍊 <> 🍉 🍇  ->  🍊 <🍉> 🍇
import Linear exposing (Direction(..))
import Emptiable exposing (Emptiable, filled)
import Stack exposing (onTopLay)
import Scroll exposing (Scroll, FocusGap)

Scroll.empty
    |> Scroll.sideAlter Up
        (\_ -> Stack.one "foo")
    |> Scroll.to (Up |> Scroll.nearest)
--> filled (Scroll.one "foo")
--: Emptiable (Scroll String FocusGap Never) Possibly

If there is no next item, the result is empty

import Linear exposing (Direction(..))
import Emptiable
import Stack exposing (topBelow)

Scroll.empty |> Scroll.to (Up |> Scroll.nearest)
--> Emptiable.empty

Scroll.one 0
    |> Scroll.sideAlter Up
        (\_ -> topBelow 1 [ 2, 3 ])
    |> Scroll.toEnd Up
    |> Scroll.to (Up |> Scroll.nearest)
--> Emptiable.empty

Scroll.to Location

import Element as Ui
import Element.Input as UIn
import Emptiable
import RecordWithoutConstructorFunction exposing (RecordWithoutConstructorFunction)
import Scroll exposing (FocusGap, Scroll)
import Stack

type alias Model =
    RecordWithoutConstructorFunction
        { numbers : Scroll Int FocusGap Never
        }

type Event
    = NumberClicked Scroll.Location

update : Event -> Model -> ( Model, Cmd Event )
update event model =
    case event of
        NumberClicked location ->
            ( { model
                | numbers =
                    model.numbers
                        |> Scroll.to location
                        |> Emptiable.fillElseOnEmpty (\_ -> model.numbers)
                        |> Scroll.focusAlter (Emptiable.map (\n -> n + 1))
              }
            , Cmd.none
            )

interface : Model -> Ui.Element Event
interface =
    \{ numbers } ->
        numbers
            |> Scroll.map numberInterface
            |> Scroll.toList
            |> Ui.column []

numberInterface :
    Scroll.Location
    -> Int
    -> Ui.Element Event
numberInterface location =
    \number ->
        UIn.button []
            { onPress = NumberClicked location |> Just
            , label =
                number
                    |> String.fromInt
                    |> Ui.text
            }

The same functionality is often provided as duplicate : Scroll item ... -> Scroll (Scroll item) ...

Saving a Scroll with every item becomes expensive for long Scrolls, though!

toGap : Linear.Direction -> Scroll item FocusGap Basics.Never -> Scroll item FocusGap Possibly

Move the focus to the gap directly Down|Up. Feel free to plug that gap right up!

Scroll.toGap Down

🍍 <🍊> 🍉  ->  🍍 <> 🍊 🍉
import Linear exposing (Direction(..))
import Emptiable exposing (filled)
import Stack exposing (topBelow)

Scroll.one "world"
    |> Scroll.toGap Down
    |> Scroll.focusAlter (\_ -> "hello" |> filled)
    |> Scroll.toStack
--> topBelow "hello" [ "world" ]

Scroll.toGap Up

🍍 <🍊> 🍉  ->  🍍 🍊 <> 🍉
import Linear exposing (Direction(..))
import Emptiable exposing (filled)
import Stack exposing (topBelow)

Scroll.one "hello"
    |> Scroll.sideAlter Up
        (\_ -> Stack.one "world")
    |> Scroll.toGap Up
    |> Scroll.focusAlter (\_ -> filled "scrollable")
    |> Scroll.toStack
--> topBelow "hello" [ "scrollable", "world" ]

toEnd : Linear.Direction -> Scroll item FocusGap possiblyOrNever -> Scroll item FocusGap possiblyOrNever

Focus the furthest item Down/Up the focus

Down

🍍 🍓 <🍊> 🍉  ->  <🍍> 🍓 🍊 🍉

Up

🍓 <🍊> 🍉 🍇  ->  🍓 🍊 🍉 <🍇>
import Linear exposing (Direction(..))
import Emptiable exposing (Emptiable)
import Stack exposing (Stacked, topBelow)

Scroll.one 1
    |> Scroll.sideAlter Up
        (\_ -> topBelow 2 [ 3, 4 ])
    |> Scroll.sideAlter Down
        (\_ -> topBelow 4 [ 3, 2 ])
    |> Scroll.toEnd Down
    |> Scroll.focusFill
--> 2

Scroll.one 1
    |> Scroll.sideAlter Up
        (\_ -> topBelow 2 [ 3, 4 ])
    |> Scroll.toEnd Up
    |> Scroll.focusFill
--> 4

Scroll.one 1
    |> Scroll.sideAlter Up
        (\_ -> topBelow 2 [ 3, 4 ])
    |> Scroll.toEnd Up
    |> Scroll.side Down
--> topBelow 3 [ 2, 1 ]
--: Emptiable (Stacked number_) Possibly

toEndGap : Linear.Direction -> Scroll item FocusGap possiblyOrNever_ -> Scroll item FocusGap Possibly

Focus the gap beyond the furthest item Down|Up. Remember that gaps surround everything!

Down

🍍 🍓 <🍊> 🍉  ->  <> 🍍 🍓 🍊 🍉

Up

🍓 <🍊> 🍉  ->  🍓 🍊 🍉 <>
import Linear exposing (Direction(..))
import Emptiable exposing (filled)
import Stack exposing (topBelow)

Scroll.one 1
        -- <1>
    |> Scroll.sideAlter Up
        (\_ -> topBelow 3 [ 4 ])
        -- <1> 3 4
    |> Scroll.toGap Up
        -- 1 <> 3 4
    |> Scroll.focusAlter (\_ -> filled 2)
        -- 1 <2> 3 4
    |> Scroll.toEndGap Down
        -- <> 1 2 3 4
    |> Scroll.focusAlter (\_ -> filled 0)
        -- <0> 1 2 3 4
    |> Scroll.toStack
--> topBelow 0 [ 1, 2, 3, 4 ]

Scroll.one 1
        -- <1>
    |> Scroll.sideAlter Up
        (\_ -> topBelow 2 [ 3 ])
        -- <1> 2 3
    |> Scroll.toEndGap Up
        -- 1 2 3 <>
    |> Scroll.focusAlter (\_ -> filled 4)
        -- 1 2 3 <4>
    |> Scroll.toStack
--> topBelow 1 [ 2, 3, 4 ]

toWhen : Linear.Direction -> ({ index : Basics.Int } -> item -> Basics.Bool) -> Scroll item FocusGap possiblyOrNever_ -> Emptiable (Scroll item FocusGap never_) Possibly

Move the focus to the nearest item Down|Up that matches a predicate. If no such item was found, Emptiable.empty

import Linear exposing (Direction(..))
import Emptiable exposing (filled)
import Stack exposing (topBelow)
import Linear exposing (Direction(..))

Scroll.one 4
    |> Scroll.sideAlter Down
        (\_ -> topBelow 2 [ -1, 0, 3 ])
    |> Scroll.toWhen Down (\_ item -> item < 0)
    |> Emptiable.map Scroll.focusFill
--> filled -1

Scroll.one 4
    |> Scroll.sideAlter Up
        (\_ -> topBelow 2 [ -1, 0, 3 ])
    |> Scroll.toWhen Up (\_ item -> item < 0)
    |> Emptiable.map focusFill
--> filled -1

Scroll.one -4
    |> Scroll.sideAlter Up
        (\_ -> topBelow 2 [ -1, 0, 3 ])
    |> Scroll.toWhen Up (\_ item -> item < 0)
    |> Emptiable.map focusFill
--> filled -4

dragFocus : Linear.Direction -> Scroll item FocusGap possiblyOrNever -> Emptiable (Scroll item FocusGap possiblyOrNever) Possibly

Try to move the focus to the nearest item Down|Up

Down

🍊 🍉 <🍓> 🍇  ->  🍊 <🍓> 🍉 🍇
🍊 🍉 <> 🍇  ->  🍊 <> 🍉 🍇

Up

🍊 <🍓> 🍉 🍇  ->  🍊 🍉 <🍓> 🍇
🍊 <> 🍉 🍇  ->  🍊 🍉 <> 🍇
import Linear exposing (Direction(..))
import Emptiable exposing (Emptiable)
import Scroll exposing (Scroll, FocusGap)

Scroll.one 0
    |> Scroll.sideAlter Down
        (\_ -> topBelow 1 [ 2, 3 ])
    |> Scroll.dragFocus Down
    |> Emptiable.mapFlat Scroll.toStack
--> topBelow 3 [ 2, 0, 1 ]
--: Emptiable (Stacked number_) Possibly

Scroll.one 0
    |> Scroll.sideAlter Up
        (\_ -> topBelow 1 [ 2, 3 ])
    |> Scroll.dragFocus Up
    |> Emptiable.mapFlat Scroll.toStack
--> topBelow 1 [ 0, 2, 3 ]
--: Emptiable (Stacked number_) Possibly

If there is no nearest item, the result is empty

import Linear exposing (Direction(..))
import Emptiable
import Stack exposing (topBelow)

Scroll.one 0
    |> Scroll.sideAlter Up
        (\_ -> topBelow 1 [ 2, 3 ])
    |> Scroll.dragFocus Down
--> Emptiable.empty

Scroll.one 0
    |> Scroll.sideAlter Down
        (\_ -> topBelow 1 [ 2, 3 ])
    |> Scroll.dragFocus Up
--> Emptiable.empty

alter

mirror : Scroll item FocusGap possiblyOrNever -> Scroll item FocusGap possiblyOrNever

Swap the stack on the side Down the focus with the stack on the side Up

🍓 <🍊> 🍉 🍇  <->  🍇 🍉 <🍊> 🍓
import Linear exposing (Direction(..))
import Stack exposing (topBelow)

Scroll.one 1
    |> Scroll.sideAlter Up
        (\_ -> topBelow 2 [ 3, 4 ])
    |> Scroll.sideAlter Down
        (\_ -> topBelow 4 [ 3, 2 ])
    |> Scroll.mirror
--> Scroll.one 1
-->     |> Scroll.sideAlter Down
-->         (\_ -> topBelow 2 [ 3, 4 ])
-->     |> Scroll.sideAlter Up
-->         (\_ -> topBelow 4 [ 3, 2 ])

In contrast to List or stack, runtime is O(1)

focusAlter : (Emptiable item possiblyOrNever -> Emptiable item alteredPossiblyOrNever) -> Scroll item FocusGap possiblyOrNever -> Scroll item FocusGap alteredPossiblyOrNever

Alter the focus – item or gap – based on its current value

Scroll.focusAlter (\_ -> 🍊 |> filled)

🍊  ->  🍓 <?> 🍉  ->  🍓 <🍊> 🍉
import Linear exposing (Direction(..))
import Emptiable exposing (filled)
import Stack exposing (topBelow, onTopLay)

Scroll.empty
        -- <>
    |> Scroll.sideAlter Down
        (onTopLay "🍓")
        -- "🍓" <>
    |> Scroll.sideAlter Up
        (onTopLay "🍉")
        -- "🍓" <> "🍉"
    |> Scroll.focusAlter (\_ -> "🍊" |> filled)
        -- "🍓" <"🍊"> "🍉"
    |> Scroll.toStack
--> topBelow "🍓" [ "🍊", "🍉" ]

Scroll.focusAlter (\_ -> Emptiable.empty)

🍓 <?> 🍉  ->  🍓 <> 🍉
import Linear exposing (Direction(..))
import Emptiable exposing (filled)
import Stack exposing (topBelow)

Scroll.one "hello"
    |> Scroll.sideAlter Up
        (\_ -> topBelow "scrollable" [ "world" ])
    |> Scroll.to (Up |> Scroll.nearest)
    |> Emptiable.map (Scroll.focusAlter (\_ -> Emptiable.empty))
    |> Emptiable.map Scroll.toList
--> filled [ "hello", "world" ]

Scroll.focusAlter (?🍒 -> ?🍊)

(?🍒 -> ?🍊)  ->  🍓 <?🍒> 🍉  ->  🍓 <?🍊> 🍉
import Linear exposing (Direction(..))
import Emptiable exposing (filled)
import Stack exposing (topBelow, onTopLay)

Scroll.empty
        -- <>
    |> Scroll.sideAlter Down
        (onTopLay "🍓")
        -- "🍓" <>
    |> Scroll.sideAlter Up
        (onTopLay "🍉")
        -- "🍓" <> "🍉"
    |> Scroll.focusAlter
        (\_ -> filled "🍊")
        -- "🍓" <"🍊"> "🍉"
    |> Scroll.toStack
--> topBelow "🍓" [ "🍊", "🍉" ]

Scroll.one "first"
    |> Scroll.sideAlter Down
        (\_ -> Stack.one "zeroth")
    |> Scroll.sideAlter Up
        (\_ -> topBelow "second" [ "third" ])
    |> Scroll.focusAlter (Emptiable.map String.toUpper)
    |> Scroll.toStack
--> topBelow "zeroth" [ "FIRST", "second", "third" ]

sideAlter : Linear.Direction -> (Emptiable (Stacked item) Possibly -> Emptiable (Stacked item) possiblyOrNever_) -> Scroll item FocusGap possiblyOrNever -> Scroll item FocusGap possiblyOrNever

Look Down|Up the focus and operate directly an the stack you see

sideAlter Linear.Direction (\_ -> 🍒🍋)

Down

🍓 <🍊> 🍉
      ↓
🍋 🍒 <🍊> 🍉

Up

🍍 🍓 <🍊> 🍉
      ↓
🍍 🍓 <🍊> 🍒 🍋
import Linear exposing (Direction(..))
import Emptiable
import Stack exposing (topBelow)

Scroll.one "selectoo"
    |> Scroll.sideAlter Down
        (\_ -> topBelow "earlee" [ "agua", "enutai" ])
    |> Scroll.sideAlter Up
        (\_ -> topBelow "orangloo" [ "iquipy", "oice" ])
    |> Scroll.sideAlter Up
        (\_ -> Emptiable.empty)
    |> Scroll.toStack
--> topBelow "enutai" [ "agua", "earlee", "selectoo" ]

sideAlter Linear.Direction (Stack.map ...)

import Linear exposing (Direction(..))
import Stack exposing (topBelow)

Scroll.one "second"
    |> Scroll.sideAlter Down
        (\_ -> topBelow "first" [ "zeroth" ])
    |> Scroll.sideAlter Down
        (Stack.map (\_ -> String.toUpper))
    |> Scroll.toStack
--> topBelow "ZEROTH" [ "FIRST", "second" ]

Scroll.one "zeroth"
    |> Scroll.sideAlter Up
        (\_ -> topBelow "first" [ "second" ])
    |> Scroll.sideAlter Up
        (Stack.map (\_ -> String.toUpper))
    |> Scroll.toStack
--> topBelow "zeroth" [ "FIRST", "SECOND" ]

Look to one side from the focus and slide items in directly at the nearest location

sideAlter Linear.Direction (Stack.attach 🍒🍋)

Down

      🍒🍋
🍍 🍓 \↓/ <🍊> 🍉

Up

        🍒🍋
🍓 <🍊> \↓/ 🍉 🍇
import Linear exposing (Direction(..))
import Stack exposing (topBelow)

Scroll.one 0
    |> Scroll.sideAlter Down
        (Stack.attach Down ([ -4, -5 ] |> Stack.fromList))
    |> Scroll.sideAlter Down
        (Stack.attach Down (topBelow -1 [ -2, -3 ]))
    |> Scroll.toStack
--> topBelow -5 [ -4, -3, -2, -1, 0 ]

Scroll.one 0
    |> Scroll.sideAlter Up
        (Stack.attach Down ([ 4, 5 ] |> Stack.fromList))
    |> Scroll.sideAlter Up
        (Stack.attach Down (topBelow 1 [ 2, 3 ]))
    |> Scroll.toStack
--> topBelow 0 [ 1, 2, 3, 4, 5 ]

Scroll.sideAlter Linear.Direction (Stack.attach Up 🍒🍋)

Down

🍋🍒
 \↓ 🍍 🍓 <🍊> 🍉

Up

              🍒🍋
🍓 <🍊> 🍉 🍇 ↓/
import Linear exposing (Direction(..))
import Stack exposing (topBelow)

Scroll.one 1
    |> Scroll.sideAlter Up
        (Stack.attach Up (topBelow 2 [ 3, 4 ]))
    |> Scroll.toEnd Up
    |> Scroll.sideAlter Down
        (Stack.attach Up (topBelow 7 [ 6, 5 ]))
    |> Scroll.toStack
--> topBelow 5 [ 6, 7, 1, 2, 3, 4 ]

Scroll.one 123
    |> Scroll.sideAlter Up
        (Stack.attach Up (Stack.one 456))
    |> Scroll.sideAlter Up
        (Stack.attach Up (topBelow 789 [ 0 ]))
    |> Scroll.toStack
--> topBelow 123 [ 456, 789, 0 ]

sideAlter Linear.Direction (Stack.onTopLay ...)

Down

      🍒
🍍 🍓 ↓ <🍊> 🍉

Up

        🍒
🍓 <🍊> ↓ 🍉 🍇
import Linear exposing (Direction(..))
import Stack exposing (topBelow, onTopLay)

Scroll.one 123
    |> Scroll.sideAlter Down
        (onTopLay 456)
    |> Scroll.toStack
--> topBelow 456 [ 123 ]

Scroll.one 123
    |> Scroll.sideAlter Up
        (\_ -> Stack.one 789)
    |> Scroll.sideAlter Up
        (onTopLay 456)
    |> Scroll.toStack
--> topBelow 123 [ 456, 789 ]

map : (Location -> item -> mappedItem) -> Scroll item FocusGap possiblyOrNever -> Scroll mappedItem FocusGap possiblyOrNever

Change every item based on its current value

import Linear exposing (Direction(..))
import Stack exposing (topBelow)

Scroll.one "first"
    |> Scroll.sideAlter Down
        (\_ -> Stack.one "zeroth")
    |> Scroll.sideAlter Up
        (\_ -> topBelow "second" [ "third" ])
    |> Scroll.map (\_ -> String.toUpper)
    |> Scroll.toStack
--> topBelow "ZEROTH" [ "FIRST", "SECOND", "THIRD" ]

focusSidesMap allows changing the individual parts separately

focusSidesMap : { focus : Emptiable item possiblyOrNever -> Emptiable mappedItem possiblyOrNeverMapped, side : Linear.Direction -> Emptiable (Stacked item) Possibly -> Emptiable (Stacked mappedItem) possiblyOrNeverMappedBefore_ } -> Scroll item FocusGap possiblyOrNever -> Scroll mappedItem FocusGap possiblyOrNeverMapped

Change the focus, the sides Down and Up using different functions

import Linear exposing (Direction(..))
import Emptiable exposing (filled)
import Stack exposing (topBelow)

Scroll.one "first"
    |> Scroll.sideAlter Up
        (\_ -> Stack.one "second")
    |> Scroll.toGap Up
    |> Scroll.focusAlter (\_ -> filled "one-and-a-halfth")
    |> Scroll.focusSidesMap
        { side =
            \side ->
                Stack.map
                    (\_ item ->
                        String.concat
                            [ side |> sideToString, ": ", item ]
                    )
        , focus =
            map (\item -> "focused item: " ++ item)
        }
    |> Scroll.toStack
--→
topBelow
    "before: first"
    [ "focused item: one-and-a-halfth"
    , "after: second"
    ]

sideToString =
    \side ->
        case side of
            Down ->
                "before"

            Up ->
                "after"

map transforms every item

focusFilled : Scroll item FocusGap possiblyOrNever -> Emptiable (Scroll item FocusGap never_) possiblyOrNever

If the current focussed thing is a gap, Emptiable.empty. If it's an item, Emptiable.filled

import Emptiable
import Stack exposing (topBelow)
import Linear exposing (Direction(..))

Scroll.one 3
    |> Scroll.sideAlter Up
        (\_ -> topBelow 2 [ 1 ])
    |> Scroll.toGap Up
    |> Scroll.focusFilled
--> Emptiable.empty

transform

foldFrom : accumulationValue -> Linear.Direction -> (item -> accumulationValue -> accumulationValue) -> Scroll item FocusGap possiblyOrNever_ -> accumulationValue

Reduce in a direction

import Linear exposing (Direction(..))
import Stack exposing (topBelow)

Scroll.one 'i'
    |> Scroll.sideAlter Down
        (\_ -> topBelow 'v' [ 'e' ])
    |> Scroll.sideAlter Up
        (\_ -> Stack.one 'l')
    |> Scroll.foldFrom "" Down String.cons
--> "evil"

Scroll.one 'v'
    |> Scroll.sideAlter Down
        (\_ -> Stack.one 'e')
    |> Scroll.sideAlter Up
        (\_ -> topBelow  'i' [ 'l' ])
    |> Scroll.foldFrom "" Up String.cons
--> "live"

Scroll.empty
    |> Scroll.sideAlter Down
        (\_ -> topBelow 'v' [ 'e' ])
    |> Scroll.sideAlter Up
        (\_ -> topBelow  'i' [ 'l' ])
    |> Scroll.foldFrom "" Up String.cons
--> "live"

fold : Linear.Direction -> (item -> item -> item) -> Scroll item FocusGap Basics.Never -> item

Fold, starting from one end item as the initial accumulation value, reducing in a given Direction

import Linear exposing (Direction(..))
import Stack exposing (topBelow)

Scroll.one 234
    |> Scroll.sideAlter Up
        (\_ -> topBelow 345 [ 543 ])
    |> Scroll.fold Up max
--> 543

To accumulate into anything that doesn't have the same type as the [Scroll]s items, foldFromOne

foldFromOne : (item -> accumulated) -> Linear.Direction -> (item -> accumulated -> accumulated) -> Scroll item FocusGap Basics.Never -> accumulated

Fold, starting from one end item transformed to the initial accumulation value, reducing in a given Direction

import Linear exposing (Direction(..))
import Stack exposing (topBelow)

Scroll.one 234
    |> Scroll.sideAlter Up
        (\_ -> topBelow 345 [ 543 ])
    |> Scroll.foldFromOne Stack.one Down Stack.onTopLay
--> topBelow 234 [ 345, 543 ]

A simpler version is

Scroll.fold =
    Scroll.foldFromOne identity

toStack : Scroll item FocusGap possiblyOrNever -> Emptiable (Stacked item) possiblyOrNever

Roll out the Scroll to both ends into a Stack

import Linear exposing (Direction(..))
import Emptiable exposing (filled)
import Stack exposing (topBelow)

Scroll.empty
    |> Scroll.toStack
--> Emptiable.empty

Scroll.one 123
    |> Scroll.sideAlter Up
        (\_ -> Stack.one 789)
    |> Scroll.toGap Up
    |> Scroll.focusAlter (\_-> filled 456)
    |> Scroll.toStack
--> topBelow 123 [ 456, 789 ]

its type information gets carried over, so

Scroll.FocusGap Never -> Emptiable Never
Scroll.FocusGap Possibly -> Emptiable Possibly

toList : Scroll item FocusGap possiblyOrNever_ -> List item

Converts it to a List, rolled out to both ends:

import Linear exposing (Direction(..))
import Stack

Scroll.one 456
    |> Scroll.sideAlter Down
        (\_ -> Stack.one 123)
    |> Scroll.sideAlter Up
        (\_ -> Stack.one 789)
    |> Scroll.toList
--> [ 123, 456, 789 ]

Only use this if you need a list in the end. Otherwise, use toStack to preserve some information about its length

type-level

focusGapAdapt : (possiblyOrNever -> possiblyOrNeverAdapted) -> Scroll item FocusGap possiblyOrNever -> Scroll item FocusGap possiblyOrNeverAdapted

Change the possiblyOrNever type

Please read more at Emptiable.emptyAdapt