KEMBAR78
GUI: Don't redraw whole dialog in Widget::setEnabled() by mikrosk · Pull Request #4962 · scummvm/scummvm · GitHub
Skip to content

Conversation

@mikrosk
Copy link
Contributor

@mikrosk mikrosk commented May 1, 2023

There are three commits presented but the second one, despite its smallness, is the most interesting one. I spent several hours on this and even now I'm not 100% sure whether I got the theme code right.

You might ask why I didn't change also kDDCHeckbox*, kDDRadiobutton* and kDDPopUp* as those also contain the idle/disabled pairs. The embarrassing answer is that a) it doesn't work (sudden flashing, missing parts of the widgets etc) and b) I didn't understand its code. So either:

  • my analysis below is wrong
  • those widgets do something special, maybe even something which are not supposed to do
  • the buttons should do something special and they don't so that's why my change is working here

So I left those as they were and intensively tested the button and dropdown button, as those were the problematic ones in the launcher. If it ain't broke, don't fix it, right? Of course, I also played with all the dialogs so see any regressions. If you can think of a problematic scenario (involving having a button / widget which can get enabled/disabled in real time without Apply / opening a new dialog etc = without forcing a full redraw), please do let me know.

Content of the commit message for better visibility:

Until now, every (even the tiniest) widget's change of the enabled state
resulted in a redraw of a complete dialog. Why this was needed and how
it has been fixed requires a bit of explanation.

There are two buffers to render the widgets in: Backbuffer and Screen
(selected by ThemeEngine::drawToBackbuffer() and
ThemeEngine::drawToScreen() respectively, setting
VectorRenderer::_activeSurface). Then there are two layers/flags:
kDrawLayerBackground and kDrawLayerForeground
(selected in Dialog::drawDialog(), setting ThemeEngine::_layerToDraw).

When asked for a complete dialog rebuild in GuiManager::redraw()
(kRedrawCloseDialog, kRedrawFull) the widgets of every dialog,
regardless of their layer, are drawn into Backbuffer and then copied
into Screen.

When asked for a partial dialog rebuild (kRedrawOpenDialog,
kRedrawTopDialog) the widgets of the topmost dialog marked with
kDrawLayerBackground are drawn into Backbuffer, then copied into Screen
and finally the widgets marked with kDrawLayerForeground are drawn into
Screen only.

When redraw() is called just within the GuiManager's event loop the
widgets of the topmost dialog are drawn into Screen only. And this is
where the layers become important.

When rebuilding the dialog, it doesn't really matter which layer has
been defined for a widget: Backbuffer contains the ones with
kDrawLayerBackground and Screen will supply the rest with
kDrawLayerForeground, if needed. But which layer is taken into account
when calling Dialog::drawWidgets() ?

It is important to realize that the content of Backbuffer is
defined by the widget's initial state (idle or disabled): so Backbuffer
will contain either "idle color" or "disabled color" after dialog's
creation.

ThemeEngine::drawDD() does two checks:

  1. if widget has kDrawLayerBackground set and _activeSurface is Screen, copy the widget from Backbuffer to Screen

  2. if widget's layer is the same as _layerToDraw, draw the widget into _activeSurface

This is what happens in redraw(kRedrawDisabled) for kDrawLayerBackground widgets:

  • Backbuffer contains an idle/disabled (depending on its initial state) rendition of the widget
  • widget is copied from Backbuffer to Screen (1st check in drawDD())
  • widget is not drawn into Screen as _layerToDraw is kDrawLayerForeground from the last redraw() (2nd check in drawDD())

Summary: when switching between idle/disabled state, widget's color is not updated.

This is what happens in redraw(kRedrawDisabled) for kDrawLayerForeground widgets:

  • Backbuffer contains an idle/disabled (depending on its initial state) rendition of the widget
  • widget is not copied from Backbuffer to Screen as widget has kDrawLayerForeground set (1st check in drawDD())
  • widget is drawn into Screen as _layerToDraw is kDrawLayerForeground from the last redraw() (2nd check in drawDD())

Summary: when switching between idle/disabled state, widget's color is correctly updated and not restored from Backbuffer.

This is why hovering didn't require scheduleTopDialogRedraw():

  • Backbuffer contains an idle [kDrawLayerBackground] rendition of the widget
  • when highlighted [kDrawLayerForeground], widget is not copied from Backbuffer to Screen
  • widget is drawn into Screen

Unhovering:

  • Backbuffer contains an idle [kDrawLayerBackground] rendition of the widget
  • when idle [kDrawLayerBackground], widget is copied from Backbuffer to Screen
  • widget is not drawn into Screen

The key difference here is that highlighting can't be stored as initial
state. If every widget had to be enabled in dialog redraw, only the
"disabled" widgets would had to be set to kDrawLayerForeground.

Copy link
Member

@lephilousophe lephilousophe left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This part of the codebase is really complex and needs a big effort to deep dive into.
I think there are interesting ideas here but some changes seem incomplete and don't cover all the cases.
Currently, I suspect merging this would lead to difficult to diagnose bugs.

// performance hog.
if (_redrawStatus == kRedrawOpenDialog && _dialogStack.size() > 2)
// Also, if redrawing only the top dialog, don't apply shading at all.
if ((_redrawStatus == kRedrawOpenDialog && _dialogStack.size() > 2) || _redrawStatus == kRedrawTopDialog)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't find this move more readable: we loose track about when to not shade when reading afterwards.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The main motivation behind this and "default buffer" and clearAll changes is that I was losing track what is set where and why.

So for instance we have a None set for open dialog and stack size > 2 but also somewhere deep in the switch/case we skip shading completely. Then "None" is also a valid value you can get from the ".Shading" XML node, i.e. you may be skipping even the "None" case there. So I wanted to have all None sources at one place otherwise it made exactly the opposite impression that if if (_redrawStatus != kRedrawTopDialog) is true, we always apply some shading.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See my proposal in lephilousophe@rework-redraw
I moved all the conditions when applying shading instead.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As far as I am concerned - 👍 , it's really a nice refactoring. Just one note: shading = (ThemeEngine::ShadingStyle)xmlEval()->getVar("Dialog." + _dialogStack.top()->_name + ".Shading", 0); can also lead to kShadingNone (typically - tooltips) so you can still get your complex condition being true and yet apply only an empty shading.

For that reason I'm still inclined to go with the "set in the beginning and always apply" path but that's just a purist's argument, readability is good in both cases.

@mikrosk mikrosk force-pushed the gui_opts branch 2 times, most recently from d1ebc2c to bda5eda Compare May 7, 2023 19:38
Copy link
Contributor Author

@mikrosk mikrosk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@lephilousophe rebased on master and applied your suggestion to make the disabled stated a child of idle.

Also added an explanation for the removal of markAsDirty calls.

There's a couple of threads to discuss left, so I'll wait for your feedback there.

// performance hog.
if (_redrawStatus == kRedrawOpenDialog && _dialogStack.size() > 2)
// Also, if redrawing only the top dialog, don't apply shading at all.
if ((_redrawStatus == kRedrawOpenDialog && _dialogStack.size() > 2) || _redrawStatus == kRedrawTopDialog)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The main motivation behind this and "default buffer" and clearAll changes is that I was losing track what is set where and why.

So for instance we have a None set for open dialog and stack size > 2 but also somewhere deep in the switch/case we skip shading completely. Then "None" is also a valid value you can get from the ".Shading" XML node, i.e. you may be skipping even the "None" case there. So I wanted to have all None sources at one place otherwise it made exactly the opposite impression that if if (_redrawStatus != kRedrawTopDialog) is true, we always apply some shading.

Copy link
Contributor Author

@mikrosk mikrosk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@lephilousophe See my comment about kShadingNone. Apart from that, I'm all in favour to merge the refactored redraw and rebase my PR on top of it.

@lephilousophe
Copy link
Member

lephilousophe commented May 10, 2023

I just rebased your PR on my commit all of it inside your PR. :)
I still think it's better to accept a call to applyScreenShading with kShadingNone.
The important part here is that it comes from settings so it's not a behaviour we want to control.

@mikrosk mikrosk force-pushed the gui_opts branch 2 times, most recently from 2a466bc to 6fff169 Compare May 10, 2023 20:51
@mikrosk mikrosk merged commit 6fff169 into scummvm:master May 10, 2023
lephilousophe and others added 3 commits May 10, 2023 22:52
When calling any of:

- Widget::setEnabled
- EditTextWidget::setEditString
- EditableWidget::setEditString
- StaticTextWidget::setLabel

there is no need to call neither GuiManager::scheduleTopDialogRedraw nor
Widget::markAsDirty afterwards -- they set up the call by themselves.

Also, refactor a couple of code blocks into calling just
GuiManager::redrawFull as it does the same thing.
Until now, every (even the tiniest) widget's change of the enabled state
resulted in a redraw of a complete *dialog*. Why this was needed and how
it has been fixed requires a bit of explanation.

There are two buffers to render the widgets in: Backbuffer and Screen
(selected by ThemeEngine::drawToBackbuffer() and
ThemeEngine::drawToScreen() respectively, setting
VectorRenderer::_activeSurface). Then there are two layers/flags:
kDrawLayerBackground and kDrawLayerForeground
(selected in Dialog::drawDialog(), setting ThemeEngine::_layerToDraw).

When asked for a complete dialog rebuild in GuiManager::redraw()
(kRedrawCloseDialog, kRedrawFull) the widgets of every dialog,
regardless of their layer, are drawn into Backbuffer and then copied
into Screen.

When asked for a partial dialog rebuild (kRedrawOpenDialog,
kRedrawTopDialog) the widgets of the topmost dialog marked with
kDrawLayerBackground are drawn into Backbuffer, then copied into Screen
and finally the widgets marked with kDrawLayerForeground are drawn into
Screen *only*.

When redraw() is called just within the GuiManager's event loop the
widgets of the topmost dialog are drawn into Screen *only*. And this is
where the layers become important.

When rebuilding the dialog, it doesn't really matter which layer has
been defined for a widget: Backbuffer contains the ones with
kDrawLayerBackground and Screen will supply the rest with
kDrawLayerForeground, if needed. But which layer is taken into account
when calling Dialog::drawWidgets() ?

It is important to realize that the content of Backbuffer is
defined by the widget's initial state (idle or disabled): so Backbuffer
will contain either "idle color" or "disabled color" after dialog's
creation.

ThemeEngine::drawDD() does two checks:

1. if widget has kDrawLayerBackground set *and* _activeSurface is
  Screen, copy the widget from Backbuffer to Screen

2. if widget's layer is the same as _layerToDraw, draw the widget into
  _activeSurface

This is what happens in redraw(kRedrawDisabled) for kDrawLayerBackground
widgets:

- Backbuffer contains an idle/disabled (depending on its initial state)
  rendition of the widget
- widget is copied from Backbuffer to Screen (1st check in drawDD())
- widget is not drawn into Screen as _layerToDraw is
  kDrawLayerForeground (2nd check in drawDD())

Summary: when switching between idle/disabled state, widget's color is
not updated.

This is what happens in redraw(kRedrawDisabled) for kDrawLayerForeground
widgets:

- Backbuffer contains an idle/disabled (depending on its initial state)
  rendition of the widget
- widget is not copied from Backbuffer to Screen as widget has
  kDrawLayerForeground set (1st check in drawDD())
- widget is drawn into Screen as _layerToDraw is still
  kDrawLayerForeground from the last redraw() (2nd check in drawDD())

Summary: when switching between idle/disabled state, widget's color is
correctly updated and not restored from Backbuffer.

Initially, I set up "button idle" to be rendered in the foreground
layer, same as "button disabled". However @lephilousophe suggested a
great improvement: render "button idle" still in the background but make
"button disabled" its child (in the foreground). Worked like a charm as
it just mimics the hovering behaviour.

And this is why hovering doesn't require scheduleTopDialogRedraw():

- Backbuffer contains an idle [kDrawLayerBackground] rendition of the
  widget
- when highlighted [kDrawLayerForeground], widget is not copied from
  Backbuffer to Screen
- widget is drawn into Screen

Unhovering:

- Backbuffer contains an idle [kDrawLayerBackground] rendition of the
  widget
- when idle [kDrawLayerBackground], widget is copied from Backbuffer to
  Screen
- widget is not drawn into Screen
@mikrosk
Copy link
Contributor Author

mikrosk commented May 10, 2023

@lephilousophe I have no problem with that, so rebased on master & pushed. Once again thanks for your thorough review.

@mikrosk mikrosk deleted the gui_opts branch May 10, 2023 20:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants