gi-gtk-declarative
gi-gtk-declarative copied to clipboard
[Draft] Custom attributes
Firstly, I am aware this is all way off-piste, completely unsolicited, and probably not where you wanted to go, but I had some time and wanted to experiment...
So as I mentioned in #75 I had an idea for "custom attributes". These are attributes that can be attached to widgets in the tree, and can have arbitrary internal state and patching behaviour. So they are a bit like custom widgets, except that they don't require any new widgets to be added to the tree and that they can be composed more easily (you can add multiple custom attributes to your existing widget).
In some sense custom attributes are like a more flexible afterCreated
attribute -- they can add custom behaviour on creation, but also on patching and destruction (and can emit events).
In this branch they are used to:
- Implement new top-level windows
- Implement "presenting"/focusing of those windows (with a separate custom attribute attached to the window) -- see the
Windows
example. - Show custom icons on particular top-level windows.
- Replace
CustomWidget
Examples
Custom attribute to add a new top-level window
-- | Construct a new declarative top-level window, with a lifecycle
-- tied to the widget the attribute is attached to.
--
-- The key should uniquely identify this window amongst all windows
-- attached to the same widget.
window
:: (Typeable key, Eq key, Hashable key)
=> key
-> Bin Gtk.Window event
-> Attribute widget event
window key bin' = customAttribute key (Window bin')
-- | Each custom attribute generally needs a new data type to represent the
-- declarative data for the attribute
newtype Window event = Window (Bin Gtk.Window event)
deriving (Functor)
-- | This is where we define how a custom attribute is created/patched/destroyed.
--
-- This also defines where the custom attribute can be used. The `widget` parameter
-- is left unconstrained here, so this custom attribute can be attached to any kind of widget.
instance CustomAttribute widget Window where
-- | Each custom attribute has an associated type that holds the runtime state for
-- the widget. This is separate to the declarative state.
--
-- In this case the runtime state consists of a `SomeState`, which is essentially
-- the runtime state for the top-level window.
data AttrState Window = WindowState SomeState
-- | This is called by the framework when a custom attribute is first used - i.e. when
-- the declarative state does not have an corresponding runtime state
-- (i.e. `AttrState Window` value) from a previous render.
attrCreate _widget (Window bin') =
WindowState <$> create bin' -- just call the create method on the top-level window
-- | This is called by the framework when a custom attribute is used and it already has
-- a runtime state. This receives the Gtk widget value, the previous runtime state, and
-- the old and new declarative states.
attrPatch _widget (WindowState state1) (Window bin1) (Window bin2) =
case patch state1 bin1 bin2 of -- | Update the underlaying top-level window
Keep -> pure $ WindowState state1 -- | The top-level window widget state has not changed,
-- so we don't need to return a new runtime state
Modify p -> WindowState <$> p -- | The top-level window widget state has changed, so
-- we update our state too.
Replace p -> do -- | The top-level window widget has been completely
-- replaced, so we need to destroy the old runtime state.
destroy state1 bin1
WindowState <$> p
-- | This is called by the framework when a custom attribute is not used any more. I.e. when a
-- declarative state appears in one render, but then no longer appears in the next render.
attrDestroy _widget (WindowState state) (Window bin') =
destroy state bin'
-- | This is called by the framework after each render to attach event listeners.
attrSubscribe _widget (WindowState state) (Window bin') cb =
subscribe bin' state cb -- | Just let the the underlying window attach it's event listeners.
Custom attribute to add a custom icon to a window
-- | Set the icon that is used by one particular window.
windowIcon :: IconData -> Attribute Gtk.Window event
windowIcon = customAttribute () . WindowIcon -- () here means that there can only be a single icon per window
-- | The declarative state just holds the icon data
newtype WindowIcon event = WindowIcon IconData
deriving (Functor)
-- | Note that the first type parameter is `Gtk.Window`, so we can only add icons to `Gtk.Window` widgets,
-- not to any kind of widget.
instance CustomAttribute Gtk.Window WindowIcon where
-- | We don't need any runtime state, so this is equivalent to the () type
data AttrState WindowIcon = WindowIconState
-- | When an declarative icon first appears then we attach it to the window.
attrCreate window' (WindowIcon dat) = do
pixbuf <- loadPixbuf dat
Gtk.windowSetIcon window' (Just pixbuf)
pure WindowIconState
-- | When a declarative icon appears in two consecutive renders, then we might need to patch the widget
-- to update the icon.
attrPatch window' state (WindowIcon old) (WindowIcon new) = do
when (old /= new) $ do
pixbuf <- loadPixbuf new
Gtk.windowSetIcon window' (Just pixbuf)
pure state
-- We use the default implementations of `attrDestroy` and `attrSubscribe` (which do nothing), since
-- we don't need to free resources on destruction, and an icon can't emit any events.
data IconData
= IconDataBytes ByteString
-- ^ Icon data in an image format supported by GdkPixbuf (e.g. PNG, JPG)
deriving (Eq)
loadPixbuf :: IconData -> IO Pixbuf.Pixbuf
loadPixbuf (IconDataBytes bs) = do
loader <- Pixbuf.pixbufLoaderNew
Pixbuf.pixbufLoaderWrite loader bs
Pixbuf.pixbufLoaderClose loader
Pixbuf.pixbufLoaderGetPixbuf loader >>= \case
Nothing -> error "Failed loading icon into pixbuf."
Just pixbuf -> pure pixbuf
Custom attributes to implement custom widgets
Custom attributes completely replace custom widgets, since they allow you to run arbitrary create/patch/destroy code.
- An easy-to-use ComboBox to allow choosing between different text strings: https://github.com/Dretch/foundationdb-explorer/blob/3f3f2c1b0107aef5a5346df1cc597f48b94392ac/src/FDBE/Widget/ComboBoxText.hs#L28
- A spinner to allow choosing integer values: https://github.com/Dretch/foundationdb-explorer/blob/3f3f2c1b0107aef5a5346df1cc597f48b94392ac/src/FDBE/Widget/IntegerSpinner.hs#L29
- A multi-line text editor: https://github.com/Dretch/foundationdb-explorer/blob/3f3f2c1b0107aef5a5346df1cc597f48b94392ac/src/FDBE/Widget/TextView.hs#L30
Downsides
Apart from the lack of tests and docs, there are a couple of downsides to this design:
- It is possible for two custom attributes to fight over the Gtk state in the widget that they are attached to, if they happen to read/write the same bits of widget state. I'm not sure what can be done about this really.
- The custom attribute patching does not support the
Keep
/Modify
/Replace
optimisation (it always returnsIO state
-- which is equivalent toModify
). This could be changed easily enough, I think, though.
I would appreciate any thoughts... or just close/ignore it :smile:
~~So I have discovered one problem with having extra windows implemented like this.~~
~~Windows really suffer from the simple diff algorithm - that does not have any kind of "key" to identify declarative widgets across updates. Each window has a unique size and position on the screen, and these need to be match up with the content. Without some kind of key, if you remove a custom-window-attribute that is not the last custom-window-attribute on that widget, then the content of any following windows will be re-parented into the (nth-minus-1) window. This looks very odd to the user.~~
~~I will have a think about ways around this problem.~~
The current code on this branch avoids this problem by allowing custom attributes to be declared with a unique identifier that the framework tracks across consecutive renders.