Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Draft] Custom attributes #76

Open
wants to merge 21 commits into
base: master
Choose a base branch
from

Conversation

Dretch
Copy link
Collaborator

@Dretch Dretch commented Feb 6, 2020

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.

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 returns IO state -- which is equivalent to Modify). This could be changed easily enough, I think, though.

I would appreciate any thoughts... or just close/ignore it 😄

@Dretch Dretch requested a review from owickstrom February 6, 2020 11:15
@Dretch
Copy link
Collaborator Author

Dretch commented Feb 24, 2020

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.

@Dretch Dretch force-pushed the custom-attributes branch from 09a735a to 06ac8f1 Compare April 18, 2020 14:52
@Dretch Dretch marked this pull request as draft April 18, 2020 14:57
@Dretch Dretch force-pushed the custom-attributes branch from 06ac8f1 to 4433412 Compare May 2, 2020 08:39
Plus a function to set the default icon.
@Dretch Dretch added enhancement New feature or request question Further information is requested labels Jul 19, 2020
@Dretch Dretch self-assigned this Jul 19, 2020
@Dretch Dretch marked this pull request as ready for review July 19, 2020 10:27
@Dretch Dretch requested a review from cblp July 19, 2020 10:29
@cblp cblp removed their request for review July 19, 2020 11:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request question Further information is requested
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant