[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