Search code examples
haskellthreepenny-gui

Changes in other elements based on listbox selections in threepenny-gui


So I have a simple example layout with a listbox, a button and a textarea, where clicking the button changes the text in the textarea:

import Control.Applicative
import Control.Monad
import Data.Maybe

import qualified Graphics.UI.Threepenny as UI
import Graphics.UI.Threepenny.Core

main :: IO ()
main = startGUI defaultConfig setup

setup :: Window -> UI ()
setup w = do
    return w # set UI.title "Simple example"

    listBox     <- UI.listBox   (pure ["First", "Second"]) (pure Nothing) ((UI.string .) <$> (pure id))
    button      <- UI.button    # set UI.text "Button"
    display     <- UI.textarea  # set UI.text "Initial value"

    element listBox # set (attr "size") "10"    

    getBody w   #+ [element listBox, element button, element display]

    on UI.click button $ const $ do
        element display # set UI.text "new text"

What I wanted to do is have the change be dependent on the listbox selection (for example have the "new text" be "First" or "Second" based on the selection).

I can quite easily get the selection by combining userSelection and facts as

facts . userSelection :: ListBox a -> Behavior (Maybe a)

but because setting the value for the textarea is done with

set text :: String -> UI Element -> UI Element

I don't know how to work around the fact that the selection is a Behavior.

All this seems a bit unidiomatic to me and I was wondering what would be the correct way to do this. Maybe I should do something already when the listbox selection is done or changed and not only when the button is pressed.


Solution

  • First of all, there was a regression which affected the code here. That issue is now solved. Threepenny 0.6.0.3 has a temporary fix, and the definitive one will be included in the release after that.


    The code in the pastebin you provided is almost correct. The only needed change is that you don't need to use sink within the button click callback - in your case, sink should establish a permanent connection between a behavior and the content of the text area, with the behavior value changing in response to the button click events.

    For the sake of completeness, here is a full solution:

    {-# LANGUAGE RecursiveDo #-}
    module Main where
    
    import Control.Applicative
    import Control.Monad
    import Data.Maybe
    
    import qualified Graphics.UI.Threepenny as UI
    import Graphics.UI.Threepenny.Core
    
    main :: IO ()
    main = startGUI defaultConfig setup
    
    setup :: Window -> UI ()
    setup w = void $ mdo
        return w # set UI.title "Simple example"
    
        listBox <- UI.listBox
            (pure ["First", "Second"]) bSelected (pure $ UI.string)
        button  <- UI.button # set UI.text "Button"
        display <- UI.textarea
    
        element listBox # set (attr "size") "10"
    
        getBody w #+ [element listBox, element button, element display]
    
        bSelected <- stepper Nothing $ rumors (UI.userSelection listBox)
        let eClick = UI.click button
            eValue = fromMaybe "No selection" <$> bSelected <@ eClick
        bValue    <- stepper "Initial value" eValue
    
        element display # sink UI.text bValue
    

    The two key things to take away are:

    • The Behavior (Maybe a) argument to listBox does not set just the initial selected value, but determines the evolution of the value throughout the lifetime of the application. In this example, facts $ UI.userSelection listBox is just bSelected, as can be verified through the source code of the Widgets module.
    • The typical way to sample a behavior on event occurrences is through (<@) (or <@> if the event carries data you wish to make use of).