[Scummvm-git-logs] scummvm master -> cb9177ecac780178317f79bf3f20fa94d16a58b5

bluegr noreply at scummvm.org
Thu May 7 00:56:06 UTC 2026


This automated email contains information about 4 new commits which have been
pushed to the 'scummvm' repo located at https://api.github.com/repos/scummvm/scummvm .

Summary:
1a8d9ceb2f NANCY: Make sure that SecondayMovie video decoder is always initialized
c7f8d942b7 NANCY: Update cursor in multibuildpuzzle if a piece can't be dropped
1a3f9a14cb NANCY: Use getters to access single and combo player table values
cb9177ecac NANCY: More work on the new inventory popup used in Nancy10+


Commit: 1a8d9ceb2fe47aff1b6c4040c496a21b7917e412
    https://github.com/scummvm/scummvm/commit/1a8d9ceb2fe47aff1b6c4040c496a21b7917e412
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-05-07T03:55:35+03:00

Commit Message:
NANCY: Make sure that SecondayMovie video decoder is always initialized

It may not be initialized when onPause() is called after loading.

Fix #16768

Changed paths:
    engines/nancy/action/secondarymovie.cpp


diff --git a/engines/nancy/action/secondarymovie.cpp b/engines/nancy/action/secondarymovie.cpp
index 8059bb28d58..6dcf803c4b7 100644
--- a/engines/nancy/action/secondarymovie.cpp
+++ b/engines/nancy/action/secondarymovie.cpp
@@ -104,11 +104,10 @@ void PlaySecondaryMovie::readData(Common::SeekableReadStream &stream) {
 
 void PlaySecondaryMovie::init() {
 	if (!_decoder) {
-		if (_videoType == kVideoPlaytypeAVF) {
+		if (_videoType == kVideoPlaytypeAVF)
 			_decoder.reset(new AVFDecoder());
-		} else {
+		else
 			_decoder.reset(new Video::BinkDecoder());
-		}
 	}
 
 	if (!_decoder->isVideoLoaded()) {
@@ -137,6 +136,13 @@ void PlaySecondaryMovie::init() {
 }
 
 void PlaySecondaryMovie::onPause(bool pause) {
+	if (!_decoder) {
+		if (_videoType == kVideoPlaytypeAVF)
+			_decoder.reset(new AVFDecoder());
+		else
+			_decoder.reset(new Video::BinkDecoder());
+	}
+
 	_decoder->pauseVideo(pause);
 	RenderActionRecord::onPause(pause);
 }


Commit: c7f8d942b724fd6ab506c2d573ca463dedaa62d5
    https://github.com/scummvm/scummvm/commit/c7f8d942b724fd6ab506c2d573ca463dedaa62d5
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-05-07T03:55:37+03:00

Commit Message:
NANCY: Update cursor in multibuildpuzzle if a piece can't be dropped

Fix #16751

Changed paths:
    engines/nancy/action/puzzle/multibuildpuzzle.cpp
    engines/nancy/action/puzzle/multibuildpuzzle.h


diff --git a/engines/nancy/action/puzzle/multibuildpuzzle.cpp b/engines/nancy/action/puzzle/multibuildpuzzle.cpp
index ac0426b79d7..9bba84fd8e0 100644
--- a/engines/nancy/action/puzzle/multibuildpuzzle.cpp
+++ b/engines/nancy/action/puzzle/multibuildpuzzle.cpp
@@ -244,6 +244,31 @@ void MultiBuildPuzzle::execute() {
 	}
 }
 
+bool MultiBuildPuzzle::isValidDrop() const {
+	const Piece &pp = _pieces[_pickedUpPiece];
+
+	// Geometric checks apply only to non-closeup puzzles with a win condition (e.g. books).
+	// Sand castle (no closeup image, _requiredPieces=0) allows free stacking.
+	// Sandwich puzzle (has closeup image) allows free-form placement.
+	if (!_hasCloseupImage && _requiredPieces > 0) {
+		// Boundary check: drop center must be inside the target zone
+		Common::Point dropCenter((pp.gameRect.left + pp.gameRect.right) / 2,
+								 (pp.gameRect.top + pp.gameRect.bottom) / 2);
+		if (!_targetZone.isEmpty() && !_targetZone.contains(dropCenter))
+			return false;
+
+		// Overlap check: piece must not overlap any already-placed piece
+		for (uint i = 0; i < _pieces.size(); ++i) {
+			if ((int)i != _pickedUpPiece && _pieces[i].isPlaced &&
+				pp.gameRect.intersects(_pieces[i].gameRect)) {
+				return false;
+			}
+		}
+	}
+
+	return true;
+}
+
 void MultiBuildPuzzle::handleInput(NancyInput &input) {
 	if (_state != kRun || _solveState != kIdle || _isSolved || _isCancelled)
 		return;
@@ -268,8 +293,9 @@ void MultiBuildPuzzle::handleInput(NancyInput &input) {
 		pp.gameRect.right  = newLeft + _pickedUpWidth;
 		pp.gameRect.bottom = newTop  + _pickedUpHeight;
 		updatePieceRender(_pickedUpPiece);
+		bool validDrop = isValidDrop();
 
-		g_nancy->_cursor->setCursorType(CursorManager::kCustom1);
+		g_nancy->_cursor->setCursorType(validDrop ? CursorManager::kCustom1 : CursorManager::kNormal);
 
 		// Right click: rotate the carried piece
 		if ((input.input & NancyInput::kRightMouseButtonUp) && pp.hasSurface[1]) {
@@ -286,30 +312,6 @@ void MultiBuildPuzzle::handleInput(NancyInput &input) {
 		// the piece overlaps an already-placed piece; piece returns to shelf on rejection.
 		// For closeup puzzles: no geometric checks — free-form placement.
 		if (input.input & NancyInput::kLeftMouseButtonUp) {
-			bool validDrop = true;
-
-			// Geometric checks apply only to non-closeup puzzles with a win condition (e.g. books).
-			// Sand castle (no closeup image, _requiredPieces=0) allows free stacking.
-			// Sandwich puzzle (has closeup image) allows free-form placement.
-			if (!_hasCloseupImage && _requiredPieces > 0) {
-				// Boundary check: drop center must be inside the target zone
-				Common::Point dropCenter((pp.gameRect.left + pp.gameRect.right) / 2,
-				                        (pp.gameRect.top  + pp.gameRect.bottom) / 2);
-				if (!_targetZone.isEmpty() && !_targetZone.contains(dropCenter))
-					validDrop = false;
-
-				// Overlap check: piece must not overlap any already-placed piece
-				if (validDrop) {
-					for (uint i = 0; i < _pieces.size(); ++i) {
-						if ((int)i != _pickedUpPiece && _pieces[i].isPlaced &&
-						        pp.gameRect.intersects(_pieces[i].gameRect)) {
-							validDrop = false;
-							break;
-						}
-					}
-				}
-			}
-
 			// Clear drag state BEFORE updatePieceRender so the correct visual is chosen:
 			// - valid drop:   isPlaced=true,  isDragging=false -> shows rotation surface at drop pos
 			// - invalid drop: isPlaced=false, isDragging=false -> shows shelf srcRect at homeRect
diff --git a/engines/nancy/action/puzzle/multibuildpuzzle.h b/engines/nancy/action/puzzle/multibuildpuzzle.h
index 37439bd82d5..918c0e6c5d5 100644
--- a/engines/nancy/action/puzzle/multibuildpuzzle.h
+++ b/engines/nancy/action/puzzle/multibuildpuzzle.h
@@ -51,6 +51,7 @@ public:
 
 protected:
 	Common::String getRecordTypeName() const override { return "MultiBuildPuzzle"; }
+	bool isValidDrop() const;
 
 	// A single puzzle piece. Each piece is its own RenderObject.
 	// Unplaced: _drawSurface shows srcRect from primary image.


Commit: 1a3f9a14cb99760ef0d15036083b520b5a8b5140
    https://github.com/scummvm/scummvm/commit/1a3f9a14cb99760ef0d15036083b520b5a8b5140
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-05-07T03:55:39+03:00

Commit Message:
NANCY: Use getters to access single and combo player table values

There are cases where these values aren't set, so a default value
should be returned. Happens sometimes when returning a crab to Holt
Scotto in Nancy 9.

Fix #16766

Changed paths:
    engines/nancy/action/datarecords.cpp


diff --git a/engines/nancy/action/datarecords.cpp b/engines/nancy/action/datarecords.cpp
index ee01633f6e9..68fcb42da13 100644
--- a/engines/nancy/action/datarecords.cpp
+++ b/engines/nancy/action/datarecords.cpp
@@ -176,14 +176,14 @@ void SetValueCombo::execute() {
 			} else {
 				if (_indices[i] < numSingleValues) {
 					// Add a single value
-					if (playerTable->singleValues[_indices[i]] != kNoTableValue) {
-						valueToAdd = playerTable->singleValues[_indices[i]];
+					if (playerTable->getSingleValue(_indices[i]) != kNoTableValue) {
+						valueToAdd = playerTable->getSingleValue(_indices[i]);
 						valueToAdd = valueToAdd * ((float)_percentages[i] / 100.f);
 					}
 				} else {
 					// Add another combo value
-					if (playerTable->comboValues[_indices[i] - numSingleValues] != kNoTableValue) {
-						valueToAdd = playerTable->comboValues[_indices[i] - numSingleValues];
+					if (playerTable->getComboValue(_indices[i] - numSingleValues) != (float)kNoTableValue) {
+						valueToAdd = playerTable->getComboValue(_indices[i] - numSingleValues);
 						valueToAdd = valueToAdd * ((float)_percentages[i] / 100.f);
 					}
 				}


Commit: cb9177ecac780178317f79bf3f20fa94d16a58b5
    https://github.com/scummvm/scummvm/commit/cb9177ecac780178317f79bf3f20fa94d16a58b5
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-05-07T03:55:40+03:00

Commit Message:
NANCY: More work on the new inventory popup used in Nancy10+

- Implement filtering and filter tabs
- Implement the filter caption above the window
- Implement the scrollbar and paging
- Implement the close button

Changed paths:
    engines/nancy/ui/inventorypopup.cpp
    engines/nancy/ui/inventorypopup.h


diff --git a/engines/nancy/ui/inventorypopup.cpp b/engines/nancy/ui/inventorypopup.cpp
index 0ccdea94dbf..a545ad1d5ab 100644
--- a/engines/nancy/ui/inventorypopup.cpp
+++ b/engines/nancy/ui/inventorypopup.cpp
@@ -36,15 +36,9 @@ namespace UI {
 InventoryPopup::InventoryPopup() :
 		// z=12: above the viewport (6) and the taskbar (7). All Nancy
 		// 10+ taskbar popups render on top of the entire scene UI.
-		RenderObject(12),
-		_uiivData(nullptr),
-		_invData(nullptr),
-		_isOpen(false),
-		_activeFilter(kFilterAll),
-		_currentPage(0) {
-	for (uint i = 0; i < kSlotsPerPage; ++i) {
+		RenderObject(12) {
+	for (uint i = 0; i < kSlotsPerPage; ++i)
 		_slotItemIDs[i] = -1;
-	}
 }
 
 void InventoryPopup::init() {
@@ -54,28 +48,26 @@ void InventoryPopup::init() {
 	_invData = GetEngineData(INV);
 	assert(_invData);
 
-	// Popup background image (e.g. "UIInv_OVL").
 	g_nancy->_resource->loadImage(_uiivData->header.imageName, _overlayImage);
-
-	// Per-item icon sheet shared with the legacy InventoryBox.
 	g_nancy->_resource->loadImage(_invData->inventoryBoxIconsImageName, _itemIcons);
 
-	// Position the popup using the "normal-size" rects from the popup
-	// header (popup state 2).
 	Common::Rect popupRect = _uiivData->header.normalDestRect;
 	if (_uiivData->header.overlayInGameFrame) {
 		const VIEW *view = GetEngineData(VIEW);
-		if (view) {
+		if (view)
 			popupRect.translate(view->screenPosition.left, view->screenPosition.top);
-		}
 	}
 	moveTo(popupRect);
+
 	Common::Rect bounds = _screenPosition;
 	bounds.moveTo(0, 0);
 	_drawSurface.create(bounds.width(), bounds.height(), g_nancy->_graphics->getInputPixelFormat());
 
+	setActiveFilterIndex(0);
+
 	drawBackground();
 	drawFilterTabs();
+	drawFilterCaption();
 
 	setTransparent(false);
 	setVisible(false);
@@ -83,15 +75,10 @@ void InventoryPopup::init() {
 	RenderObject::init();
 }
 
-void InventoryPopup::registerGraphics() {
-	RenderObject::registerGraphics();
-}
-
 void InventoryPopup::open() {
-	if (_isOpen) {
+	if (_isVisible)
 		return;
-	}
-	_isOpen = true;
+
 	_currentPage = 0;
 
 	rebuildVisibleList();
@@ -106,10 +93,8 @@ void InventoryPopup::open() {
 }
 
 void InventoryPopup::close() {
-	if (!_isOpen) {
+	if (!_isVisible)
 		return;
-	}
-	_isOpen = false;
 
 	setVisible(false);
 
@@ -122,9 +107,9 @@ void InventoryPopup::close() {
 void InventoryPopup::refreshGrid() {
 	rebuildVisibleList();
 
-	// Re-blit the popup background; this also wipes any previous slot icons.
 	drawBackground();
 	drawFilterTabs();
+	drawFilterCaption();
 
 	for (uint i = 0; i < kSlotsPerPage; ++i) {
 		const uint listIndex = _currentPage * kSlotsPerPage + i;
@@ -143,35 +128,24 @@ void InventoryPopup::rebuildVisibleList() {
 										_invData->itemDescriptions.size());
 
 	for (uint16 id = 0; id < numItems; ++id) {
-		if (NancySceneState.hasItem(id) != g_nancy->_true) {
+		if (NancySceneState.hasItem(id) != g_nancy->_true)
 			continue;
-		}
 
 		const INV::ItemDescription &desc = _invData->itemDescriptions[id];
-		bool include;
 
-		switch (_activeFilter) {
-		case kFilterDocuments:
-			include = (desc.keepItem == 3);
-			break;
-		case kFilterUsable:
-			include = (desc.keepItem <= 2);
+		switch (_activeFilterIndex) {
+		case kFilterViewable:
+			if (desc.keepItem == 3)
+				_visibleItems.push_back(id);
 			break;
-		case kFilterSpecial1:
-		case kFilterSpecial2:
-		case kFilterSpecial3:
-			// Specialty filters
-			// TODO: Show nothing, for now
-			include = false;
+		case kFilterPortable:
+			if (desc.keepItem <= 2)
+				_visibleItems.push_back(id);
 			break;
 		case kFilterAll:
 		default:
-			include = true;
-			break;
-		}
-
-		if (include) {
 			_visibleItems.push_back(id);
+			break;
 		}
 	}
 }
@@ -179,41 +153,135 @@ void InventoryPopup::rebuildVisibleList() {
 void InventoryPopup::drawBackground() {
 	Common::Rect src = _uiivData->header.normalSrcRect;
 	_drawSurface.blitFrom(_overlayImage, src, Common::Point(0, 0));
+
+	drawCloseButton(_closeButtonHovered ? kStateHover : kStateIdle);
+
+	WidgetState sliderState = kStateIdle;
+	if (_scrollbarDragging)
+		sliderState = kStatePressed;
+	else if (_scrollbarHovered)
+		sliderState = kStateHover;
+
+	drawScrollbar(sliderState);
 }
 
-void InventoryPopup::drawFilterTabs() {
-	// Sub-rects in the chunk are stored relative to header.normalDestRect.
-	// After we translate the popup by the viewport offset, _screenPosition
-	// no longer matches that origin, so popup-local conversions must
-	// subtract chunk.normalDestRect.topLeft, not _screenPosition.
+Common::Rect InventoryPopup::computeSliderRect() const {
+	const UISliderRecord &sl = _uiivData->header.slider;
+	if (!_uiivData->header.sliderEnabled)
+		return Common::Rect();
+
+	const int trackHeight = sl.destRect.height();
+	const int thumbHeight = sl.sourceRects[0].height();
+	const int travel = MAX(0, trackHeight - thumbHeight);
+	const int thumbY = sl.destRect.top + (int)(_scrollPos * travel);
+
 	const Common::Point chunkOrigin(_uiivData->header.normalDestRect.left,
 									_uiivData->header.normalDestRect.top);
 
-	for (const auto &filter : _uiivData->filters) {
-		if (!filter.enabled || filter.button.sourceRects[0].isEmpty()) {
-			continue;
-		}
+	Common::Rect r(sl.destRect.left, thumbY,
+					sl.destRect.left + sl.sourceRects[0].width(),
+					thumbY + thumbHeight);
+	r.translate(-chunkOrigin.x, -chunkOrigin.y);
+	return r;
+}
 
-		_drawSurface.blitFrom(_overlayImage, filter.button.sourceRects[0],
-								Common::Point(filter.button.destRect.left - chunkOrigin.x,
-												filter.button.destRect.top - chunkOrigin.y));
-	}
+void InventoryPopup::drawScrollbar(WidgetState state) {
+	const UISliderRecord &sl = _uiivData->header.slider;
+	if (!_uiivData->header.sliderEnabled)
+		return;
+
+	Common::Rect spr = sl.sourceRects[state];
+	if (spr.isEmpty())
+		spr = sl.sourceRects[0];
+	if (spr.isEmpty())
+		return;
+
+	const Common::Rect thumb = computeSliderRect();
+	if (thumb.isEmpty())
+		return;
+
+	_drawSurface.blitFrom(_overlayImage, spr, Common::Point(thumb.left, thumb.top));
 }
 
-void InventoryPopup::drawSlot(uint slotIndex, int16 itemID) {
-	if (slotIndex >= _uiivData->slotDestRects.size()) {
+void InventoryPopup::updatePageFromScroll() {
+	const uint numPages = (_visibleItems.size() + kSlotsPerPage - 1) / kSlotsPerPage;
+	if (numPages <= 1) {
+		_currentPage = 0;
+		_scrollPos = 0.0f;
 		return;
 	}
 
-	if (itemID < 0 || itemID >= (int16)_invData->itemDescriptions.size()) {
-		// Empty slot — leave the popup-background pixels in place.
-		return;
+	const uint maxPage = numPages - 1;
+	uint page = (uint)(_scrollPos * maxPage + 0.5f);
+	_currentPage = MIN<uint>(page, maxPage);
+}
+
+void InventoryPopup::drawCloseButton(WidgetState state) {
+	const UIButtonRecord &btn = _uiivData->header.secondaryButton;
+	Common::Rect spr = btn.sourceRects[state];
+	const Common::Point chunkOrigin(_uiivData->header.normalDestRect.left,
+									_uiivData->header.normalDestRect.top);
+	const Common::Point dst(btn.destRect.left - chunkOrigin.x,
+							btn.destRect.top - chunkOrigin.y);
+
+	_drawSurface.blitFrom(_overlayImage, spr, dst);
+}
+
+void InventoryPopup::drawFilterTabs() {
+	for (uint i = 0; i < kNumFilters; ++i) {
+		drawFilterTab(i);
 	}
+}
+
+void InventoryPopup::drawFilterTab(uint index, bool drawHover) {
+	const Common::Point chunkOrigin(_uiivData->header.normalDestRect.left,
+									_uiivData->header.normalDestRect.top);
 
-	const INV::ItemDescription &desc = _invData->itemDescriptions[itemID];
-	if (desc.sourceRect.isEmpty()) {
+	const UIButtonSlot &filter = _uiivData->filters[index];
+	if (!filter.enabled)
+		return;
+
+	const bool isActive = (index == _activeFilterIndex);
+	uint rectIndex = isActive ? kStatePressed : kStateIdle;
+	if (drawHover)
+		rectIndex = kStateHover;
+	Common::Rect spr = filter.button.sourceRects[rectIndex];
+
+	_drawSurface.blitFrom(_overlayImage, spr,
+						  Common::Point(filter.button.destRect.left - chunkOrigin.x,
+										filter.button.destRect.top - chunkOrigin.y));
+}
+
+
+void InventoryPopup::drawFilterCaption() {
+	if (_activeFilterIndex >= _uiivData->tabCaptionSrcRects.size())
+		return;
+
+	const Common::Rect &spr = _uiivData->tabCaptionSrcRects[_activeFilterIndex];
+	const Common::Point chunkOrigin(_uiivData->header.normalDestRect.left,
+									_uiivData->header.normalDestRect.top);
+	_drawSurface.blitFrom(_overlayImage, spr,
+							Common::Point(_uiivData->tabCaptionDestRect.left - chunkOrigin.x,
+											_uiivData->tabCaptionDestRect.top - chunkOrigin.y));
+}
+
+void InventoryPopup::setActiveFilterIndex(uint index) {
+	if (index >= kNumFilters)
+		return;
+
+	_activeFilterIndex = index;
+}
+
+void InventoryPopup::drawSlot(uint slotIndex, int16 itemId) {
+	if (slotIndex >= _uiivData->slotDestRects.size())
+		return;
+
+	if (itemId < 0 || itemId >= (int16)_invData->itemDescriptions.size())
+		return;
+
+	const INV::ItemDescription &desc = _invData->itemDescriptions[itemId];
+	if (desc.sourceRect.isEmpty())
 		return;
-	}
 
 	const Common::Point chunkOrigin(_uiivData->header.normalDestRect.left,
 									_uiivData->header.normalDestRect.top);
@@ -224,25 +292,90 @@ void InventoryPopup::drawSlot(uint slotIndex, int16 itemID) {
 }
 
 void InventoryPopup::handleInput(NancyInput &input) {
-	if (!_isOpen) {
+	if (!_isVisible)
 		return;
-	}
 
-	// Bring the mouse into the chunk's coordinate system so hit-tests
-	// against the chunk's destRects work after we translated the popup
-	// by the viewport offset.
 	const Common::Point chunkMouse(
 		input.mousePos.x - _screenPosition.left + _uiivData->header.normalDestRect.left,
 		input.mousePos.y - _screenPosition.top  + _uiivData->header.normalDestRect.top);
 
+	// Scrollbar interaction takes priority while dragging.
+	const UISliderRecord &slider = _uiivData->header.slider;
+	if (_uiivData->header.sliderEnabled) {
+		const Common::Rect &track = slider.destRect;
+		// The thumb rect sits inside the track; build it in chunk coords
+		// for hit-testing against `chunkMouse` (which is also in chunk
+		// coords since we did the conversion above).
+		const int trackHeight = track.height();
+		const int thumbHeight = slider.sourceRects[0].height();
+		const int travel = MAX(0, trackHeight - thumbHeight);
+		const int thumbY = track.top + (int)(_scrollPos * travel);
+		Common::Rect thumbInChunk(track.left, thumbY,
+									track.left + slider.sourceRects[0].width(),
+									thumbY + thumbHeight);
+
+		const bool overScrollbar = thumbInChunk.contains(chunkMouse);
+
+		if (_scrollbarDragging) {
+			g_nancy->_cursor->setCursorType(CursorManager::kHotspotArrow);
+
+			const int newThumbTop = chunkMouse.y - _scrollbarGrabOffset;
+			const int clamped = CLIP<int>(newThumbTop, track.top, track.top + travel);
+			_scrollPos = travel > 0 ? (float)(clamped - track.top) / (float)travel : 0.0f;
+			updatePageFromScroll();
+			refreshGrid();
+
+			if (input.input & NancyInput::kLeftMouseButtonUp) {
+				_scrollbarDragging = false;
+				drawScrollbar(overScrollbar ? kStateHover : kStateIdle);
+				_needsRedraw = true;
+			}
+			input.eatMouseInput();
+			return;
+		}
+
+		if (overScrollbar != _scrollbarHovered) {
+			_scrollbarHovered = overScrollbar;
+			drawScrollbar(overScrollbar ? kStateHover : kStateIdle);
+			_needsRedraw = true;
+		}
+		if (overScrollbar) {
+			g_nancy->_cursor->setCursorType(CursorManager::kHotspotArrow);
+			if (slider.isDraggable && (input.input & NancyInput::kLeftMouseButtonDown)) {
+				_scrollbarDragging = true;
+				_scrollbarGrabOffset = chunkMouse.y - thumbY;
+				drawScrollbar(kStatePressed);
+				_needsRedraw = true;
+				input.eatMouseInput();
+				return;
+			}
+		}
+	}
+
+	if (_uiivData->header.secondaryButtonEnabled) {
+		const Common::Rect &closeRect = _uiivData->header.secondaryButton.destRect;
+		const bool overClose = closeRect.contains(chunkMouse);
+		if (overClose != _closeButtonHovered) {
+			_closeButtonHovered = overClose;
+			drawCloseButton(overClose ? kStateHover : kStateIdle);
+			_needsRedraw = true;
+		}
+		if (overClose) {
+			g_nancy->_cursor->setCursorType(CursorManager::kHotspotArrow);
+			if (input.input & NancyInput::kLeftMouseButtonUp) {
+				input.eatMouseInput();
+				close();
+				return;
+			}
+		}
+	}
+
 	int newHovered = -1;
 	for (uint i = 0; i < kSlotsPerPage; ++i) {
-		if (i >= _uiivData->slotDestRects.size()) {
+		if (i >= _uiivData->slotDestRects.size())
 			break;
-		}
-		if (_slotItemIDs[i] < 0) {
+		if (_slotItemIDs[i] < 0)
 			continue;
-		}
 		if (_uiivData->slotDestRects[i].contains(chunkMouse)) {
 			newHovered = (int)i;
 			break;
@@ -266,20 +399,37 @@ void InventoryPopup::handleInput(NancyInput &input) {
 		}
 	}
 
-	// Filter tabs: clicking switches the active filter.
-	for (const auto &filter : _uiivData->filters) {
-		if (!filter.enabled) {
-			continue;
+	bool wasHovered = _filterHovered;
+	_filterHovered = false;
+
+	for (uint i = 0; i < kNumFilters; ++i) {
+		const UIButtonSlot &filter = _uiivData->filters[i];
+		if (filter.button.destRect.contains(chunkMouse)) {
+			_filterHovered = true;
+			break;
 		}
+	}
+
+	for (uint i = 0; i < kNumFilters; ++i) {
+		const UIButtonSlot &filter = _uiivData->filters[i];
+		if (!filter.enabled)
+			continue;
+
 		if (!filter.button.destRect.contains(chunkMouse)) {
+			if (_filterHovered || wasHovered)
+				drawFilterTab(i);
 			continue;
 		}
 
 		g_nancy->_cursor->setCursorType(CursorManager::kHotspotArrow);
 
+		drawFilterTab(i, true);
+
 		if (input.input & NancyInput::kLeftMouseButtonUp) {
-			_activeFilter = static_cast<FilterID>(filter.id);
+			setActiveFilterIndex(i);
 			_currentPage = 0;
+			_scrollPos = 0.0f;
+			_scrollbarDragging = false;
 			refreshGrid();
 			input.eatMouseInput();
 			return;
@@ -287,11 +437,13 @@ void InventoryPopup::handleInput(NancyInput &input) {
 		break;
 	}
 
+	if (_filterHovered || wasHovered)
+		_needsRedraw = true;
+
 	// While the popup is open, swallow clicks that fall on the popup so
 	// the underlying scene/viewport doesn't react.
-	if (_screenPosition.contains(input.mousePos)) {
+	if (_screenPosition.contains(input.mousePos))
 		input.eatMouseInput();
-	}
 }
 
 } // End of namespace UI
diff --git a/engines/nancy/ui/inventorypopup.h b/engines/nancy/ui/inventorypopup.h
index a2b340640aa..97c7193c185 100644
--- a/engines/nancy/ui/inventorypopup.h
+++ b/engines/nancy/ui/inventorypopup.h
@@ -45,50 +45,66 @@ public:
 	~InventoryPopup() override = default;
 
 	void init() override;
-	void registerGraphics() override;
 	void handleInput(NancyInput &input);
 
-	bool isOpen() const { return _isOpen; }
+	bool isOpen() const { return _isVisible; }
 	void open();
 	void close();
 
 	// The taskbar's inventory button toggles the popup; convenience helper.
-	void toggle() { if (_isOpen) close(); else open(); }
+	void toggle() { if (_isVisible) close(); else open(); }
 
 	// Re-render the slot grid. Called by Scene whenever the inventory
 	// contents change while the popup is open.
 	void refreshGrid();
 
-	enum FilterID {
-		kFilterAll       = 0x64, // default branch — every owned item
-		kFilterDocuments = 0x65, // keepItem == 3
-		kFilterUsable    = 0x66, // keepItem == 0/1/2
-		kFilterSpecial1  = 0x67,
-		kFilterSpecial2  = 0x68,
-		kFilterSpecial3  = 0x69
+	enum FilterType {
+		kFilterAll       = 0,
+		kFilterViewable  = 1,
+		kFilterPortable  = 2,
+	};
+
+	enum WidgetState {
+		kStatePressed = 0,
+		kStateHover = 1,
+		kStateIdle = 2,
+		kStateDisabled = 3
 	};
 
 private:
 	static const uint kSlotsPerPage = 16;
-	static const uint kNumFilters = 6;
+	static const uint kNumFilters = 3;
 
 	void drawBackground();
-	void drawSlot(uint slotIndex, int16 itemID);
+	void drawSlot(uint slotIndex, int16 itemId);
 	void drawFilterTabs();
+	void drawFilterTab(uint index, bool drawHover = false);
+	void drawFilterCaption();
+	void drawCloseButton(WidgetState state);
+	void drawScrollbar(WidgetState state);
 	void rebuildVisibleList();
+	void setActiveFilterIndex(uint index);
+
+	// Apply the current scrollbar position to the page index, clamping
+	// to the number of pages required by the active filter.
+	void updatePageFromScroll();
 
-	const UIIV *_uiivData;
-	const INV *_invData;
+	// Returns the on-popup-surface bounding rect of the slider thumb at
+	// the current scroll position (in popup-local coords).
+	Common::Rect computeSliderRect() const;
 
-	Graphics::ManagedSurface _overlayImage;  // popup background image
-	Graphics::ManagedSurface _itemIcons;     // per-item icon sheet
+	const UIIV *_uiivData = nullptr;
+	const INV *_invData = nullptr;
 
-	bool _isOpen;
+	Graphics::ManagedSurface _overlayImage;     // popup background image
+	Graphics::ManagedSurface _itemIcons;        // per-item icon sheet
 
-	FilterID _activeFilter;
+	bool _closeButtonHovered = false;
+
+	uint _activeFilterIndex = 0;
 
 	// Page index within the active filter (0-based).
-	uint _currentPage;
+	uint _currentPage = 0;
 
 	// Items the player owns that match the active filter, in inventory
 	// order. The on-screen grid is a 16-item window into this array.
@@ -96,6 +112,14 @@ private:
 
 	// Item ID currently shown in each of the 16 slots (-1 if empty).
 	int16 _slotItemIDs[kSlotsPerPage];
+
+	// Slider state. Driven by header.slider.
+	float _scrollPos = 0.0f;        // [0, 1]: 0 = top page, 1 = bottom page
+	bool _scrollbarDragging = false;
+	bool _scrollbarHovered = false;
+	int _scrollbarGrabOffset = 0;   // mouse-y minus thumb-top at click time
+
+	bool _filterHovered = false;
 };
 
 } // End of namespace UI




More information about the Scummvm-git-logs mailing list