[Scummvm-git-logs] scummvm master -> 79732d81cf253ef460db80ee62c96511e7604a66
bluegr
noreply at scummvm.org
Wed Jul 8 22:40:06 UTC 2026
This automated email contains information about 8 new commits which have been
pushed to the 'scummvm' repo located at https://api.github.com/repos/scummvm/scummvm .
Summary:
7c6206548d NANCY: NANCY10: Fix hovering over icons with active notification badges
ac988279aa NANCY: NANCY10: Cellphone UI fixes
bb346803cf NANCY: NANCY10: Read scene change data in PlaySecondaryMovie
33893e6eb8 NANCY: Add extra debug info to some value handling ARs
10d7bf2135 NANCY: NANCY10: Clip hit test and highlighting to the visible viewport
3572b9725e NANCY: Make conversation responses clickable across the whole text area
7369479f54 NANCY: Set enum values for continueSceneSound
79732d81cf NANCY: NANCY10: Fix cellphone conversation cursor repositioning
Commit: 7c6206548d1ef93e389145b200674628923adb61
https://github.com/scummvm/scummvm/commit/7c6206548d1ef93e389145b200674628923adb61
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-09T01:39:46+03:00
Commit Message:
NANCY: NANCY10: Fix hovering over icons with active notification badges
Fix #16925
Changed paths:
engines/nancy/ui/taskbar.cpp
diff --git a/engines/nancy/ui/taskbar.cpp b/engines/nancy/ui/taskbar.cpp
index bdca7818376..92ef2fcb6f0 100644
--- a/engines/nancy/ui/taskbar.cpp
+++ b/engines/nancy/ui/taskbar.cpp
@@ -422,7 +422,11 @@ void Taskbar::handleInput(NancyInput &input) {
drawButton(_hoveredButton, restingState(_hoveredButton));
}
if (newHovered != -1 && hoveredActive) {
- drawButton(newHovered, kButtonHover);
+ // A pending notification badge takes priority over the hover sprite,
+ // so a badged button keeps its badge while hovered.
+ const ButtonState hoverState =
+ restingState(newHovered) == kButtonNotification ? kButtonNotification : kButtonHover;
+ drawButton(newHovered, hoverState);
if (isMoneyDisplay(newHovered)) {
drawMoney();
}
Commit: ac988279aadeac4de55b12811818b375d3a12372
https://github.com/scummvm/scummvm/commit/ac988279aadeac4de55b12811818b375d3a12372
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-09T01:39:47+03:00
Commit Message:
NANCY: NANCY10: Cellphone UI fixes
Fix #16923, #16926 and #16928
- Highlight mail and web icons when hovered
- Add an animation from closed to open envelope icon when opening an
email
- Fix whitespace on top of emails
- Add BACK button in the inbox listing
- Show River Heights Wireless homepage when opening the browser
- Adjust the search terms screen layout
- Increase scroll step when viewing web pages
- Fix cursor stuttering when hovering over and away from scroll arrows
- The cellphone should not close after a player-initiated call
Changed paths:
engines/nancy/action/miscrecords.cpp
engines/nancy/ui/cellphonepopup.cpp
engines/nancy/ui/cellphonepopup.h
diff --git a/engines/nancy/action/miscrecords.cpp b/engines/nancy/action/miscrecords.cpp
index c0de26b1d90..70fcc76d3d4 100644
--- a/engines/nancy/action/miscrecords.cpp
+++ b/engines/nancy/action/miscrecords.cpp
@@ -390,8 +390,9 @@ void CellPhonePopCellSceneFromStack::execute() {
NancySceneState.changeScene(returnScene);
}
- // Conversation is over; take the phone down.
- phone.close();
+ // Conversation is over. An incoming call closes the phone; a player-placed
+ // call leaves it open at the welcome screen.
+ phone.endCall();
finishExecution();
}
diff --git a/engines/nancy/ui/cellphonepopup.cpp b/engines/nancy/ui/cellphonepopup.cpp
index 2d38db329c1..ec2e7c319fd 100644
--- a/engines/nancy/ui/cellphonepopup.cpp
+++ b/engines/nancy/ui/cellphonepopup.cpp
@@ -19,6 +19,8 @@
*
*/
+#include "common/system.h"
+
#include "engines/nancy/cursor.h"
#include "engines/nancy/font.h"
#include "engines/nancy/graphics.h"
@@ -294,11 +296,43 @@ void CellPhonePopup::close() {
setVisible(false);
}
+void CellPhonePopup::endCall() {
+ if (_callWasIncoming) {
+ // Incoming call: take the phone down.
+ _callWasIncoming = false;
+ close();
+ return;
+ }
+
+ // Player-placed call: return to the welcome screen and stay open.
+ _callWasIncoming = false;
+ if (!_isVisible) {
+ return;
+ }
+ _screenState = kWelcome;
+ _dialedNumber.clear();
+ _resolvedContact = -1;
+ _autoDialPending = false;
+ _pressedSlot = -1;
+ drawScreenContent();
+}
+
void CellPhonePopup::updateGraphics() {
if (!_isVisible) {
return;
}
+ // Finish the email "opening" flash: once the brief delay elapses, open the
+ // message body (the closedâopen envelope flash showed on the list in the
+ // meantime).
+ if (_openingEmailRow != -1 && g_system->getMillis() >= _openingEmailTime) {
+ const Common::String key = _openingEmailKey;
+ _openingEmailRow = -1;
+ _openingEmailKey.clear();
+ openContentView(key, _uiclData->emailHeading);
+ return;
+ }
+
// A queued auto-dial / Talk waits for the key's DTMF tone to finish so the
// last digit stays audible before the outgoing-ring sound takes over the
// shared call-sound channel.
@@ -367,17 +401,21 @@ void CellPhonePopup::updateGraphics() {
case kConnected:
// Trigger the scene change once, then sit in kConnected so the
// connecting sprite stays on screen for the duration of the
- // conversation. AR 128 closes the popup when the call ends.
+ // conversation. AR 128 (endCall) takes the phone down afterwards.
// Incoming calls carry their destination in _pendingCallScene;
- // outgoing calls resolve it from the active contact.
+ // outgoing calls resolve it from the active contact. The origin is
+ // remembered so AR 128 can close the phone after an incoming call but
+ // leave it open (welcome screen) after a player-placed one.
if (_hasPendingCallScene) {
SceneChangeDescription scene = _pendingCallScene;
_hasPendingCallScene = false;
+ _callWasIncoming = true;
setReturnScene(NancySceneState.getSceneInfo());
NancySceneState.changeScene(scene);
resetDialPad();
} else if (_resolvedContact >= 0 &&
_resolvedContact < (int)_contacts.size()) {
+ _callWasIncoming = false;
triggerContactCallSceneChange((uint)_resolvedContact);
_resolvedContact = -1;
resetDialPad();
@@ -491,37 +529,33 @@ void CellPhonePopup::drawScreenContent() {
drawHeading(_uiclData->onlineHeading);
drawBackButton(0);
// Email / Web option buttons (subButtons 3 and 4) sit inside the LCD.
- const UICL::ThreeRectWidget &emailBtn = _uiclData->subButtons[3];
- const UICL::ThreeRectWidget &webBtn = _uiclData->subButtons[4];
- const Common::Point chunkOrigin(_screenPosition.left, _screenPosition.top);
- if (!emailBtn.srcRectIdle.isEmpty() && !emailBtn.destRect.isEmpty()) {
- _drawSurface.blitFrom(_spritesImage, emailBtn.srcRectIdle,
- Common::Point(emailBtn.destRect.left - chunkOrigin.x,
- emailBtn.destRect.top - chunkOrigin.y));
- }
- if (!webBtn.srcRectIdle.isEmpty() && !webBtn.destRect.isEmpty()) {
- _drawSurface.blitFrom(_spritesImage, webBtn.srcRectIdle,
- Common::Point(webBtn.destRect.left - chunkOrigin.x,
- webBtn.destRect.top - chunkOrigin.y));
- }
+ // Each highlights (its pressed sprite) when the cursor is over it.
+ drawHubButton(3);
+ drawHubButton(4);
break;
}
case kWebList:
- // Web search-results list (AR-131 mode 1).
+ // Web search-results list (AR-131 mode 1). Bottom button is HOME
+ // (subButtons[9]) â back to the browser homepage.
drawHeading(_uiclData->searchHeading);
drawLinkList();
drawDirectoryArrows();
+ drawBackButton(9);
break;
case kEmailList:
drawHeading(_uiclData->emailHeading);
drawLinkList();
drawDirectoryArrows();
+ drawBackButton(7);
break;
case kContentView:
- if (_contentHeading) {
+ // Browser pages use the interactive top-row "SEARCH" button
+ // (subButtons[8], drawn below) in place of a static heading; help /
+ // email keep their static heading.
+ if (_contentHeading && _contentHeading != &_uiclData->browserHeading) {
drawHeading(*_contentHeading);
}
drawContentView();
@@ -530,6 +564,11 @@ void CellPhonePopup::drawScreenContent() {
// drawDirectoryArrows() blits whichever scroll pair applies.
drawDirectoryArrows();
drawBackButton(isHelpContentView() ? 0 : 7);
+ // Browser pages carry the top-row "SEARCH" button (subButtons[8]),
+ // which highlights green on hover and opens the search-topics list.
+ if (_contentHeading == &_uiclData->browserHeading) {
+ drawHubButton(8);
+ }
break;
}
@@ -775,11 +814,14 @@ void CellPhonePopup::drawLinkList() {
const uint absolute = visible[_directoryScroll + visibleRow];
const Common::Rect rowRect = directoryRowRect(titleRows + visibleRow);
- // In email mode, prefix each row with the unread / selected icon.
+ // Every inbox row shows the closed-envelope icon; the opened envelope is
+ // only flashed on the row being opened (see _openingEmailRow), not used
+ // as a persistent read/selection indicator.
int textX = rowRect.left;
if (_screenState == kEmailList) {
- const Common::Rect &icon = (visibleRow == _directorySelection &&
- !_uiclData->emailIconSelected.isEmpty())
+ const bool opening = ((int)(_directoryScroll + visibleRow) == _openingEmailRow) &&
+ !_uiclData->emailIconSelected.isEmpty();
+ const Common::Rect &icon = opening
? _uiclData->emailIconSelected
: _uiclData->emailIconUnread;
if (!icon.isEmpty()) {
@@ -820,45 +862,19 @@ void CellPhonePopup::openContentView(const Common::String &key, const UICL::SrcD
}
void CellPhonePopup::openBrowserHome() {
- // Web shows the navigable topic list (clickable rows + arrow-key
- // selection); clicking a topic opens its article in the content view.
- enterScreenState(kWebList);
-}
-
-void CellPhonePopup::drawContentView() {
- if (_contentKey.empty()) {
- return;
+ // The Web button opens the browser home page (UIBW page 0 â the "River
+ // Heights Wireless" homepage), matching the original's FUN_004dae28(0).
+ // Its in-page hyperlinks then navigate to further pages / the search list.
+ const UIBW *browserData = GetEngineData(UIBW);
+ if (browserData && !browserData->pages.empty()) {
+ openContentView(browserData->pages[0].imageName.toString(), _uiclData->browserHeading);
+ } else {
+ enterScreenState(kWebList);
}
+}
+void CellPhonePopup::renderContentPage(int surfaceWidth) {
const CVTX *autotext = (const CVTX *)g_nancy->getEngineData("AUTOTEXT");
-
- const Font *font = g_nancy->_graphics->getFont(_uiclData->fontId2);
- if (!font) {
- return;
- }
-
- // Browser / email articles run under the zoomed-in chrome (drawChrome
- // blits fullEmptyScreenSrc), so the keypad is no longer visible underneath
- // and we render into the larger LCD area that emailListContainer defines.
- // The help page keeps the regular chrome, so it renders into the small LCD.
- const Common::Rect &ws =
- (isHelpContentView() || _uiclData->emailListContainer.isEmpty())
- ? _uiclData->welcomeScreen.destRect
- : _uiclData->emailListContainer;
- const int lcdLeft = ws.left - _screenPosition.left;
- const int lcdTop = ws.top - _screenPosition.top;
- const int lcdW = ws.width();
- const int lcdH = ws.height();
- // The email / browser heading sits inside the zoomed LCD, so the article
- // text starts below it. The help page's heading is in the title-bar strip
- // above the small LCD, so its text starts flush with the LCD top (matching
- // the original's "Help info in small window" placement).
- const int textTop = isHelpContentView() ? 2 : 22;
- const int viewH = MAX(0, lcdH - textTop);
- const int rowH = MAX(font->getFontHeight() + 1, 12);
-
- // Render the engine's hypertext markup into a tall scratch surface,
- // then blit a vertically-scrolled window of it into the LCD.
const Common::String renderText = autotext->texts.getValOrDefault(_contentKey, "");
// Find this page in the UIBW chunk (browser pages only); its hotspot
@@ -879,10 +895,9 @@ void CellPhonePopup::drawContentView() {
}
}
- // Parse the <H>...<L> regions out of the body before rendering â each
- // becomes a clickable in-page hyperlink. The text between the markers
- // is used as the target article CVTX key.
- _contentHotspotTargets.clear();
+ // Parse the <H>...<L> regions out of the body â each becomes a clickable
+ // in-page hyperlink; the text between the markers is the target CVTX key.
+ _contentCacheTargets.clear();
{
uint32 cursor = 0;
while (cursor < renderText.size()) {
@@ -897,7 +912,7 @@ void CellPhonePopup::drawContentView() {
}
Common::String linkText = renderText.substr(linkTextStart, lStart - linkTextStart);
linkText.toUppercase();
- _contentHotspotTargets.push_back(linkText);
+ _contentCacheTargets.push_back(linkText);
cursor = lStart + 3;
}
}
@@ -913,11 +928,52 @@ void CellPhonePopup::drawContentView() {
}
}
const uint32 trans = g_nancy->_graphics->getTransColor();
- ht.render(lcdW, 2000, trans, renderText, _uiclData->fontId2);
+ ht.render(surfaceWidth, 2000, trans, renderText, _uiclData->fontId2);
+
+ _contentCacheSurface.copyFrom(ht.surface());
+ _contentCacheSurface.setTransparentColor(trans);
+ _contentCacheTextHeight = ht.textHeight();
+ _contentCacheHotspots = ht.hotspots();
+}
+
+void CellPhonePopup::drawContentView() {
+ if (_contentKey.empty()) {
+ return;
+ }
+
+ const Font *font = g_nancy->_graphics->getFont(_uiclData->fontId2);
+ if (!font) {
+ return;
+ }
+
+ // Browser / email articles run under the zoomed-in chrome (drawChrome
+ // blits fullEmptyScreenSrc), so the keypad is no longer visible underneath
+ // and we render into the larger LCD area that emailListContainer defines.
+ // The help page keeps the regular chrome, so it renders into the small LCD.
+ const Common::Rect &ws =
+ (isHelpContentView() || _uiclData->emailListContainer.isEmpty())
+ ? _uiclData->welcomeScreen.destRect
+ : _uiclData->emailListContainer;
+ const int lcdLeft = ws.left - _screenPosition.left;
+ const int lcdTop = ws.top - _screenPosition.top;
+ const int lcdW = ws.width();
+ const int lcdH = ws.height();
+ // The heading (help / email / browser) sits in the title-bar strip above the
+ // LCD, so the body text starts flush with the LCD top â a small inset only.
+ const int textTop = 2;
+ const int viewH = MAX(0, lcdH - textTop);
+ const int rowH = MAX(font->getFontHeight() + 1, 12);
+
+ // (Re)render the page only when its key changes; scrolling and hover
+ // redraws reuse the cached surface (just re-blit a different window).
+ if (_contentKey != _contentCacheKey) {
+ renderContentPage(lcdW);
+ _contentCacheKey = _contentKey;
+ }
+ _contentHotspotTargets = _contentCacheTargets;
// Clamp scroll to the rendered text height.
- const int textH = ht.textHeight();
- const int maxScrollPx = MAX(0, textH - viewH);
+ const int maxScrollPx = MAX(0, (int)_contentCacheTextHeight - viewH);
const int maxScroll = maxScrollPx / rowH;
if ((int)_contentScroll > maxScroll) {
_contentScroll = maxScroll;
@@ -925,23 +981,22 @@ void CellPhonePopup::drawContentView() {
const int srcTop = (int)_contentScroll * rowH;
Common::Rect srcRect(0, srcTop, lcdW, srcTop + viewH);
- srcRect.clip(Common::Rect(ht.surface().w, ht.surface().h));
+ srcRect.clip(Common::Rect(_contentCacheSurface.w, _contentCacheSurface.h));
if (srcRect.isEmpty()) {
_contentHotspots.clear();
return;
}
- _drawSurface.blitFrom(ht.surface(), srcRect,
+ _drawSurface.blitFrom(_contentCacheSurface, srcRect,
Common::Point(lcdLeft, lcdTop + textTop));
- // Translate the parser's hotspots (surface coords) into popup-local
- // coords for the current scroll. Drop any that aren't fully visible
- // inside the LCD window so we don't fire on partially-clipped links.
+ // Translate the cached hotspots (surface coords) into popup-local coords
+ // for the current scroll. Drop any that aren't fully visible inside the
+ // LCD window so we don't fire on partially-clipped links.
_contentHotspots.clear();
- const Common::Array<Common::Rect> &surfaceHs = ht.hotspots();
- const uint linkCount = MIN(surfaceHs.size(), _contentHotspotTargets.size());
+ const uint linkCount = MIN(_contentCacheHotspots.size(), _contentHotspotTargets.size());
for (uint i = 0; i < linkCount; ++i) {
- Common::Rect r = surfaceHs[i];
+ Common::Rect r = _contentCacheHotspots[i];
r.translate(lcdLeft, lcdTop + textTop - srcTop);
const Common::Rect lcdClip(lcdLeft, lcdTop + textTop,
lcdLeft + lcdW, lcdTop + textTop + viewH);
@@ -1031,6 +1086,24 @@ void CellPhonePopup::drawBackButton(uint subButtonIndex) {
back.destRect.top - chunkOrigin.y));
}
+void CellPhonePopup::drawHubButton(uint subButtonIndex) {
+ const UICL::ThreeRectWidget &btn = _uiclData->subButtons[subButtonIndex];
+ if (btn.destRect.isEmpty()) {
+ return;
+ }
+ const bool hovered = (_hoveredHubButton == (int)subButtonIndex);
+ const Common::Rect &src = (hovered && !btn.srcRectPressed.isEmpty())
+ ? btn.srcRectPressed
+ : btn.srcRectIdle;
+ if (src.isEmpty()) {
+ return;
+ }
+ const Common::Point chunkOrigin(_screenPosition.left, _screenPosition.top);
+ _drawSurface.blitFrom(_spritesImage, src,
+ Common::Point(btn.destRect.left - chunkOrigin.x,
+ btn.destRect.top - chunkOrigin.y));
+}
+
void CellPhonePopup::drawPressedDialKey() {
if (_pressedSlot < 0 || _pressedSlot >= (int)UICL::kNumDialPadSlots) {
return;
@@ -1101,11 +1174,10 @@ void CellPhonePopup::drawDirectoryArrows() {
}
}
- // Selection indicator (dirArrowSrc sprite) at the dirCursorSrc column,
- // stepped down by the active entry's layout row. Drawn for directory
- // and the search-topic list; the email list signals the current row
- // by swapping its per-row icon, so no separate arrow there.
- if (_screenState != kDirectory && _screenState != kWebList) {
+ // Selection indicator (dirArrowSrc sprite) â only the contacts directory
+ // shows it. The search-topic and email lists are plain lists in the
+ // original (no per-row selection arrow).
+ if (_screenState != kDirectory) {
return;
}
const Common::Rect &arrowSrc = _uiclData->dirArrowSrc;
@@ -1135,6 +1207,11 @@ void CellPhonePopup::resetDialPad() {
void CellPhonePopup::enterScreenState(ScreenState newState) {
// Always redraw, so successive digit entries refresh the readout.
_screenState = newState;
+ _hoveredHubButton = -1;
+ if (newState != kContentView) {
+ // Cancel a pending email-open flash unless we're completing it.
+ _openingEmailRow = -1;
+ }
drawScreenContent();
}
@@ -1294,10 +1371,10 @@ bool CellPhonePopup::consumeReturnScene(SceneChangeDescription &out) {
// --------------------------------------------------------------------
int CellPhonePopup::rowPitch() const {
- // Email rows are sized by the unread/selected icon so they don't
- // overlap; directory and search lists use the compact arrow-cursor
- // pitch.
- if (_screenState == kEmailList && !_uiclData->emailIconUnread.isEmpty()) {
+ // The email and search lists render in the tall zoomed LCD with generous,
+ // evenly-spaced rows (sized by the envelope icon); the contacts directory
+ // uses the compact arrow-cursor pitch.
+ if (isLinkListMode() && !_uiclData->emailIconUnread.isEmpty()) {
return _uiclData->emailIconUnread.height() + 1;
}
const Common::Rect &cursor = _uiclData->dirCursorSrc;
@@ -1308,9 +1385,9 @@ int CellPhonePopup::rowPitch() const {
}
int CellPhonePopup::rowTopScreen() const {
- // Email list anchors on the zoomed-chrome list container; everything
- // else stacks under the arrow-cursor row.
- if (_screenState == kEmailList && !_uiclData->emailListContainer.isEmpty()) {
+ // The email / search lists anchor on the zoomed-chrome list container;
+ // the directory stacks under the arrow-cursor row.
+ if (isLinkListMode() && !_uiclData->emailListContainer.isEmpty()) {
return _uiclData->emailListContainer.top;
}
const Common::Rect &cursor = _uiclData->dirCursorSrc;
@@ -1325,7 +1402,7 @@ uint CellPhonePopup::maxDirectoryRows() const {
if (pitch <= 0) {
return 0;
}
- const int yLimit = (_screenState == kEmailList && !_uiclData->emailListContainer.isEmpty())
+ const int yLimit = (isLinkListMode() && !_uiclData->emailListContainer.isEmpty())
? _uiclData->emailListContainer.bottom
: _uiclData->welcomeScreen.destRect.bottom;
int y = rowTopScreen();
@@ -1354,7 +1431,11 @@ Common::Rect CellPhonePopup::directoryRowRect(uint visibleIndex) const {
// Row text spans from just right of the arrow cursor to a margin
// inside the LCD's right edge.
int xLeftScreen, xRightScreen;
- if (!cursor.isEmpty()) {
+ if (_screenState == kWebList) {
+ // Search list: a plain left-aligned list â no arrow/icon column.
+ xLeftScreen = lcd.left + 8;
+ xRightScreen = lcd.right - 8;
+ } else if (!cursor.isEmpty()) {
xLeftScreen = cursor.right + 5;
xRightScreen = lcd.right - 30;
} else {
@@ -1738,6 +1819,14 @@ void CellPhonePopup::handleInput(NancyInput &input) {
const Common::Rect webR = hubWebRect();
const Common::Rect backHit = backButtonHitRect(0);
+ // Highlight whichever option button the cursor is over.
+ const int newHubHover = emailR.contains(popupMouse) ? 3
+ : webR.contains(popupMouse) ? 4 : -1;
+ if (newHubHover != _hoveredHubButton) {
+ _hoveredHubButton = newHubHover;
+ drawScreenContent();
+ }
+
if (emailR.contains(popupMouse)) {
g_nancy->_cursor->setCursorType(CursorManager::kHotspotArrow);
if (input.input & NancyInput::kLeftMouseButtonUp) {
@@ -1789,7 +1878,10 @@ void CellPhonePopup::handleInput(NancyInput &input) {
}
}
- const Common::Rect backHit = backLabelHitRect();
+ // The list views show a bottom button at the same spot: the email list
+ // a BACK (subButtons[7]) to the hub, the search list a HOME
+ // (subButtons[9]) to the browser homepage. Same dest rect either way.
+ const Common::Rect backHit = backButtonHitRect(_screenState == kWebList ? 9 : 7);
const Common::Point popupMouse(chunkMouse.x - _screenPosition.left,
chunkMouse.y - _screenPosition.top);
const bool overUpDown =
@@ -1799,7 +1891,13 @@ void CellPhonePopup::handleInput(NancyInput &input) {
if (input.input & NancyInput::kLeftMouseButtonUp) {
_directoryScroll = 0;
_directorySelection = 0;
- enterScreenState(kOnlineHub);
+ // Search list returns HOME (the browser homepage); the email
+ // list returns to the online hub it was opened from.
+ if (_screenState == kWebList) {
+ openBrowserHome();
+ } else {
+ enterScreenState(kOnlineHub);
+ }
input.eatMouseInput();
return;
}
@@ -1832,9 +1930,13 @@ void CellPhonePopup::handleInput(NancyInput &input) {
NancySceneState.setEventFlag(e.eventFlag, g_nancy->_true);
}
if (_screenState == kEmailList && !e.value.empty()) {
- // Email: open the message body and mark as read.
+ // Flash the opened-envelope icon on this row, then open
+ // the message body a beat later (see updateGraphics).
e.read = true;
- openContentView(e.value, _uiclData->emailHeading);
+ _openingEmailRow = (int)visIdx;
+ _openingEmailKey = e.value;
+ _openingEmailTime = g_system->getMillis() + 200;
+ drawScreenContent();
} else if (_screenState == kWebList) {
// AR-131 mode-1 stores a browser-page INDEX in
// `extra`; the page body lives in the UIBW chunk
@@ -1868,6 +1970,30 @@ void CellPhonePopup::handleInput(NancyInput &input) {
// overlapping fallthrough hit (e.g. the back hotspot).
const Common::Point popupMouseLink(chunkMouse.x - _screenPosition.left,
chunkMouse.y - _screenPosition.top);
+
+ // On browser pages the top-row "SEARCH" button (subButtons[8]) opens the
+ // search-topics list and highlights green while hovered.
+ if (_contentHeading == &_uiclData->browserHeading &&
+ !_uiclData->subButtons[8].destRect.isEmpty()) {
+ Common::Rect searchBtn = _uiclData->subButtons[8].destRect;
+ searchBtn.translate(-_screenPosition.left, -_screenPosition.top);
+ const int newHover = searchBtn.contains(popupMouseLink) ? 8 : -1;
+ if (newHover != _hoveredHubButton) {
+ _hoveredHubButton = newHover;
+ drawScreenContent();
+ }
+ if (searchBtn.contains(popupMouseLink)) {
+ g_nancy->_cursor->setCursorType(CursorManager::kHotspotArrow);
+ if (input.input & NancyInput::kLeftMouseButtonUp) {
+ _directoryScroll = 0;
+ _directorySelection = 0;
+ enterScreenState(kWebList);
+ input.eatMouseInput();
+ return;
+ }
+ }
+ }
+
for (uint i = 0; i < _contentHotspots.size(); ++i) {
if (_contentHotspots[i].isEmpty()) {
continue;
@@ -1892,11 +2018,14 @@ void CellPhonePopup::handleInput(NancyInput &input) {
const Common::Rect &upDst = scrollUpButton().destRect;
const Common::Rect &downDst = scrollDownButton().destRect;
+ // One click scrolls several lines, matching the original (a single line
+ // per click makes long web pages tedious to read).
+ const uint kContentScrollStep = 3;
if (upDst.contains(chunkMouse)) {
g_nancy->_cursor->setCursorType(CursorManager::kHotspotArrow);
if (input.input & NancyInput::kLeftMouseButtonUp) {
if (_contentScroll > 0) {
- --_contentScroll;
+ _contentScroll = _contentScroll > kContentScrollStep ? _contentScroll - kContentScrollStep : 0;
drawScreenContent();
}
input.eatMouseInput();
@@ -1905,7 +2034,7 @@ void CellPhonePopup::handleInput(NancyInput &input) {
} else if (downDst.contains(chunkMouse)) {
g_nancy->_cursor->setCursorType(CursorManager::kHotspotArrow);
if (input.input & NancyInput::kLeftMouseButtonUp) {
- ++_contentScroll;
+ _contentScroll += kContentScrollStep;
drawScreenContent();
input.eatMouseInput();
return;
diff --git a/engines/nancy/ui/cellphonepopup.h b/engines/nancy/ui/cellphonepopup.h
index 4570424424d..45e532aa78d 100644
--- a/engines/nancy/ui/cellphonepopup.h
+++ b/engines/nancy/ui/cellphonepopup.h
@@ -77,6 +77,11 @@ public:
// into `scene` (AR 128 returns via the setReturnScene slot).
void startIncomingCall(const SceneChangeDescription &scene);
+ // Called by AR 128 when a call's conversation ends. An incoming call takes
+ // the phone down; a player-placed call leaves it open at the welcome screen
+ // (matching the original CCellPhonePopCellSceneFromStack).
+ void endCall();
+
private:
enum ScreenState : int {
kWelcome = 0,
@@ -114,6 +119,9 @@ private:
// Back buttons: subButtons[0] on the help / directory / online screens,
// subButtons[7] in the zoomed email / browser content view).
void drawBackButton(uint subButtonIndex);
+ // Blit an online-hub option button (mail / browser), using its highlighted
+ // sprite when the cursor is over it.
+ void drawHubButton(uint subButtonIndex);
// Blit the lit key sprite of the currently held dial-pad slot over its
// dest rect, so keypad keys visually depress while pressed.
void drawPressedDialKey();
@@ -125,6 +133,10 @@ private:
void drawHeading(const UICL::SrcDestRectPair &heading);
// Render the opened entry's body text in the LCD area, word-wrapped.
void drawContentView();
+ // Expensive: render the current content page's hypertext into the cache
+ // surface (+ text height, image/link hotspots). Called by drawContentView
+ // only when the page key changes.
+ void renderContentPage(int surfaceWidth);
// Enter the content view for a list entry whose AUTOTEXT key is `key`.
void openContentView(const Common::String &key, const UICL::SrcDestRectPair &heading);
// Web button: open the first url entry as the browser home page (page 0).
@@ -243,6 +255,10 @@ private:
// Dial-pad slot currently held down (shows the lit / depressed key), or -1.
int _pressedSlot = -1;
+ // Online-hub option button under the cursor (subButtons index 3 = mail,
+ // 4 = browser), drawn with its highlighted sprite; -1 = none.
+ int _hoveredHubButton = -1;
+
// A call queued by auto-dial / Talk, waiting for the key's DTMF tone to
// finish before entering kPlaceCall (see updateGraphics).
bool _autoDialPending = false;
@@ -257,11 +273,28 @@ private:
Common::String _contentKey;
uint _contentScroll = 0;
+ // Email "opening" transition: the visible row whose closed envelope briefly
+ // flashes open before the body is shown (-1 = none), the deadline for that
+ // transition, and the body CVTX key to open when it fires.
+ int _openingEmailRow = -1;
+ uint32 _openingEmailTime = 0;
+ Common::String _openingEmailKey;
+
// In-page hyperlinks: rects (popup-local, recomputed every draw) and
// the target CVTX key parsed from each <H>...<L> region of the body.
Common::Array<Common::Rect> _contentHotspots;
Common::Array<Common::String> _contentHotspotTargets;
+ // Cached render of the current content page. Rendering the hypertext is
+ // expensive, so it's only rebuilt when the page key changes â scrolling
+ // and hover redraws just re-blit a window of the cached surface (fixes the
+ // cursor stutter while hovering the scroll arrows).
+ Graphics::ManagedSurface _contentCacheSurface;
+ Common::String _contentCacheKey;
+ uint16 _contentCacheTextHeight = 0;
+ Common::Array<Common::Rect> _contentCacheHotspots;
+ Common::Array<Common::String> _contentCacheTargets;
+
bool _noSignal = false;
bool _batteryLow = false;
@@ -270,6 +303,11 @@ private:
SceneChangeDescription _pendingCallScene;
bool _hasPendingCallScene = false;
+ // True while the in-progress / just-finished call was incoming (auto-rung),
+ // so AR 128 knows to close the phone afterwards; player-placed calls leave
+ // it open. Persists past _hasPendingCallScene, which is consumed on connect.
+ bool _callWasIncoming = false;
+
SceneChangeDescription _returnScene;
bool _hasReturnScene = false;
};
Commit: bb346803cf850d701178dc8094afb8eb629dcc6e
https://github.com/scummvm/scummvm/commit/bb346803cf850d701178dc8094afb8eb629dcc6e
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-09T01:39:48+03:00
Commit Message:
NANCY: NANCY10: Read scene change data in PlaySecondaryMovie
Fixes the pendulum scene at the start of Nancy11
Changed paths:
engines/nancy/action/secondarymovie.cpp
diff --git a/engines/nancy/action/secondarymovie.cpp b/engines/nancy/action/secondarymovie.cpp
index 2e4701d9952..e8656d340ef 100644
--- a/engines/nancy/action/secondarymovie.cpp
+++ b/engines/nancy/action/secondarymovie.cpp
@@ -237,8 +237,12 @@ void PlaySecondaryMovie::readData(Common::SeekableReadStream &stream) {
ser.syncAsUint16LE(_firstFrame);
ser.syncAsUint16LE(_lastFrame);
- ser.syncAsSint16LE(_sceneChange.sceneID, kGameTypeNancy10);
- ser.skip(3 * 2, kGameTypeNancy10); // TODO
+ if (g_nancy->getGameType() >= kGameTypeNancy10) {
+ ser.syncAsSint16LE(_sceneChange.sceneID);
+ ser.syncAsUint16LE(_sceneChange.frameID);
+ ser.syncAsUint16LE(_sceneChange.verticalOffset);
+ ser.syncAsUint16LE(_sceneChange.continueSceneSound);
+ }
if (g_nancy->getGameType() >= kGameTypeNancy10) {
ser.syncAsSint16LE(_videoStartFlag.label);
Commit: 33893e6eb833feba34aae25159f8cf4030c73def
https://github.com/scummvm/scummvm/commit/33893e6eb833feba34aae25159f8cf4030c73def
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-09T01:39:49+03:00
Commit Message:
NANCY: Add extra debug info to some value handling ARs
Changed paths:
engines/nancy/action/datarecords.h
diff --git a/engines/nancy/action/datarecords.h b/engines/nancy/action/datarecords.h
index cf4cecacb34..c734bc5318f 100644
--- a/engines/nancy/action/datarecords.h
+++ b/engines/nancy/action/datarecords.h
@@ -59,6 +59,10 @@ public:
void readData(Common::SeekableReadStream &stream) override;
void execute() override;
+ Common::String getRecordExtraInfo() const override {
+ return Common::String::format("Value %d -> %d (should set is %d)", _index, _value, _shouldSet);
+ }
+
protected:
Common::String getRecordTypeName() const override { return "SetValue"; }
@@ -72,6 +76,10 @@ public:
void readData(Common::SeekableReadStream &stream) override;
void execute() override;
+ Common::String getRecordExtraInfo() const override {
+ return Common::String::format("Value %d", _valueIndex);
+ }
+
protected:
Common::String getRecordTypeName() const override { return "SetValueCombo"; }
@@ -85,6 +93,10 @@ public:
void readData(Common::SeekableReadStream &stream) override;
void execute() override;
+ Common::String getRecordExtraInfo() const override {
+ return Common::String::format("Value %d. Test type %d, condition %d. Set flag %d", _valueIndex, _testType, _condition, _flagToSet);
+ }
+
protected:
Common::String getRecordTypeName() const override { return "ValueTest"; }
Commit: 10d7bf2135d074c14b3d1375bc8341e376a34018
https://github.com/scummvm/scummvm/commit/10d7bf2135d074c14b3d1375bc8341e376a34018
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-09T01:39:50+03:00
Commit Message:
NANCY: NANCY10: Clip hit test and highlighting to the visible viewport
Fixes cases where hit testing would match hotspots that are outside the
visible viewport
Changed paths:
engines/nancy/ui/conversationpopup.cpp
diff --git a/engines/nancy/ui/conversationpopup.cpp b/engines/nancy/ui/conversationpopup.cpp
index e4dd2892d15..c3e86c3a73d 100644
--- a/engines/nancy/ui/conversationpopup.cpp
+++ b/engines/nancy/ui/conversationpopup.cpp
@@ -272,11 +272,16 @@ void ConversationPopup::handleInput(NancyInput &input) {
const uint16 outer = localTextRect.height();
const int scrollY = inner > outer ? (int)(_scrollPos * (inner - outer)) : 0;
+ // Clip hit-testing and highlighting to the visible text viewport, not the
+ // whole popup: a response scrolled out of view still overlaps the popup rect,
+ // so intersecting with the popup would highlight text that is clipped away.
+ const Common::Rect textAreaOnScreen = convertToScreen(localTextRect);
+
bool hasHighlight = false;
for (uint i = _responseStartIdx; i < _hotspots.size(); ++i) {
Common::Rect hotspot = _hotspots[i];
hotspot.translate(localTextRect.left, localTextRect.top - scrollY);
- Common::Rect hotspotOnScreen = convertToScreen(hotspot).findIntersectingRect(_screenPosition);
+ Common::Rect hotspotOnScreen = convertToScreen(hotspot).findIntersectingRect(textAreaOnScreen);
if (hotspotOnScreen.contains(input.mousePos)) {
g_nancy->_cursor->setCursorType(CursorManager::kHotspotArrow);
Commit: 3572b9725e821509f9701f2acbc01f00f4bd8c25
https://github.com/scummvm/scummvm/commit/3572b9725e821509f9701f2acbc01f00f4bd8c25
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-09T01:39:52+03:00
Commit Message:
NANCY: Make conversation responses clickable across the whole text area
Fix #16930
Changed paths:
engines/nancy/misc/hypertext.cpp
diff --git a/engines/nancy/misc/hypertext.cpp b/engines/nancy/misc/hypertext.cpp
index 6e737641559..c43eb941456 100644
--- a/engines/nancy/misc/hypertext.cpp
+++ b/engines/nancy/misc/hypertext.cpp
@@ -248,7 +248,11 @@ void HypertextParser::drawAllText(const Common::Rect &textBounds, uint leftOffse
hotspot.left = textBounds.left;
hotspot.top = textBounds.top + (_numDrawnLines * lineStep(font)) - 1;
hotspot.setHeight(0);
- hotspot.setWidth(0);
+ // Conversation responses are clickable across the whole width of the
+ // text area, not only where the glyphs are, so the player can click
+ // (and highlight) anywhere on the line. The MAX() width accumulation
+ // below preserves this since no wrapped line is wider than the area.
+ hotspot.setWidth(textBounds.width());
}
// Go through the wrapped lines and draw them, making sure to
Commit: 7369479f549a3fa8c3e9c34d210336107cca2f9e
https://github.com/scummvm/scummvm/commit/7369479f549a3fa8c3e9c34d210336107cca2f9e
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-09T01:39:52+03:00
Commit Message:
NANCY: Set enum values for continueSceneSound
Changed paths:
engines/nancy/state/scene.cpp
diff --git a/engines/nancy/state/scene.cpp b/engines/nancy/state/scene.cpp
index ca6e0c59cf6..15632c68c40 100644
--- a/engines/nancy/state/scene.cpp
+++ b/engines/nancy/state/scene.cpp
@@ -278,11 +278,11 @@ void Scene::pushScene(int16 itemID) {
void Scene::popScene(bool inventory) {
if (!inventory || _sceneState.pushedInvItemID == -1) {
- _sceneState.pushedScene.continueSceneSound = true;
+ _sceneState.pushedScene.continueSceneSound = kContinueSceneSound;
changeScene(_sceneState.pushedScene);
_sceneState.isScenePushed = false;
} else {
- _sceneState.pushedInvScene.continueSceneSound = true;
+ _sceneState.pushedInvScene.continueSceneSound = kContinueSceneSound;
changeScene(_sceneState.pushedInvScene);
_sceneState.isInvScenePushed = false;
addItemToInventory(_sceneState.pushedInvItemID);
Commit: 79732d81cf253ef460db80ee62c96511e7604a66
https://github.com/scummvm/scummvm/commit/79732d81cf253ef460db80ee62c96511e7604a66
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-09T01:39:53+03:00
Commit Message:
NANCY: NANCY10: Fix cellphone conversation cursor repositioning
Fix #16927
Changed paths:
engines/nancy/state/scene.cpp
diff --git a/engines/nancy/state/scene.cpp b/engines/nancy/state/scene.cpp
index 15632c68c40..eb3b4c59d6a 100644
--- a/engines/nancy/state/scene.cpp
+++ b/engines/nancy/state/scene.cpp
@@ -1364,7 +1364,11 @@ Common::Rect Scene::activePopupConfinement() const {
if (_conversationPopup.isVisible()) return _conversationPopup.getScreenPosition();
if (_inventoryPopup.isVisible()) return _inventoryPopup.getScreenPosition();
if (_notebookPopup.isVisible()) return _notebookPopup.getScreenPosition();
- if (_cellPhonePopup.isVisible()) return _cellPhonePopup.getScreenPosition();
+ // The cellphone stays up during a call it placed, but the conversation
+ // (textbox) is the active UI then â don't confine the cursor to the phone,
+ // or it fights the textbox as each new line starts.
+ if (_cellPhonePopup.isVisible() && _activeConversation == nullptr)
+ return _cellPhonePopup.getScreenPosition();
return Common::Rect();
}
More information about the Scummvm-git-logs
mailing list