xwords/xwords4/common/boarddrw.c
eehouse d263b47a56 lots of changes, still rough, to allow zooming in and out on board.
Includes ability to scroll horizontally by dragging.  Board init API
changes to specify board width and max height instead of cell
dimensions, so now board owns task of picking cell size.  If the
number of cells does not evenly divide into a board dimension then the
extra pixels are distributed among some columns/rows in a way that
still allows bitblit-based scroll implementations (though horizontal
scrolling at this point always does an invalAll()).  Not yet tested
with overlapping tray.  And still need to allow rows to be taller than
cols are wide if platform code has given the space.  Stream format
changes with this checkin.
2010-04-08 04:14:14 +00:00

579 lines
19 KiB
C

/* -*-mode: C; fill-column: 78; compile-command: "cd ../linux && make MEMDEBUG=TRUE"; -*- */
/*
* Copyright 1997 - 2009 by Eric House (xwords@eehouse.org). All rights
* reserved.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*/
/* Re: boards that can't fit on the screen. Let's have an assumption, that
* the tray is always either below the board or overlapping its bottom. There
* is never any board visible below the tray. But it's possible to have a
* board small enough that scrolling is necessary even with the tray hidden.
*
* Currently we don't specify the board bounds. We give top,left and the size
* of cells, and the board figures out the bounds. That's probably a mistake.
* Better to give bounds, and maybe a min scale, and let it figure out how
* many cells can be visible. Could it also decide if the tray should overlap
* or be below? Some platforms have to own that decision since the tray is
* narrower than the board. So give them separate bounds-setting functions,
* and let the board code figure out if they overlap.
*
* Problem: the board size must always be a multiple of the scale. The
* platform-specific code has an easy time doing that math. The board can't:
* it'd have to take bounds, then spit them back out slightly modified. It'd
* also have to refuse to work (maybe just assert) if asked to take bounds
* before it had a min_scale.
*
* Another way of looking at it closer to the current: the board's position
* and the tray's bounds determine the board's bounds. If the board's vScale
* times the number of rows places its would-be bottom at or above the bottom
* of the tray, then it's potentially visible. If its would-be bottom is
* above the top of the tray, no scrolling is needed. But if it's below the
* tray entirely then scrolling will happen even with the tray hidden. As
* above, we assume the board never appears below the tray.
*/
#include "comtypes.h"
#include "board.h"
#include "scorebdp.h"
#include "game.h"
#include "server.h"
#include "comms.h" /* for CHANNEL_NONE */
#include "dictnry.h"
#include "draw.h"
#include "engine.h"
#include "util.h"
#include "mempool.h" /* debug only */
#include "memstream.h"
#include "strutils.h"
#include "LocalizedStrIncludes.h"
#include "boardp.h"
#include "dragdrpp.h"
#include "dbgutil.h"
#ifdef CPLUS
extern "C" {
#endif
static XP_Bool drawCell( BoardCtxt* board, XP_U16 col, XP_U16 row,
XP_Bool skipBlanks );
static void drawBoard( BoardCtxt* board );
static void scrollIfCan( BoardCtxt* board );
static XP_Bool cellFocused( const BoardCtxt* board, XP_U16 col, XP_U16 row );
static void drawTradeWindowIf( BoardCtxt* board );
#ifdef XWFEATURE_SEARCHLIMIT
static HintAtts figureHintAtts( BoardCtxt* board, XP_U16 col, XP_U16 row );
#else
# define figureHintAtts(b,c,r) HINT_BORDER_NONE
#endif
#ifdef POINTER_SUPPORT
static void drawDragTileIf( BoardCtxt* board );
#endif
#ifdef KEYBOARD_NAV
#ifdef PERIMETER_FOCUS
static void
invalOldPerimeter( BoardCtxt* board )
{
/* We need to inval the center of the row that's moving into the center
from a border (at which point it got borders drawn on it.) */
XP_S16 diff = board->yOffset - board->prevYScrollOffset;
XP_U16 firstRow, lastRow;
XP_ASSERT( diff != 0 );
if ( diff < 0 ) {
/* moving up; inval row previously on bottom */
firstRow = board->yOffset + 1;
lastRow = board->prevYScrollOffset;
} else {
XP_U16 nVisible = board->lastVisibleRow - board->yOffset + 1;
lastRow = board->prevYScrollOffset + nVisible - 1;
firstRow = lastRow - diff + 1;
}
XP_ASSERT( firstRow <= lastRow );
while ( firstRow <= lastRow ) {
board->redrawFlags[firstRow] |= ~0;
++firstRow;
}
} /* invalOldPerimeter */
#endif
#endif
/* if any of a blank's neighbors is invalid, so must the blank become (since
* they share a border and drawing the neighbor will redraw the blank's border
* too) We'll want to redraw only those blanks that are themselves already
* invalid *OR* that become invalid this way, and so we'll build a new
* BlankQueue of them and replace the old.
*
* I'm not sure what happens if two blanks are neighbors.
*/
#define INVAL_BIT_SET(b,c,r) (((b)->redrawFlags[(r)] & (1 <<(c))) != 0)
static void
invalBlanksWithNeighbors( BoardCtxt* board, BlankQueue* bqp )
{
XP_U16 i;
XP_U16 lastCol, lastRow;
BlankQueue invalBlanks;
XP_U16 nInvalBlanks = 0;
lastCol = model_numCols(board->model) - 1;
lastRow = model_numRows(board->model) - 1;
for ( i = 0; i < bqp->nBlanks; ++i ) {
XP_U16 modelCol = bqp->col[i];
XP_U16 modelRow = bqp->row[i];
XP_U16 col, row;
flipIf( board, modelCol, modelRow, &col, &row );
if ( INVAL_BIT_SET( board, col, row )
|| (col > 0 && INVAL_BIT_SET( board, col-1, row ))
|| (col < lastCol && INVAL_BIT_SET( board, col+1, row ))
|| (row > 0 && INVAL_BIT_SET( board, col, row-1 ))
|| (row < lastRow && INVAL_BIT_SET( board, col, row+1 )) ) {
invalCell( board, col, row );
invalBlanks.col[nInvalBlanks] = (XP_U8)col;
invalBlanks.row[nInvalBlanks] = (XP_U8)row;
++nInvalBlanks;
}
}
invalBlanks.nBlanks = nInvalBlanks;
XP_MEMCPY( bqp, &invalBlanks, sizeof(*bqp) );
} /* invalBlanksWithNeighbors */
#ifdef XWFEATURE_SEARCHLIMIT
static HintAtts
figureHintAtts( BoardCtxt* board, XP_U16 col, XP_U16 row )
{
HintAtts result = HINT_BORDER_NONE;
/* while lets us break to exit... */
while ( board->trayVisState == TRAY_REVEALED
&& !board->gi->hintsNotAllowed
&& board->gi->allowHintRect ) {
BdHintLimits limits;
if ( dragDropGetHintLimits( board, &limits ) ) {
/* do nothing */
} else if ( board->selInfo->hasHintRect ) {
limits = board->selInfo->limits;
} else {
break;
}
if ( col < limits.left ) break;
if ( row < limits.top ) break;
if ( col > limits.right ) break;
if ( row > limits.bottom ) break;
if ( col == limits.left ) {
result |= HINT_BORDER_LEFT;
}
if ( col == limits.right ) {
result |= HINT_BORDER_RIGHT;
}
if ( row == limits.top) {
result |= HINT_BORDER_TOP;
}
if ( row == limits.bottom ) {
result |= HINT_BORDER_BOTTOM;
}
#ifndef XWFEATURE_SEARCHLIMIT_DOCENTERS
if ( result == HINT_BORDER_NONE ) {
result = HINT_BORDER_CENTER;
}
#endif
break;
}
return result;
} /* figureHintAtts */
#endif
static XP_Bool
rectContainsRect( XP_Rect* rect1, XP_Rect* rect2 )
{
return ( rect1->top <= rect2->top
&& rect1->left <= rect2->left
&& rect1->top + rect1->height >= rect2->top + rect2->height
&& rect1->left + rect1->width >= rect2->left + rect2->width );
} /* rectContainsRect */
static void
makeMiniWindowForTrade( BoardCtxt* board )
{
const XP_UCHAR* text;
text = draw_getMiniWText( board->draw, INTRADE_MW_TEXT );
makeMiniWindowForText( board, text, MINIWINDOW_TRADING );
} /* makeMiniWindowForTrade */
static void
drawBoard( BoardCtxt* board )
{
if ( board->needsDrawing
&& draw_boardBegin( board->draw,
&board->boardBounds,
dfsFor( board, OBJ_BOARD ) ) ) {
XP_Bool allDrawn = XP_TRUE;
XP_S16 i;
XP_S16 col, row, nVisCols;
ModelCtxt* model = board->model;
BoardArrow const* arrow = NULL;
BlankQueue bq;
scrollIfCan( board ); /* this must happen before we count blanks
since it invalidates squares */
/* This is freaking expensive!!!! PENDING FIXME Can't we start from
what's invalid rather than scanning the entire model every time
somebody dirties a single cell? */
model_listPlacedBlanks( model, board->selPlayer,
board->trayVisState == TRAY_REVEALED, &bq );
dragDropAppendBlank( board, &bq );
invalBlanksWithNeighbors( board, &bq );
/* figure out now, before clearing inval bits, if we'll need to draw
the arrow later */
if ( board->trayVisState == TRAY_REVEALED ) {
BoardArrow const* tmpArrow = &board->selInfo->boardArrow;
if ( tmpArrow->visible ) {
XP_U16 col = tmpArrow->col;
XP_U16 row = tmpArrow->row;
if ( INVAL_BIT_SET( board, col, row )
&& !cellOccupied( board, col, row, XP_TRUE ) ) {
arrow = tmpArrow;
}
}
}
nVisCols = model_numCols( model ) - board->zoomCount;
for ( row = board->yOffset; row <= board->lastVisibleRow; ++row ) {
XP_U16 rowFlags = board->redrawFlags[row];
if ( rowFlags != 0 ) {
XP_U16 failedBits = 0;
for ( col = 0; col < nVisCols; ++col ) {
XP_U16 colMask = 1 << (col + board->xOffset);
if ( 0 != (rowFlags & colMask) ) {
if ( !drawCell( board, col + board->xOffset,
row, XP_TRUE )) {
failedBits |= colMask;
allDrawn = XP_FALSE;
}
}
}
board->redrawFlags[row] = failedBits;
}
}
/* draw the blanks we skipped before */
for ( i = 0; i < bq.nBlanks; ++i ) {
if ( !drawCell( board, bq.col[i], bq.row[i], XP_FALSE ) ) {
allDrawn = XP_FALSE;
}
}
if ( !!arrow ) {
XP_U16 col = arrow->col;
XP_U16 row = arrow->row;
XP_Rect arrowRect;
if ( getCellRect( board, col, row, &arrowRect ) ) {
XWBonusType bonus;
HintAtts hintAtts;
CellFlags flags = CELL_NONE;
bonus = util_getSquareBonus( board->util, model, col, row );
hintAtts = figureHintAtts( board, col, row );
#ifdef KEYBOARD_NAV
if ( cellFocused( board, col, row ) ) {
flags |= CELL_ISCURSOR;
}
#endif
draw_drawBoardArrow( board->draw, &arrowRect, bonus,
arrow->vert, hintAtts, flags );
}
}
/* I doubt the two of these can happen at the same time */
drawTradeWindowIf( board );
#ifdef POINTER_SUPPORT
drawDragTileIf( board );
#endif
draw_objFinished( board->draw, OBJ_BOARD, &board->boardBounds,
dfsFor( board, OBJ_BOARD ) );
board->needsDrawing = !allDrawn;
}
} /* drawBoard */
static XP_Bool
drawCell( BoardCtxt* board, XP_U16 col, XP_U16 row, XP_Bool skipBlanks )
{
XP_Bool success = XP_TRUE;
XP_Rect cellRect;
Tile tile;
XP_Bool isBlank, isEmpty, recent, pending = XP_FALSE;
XWBonusType bonus;
ModelCtxt* model = board->model;
DictionaryCtxt* dict = model_getDictionary( model );
XP_U16 modelCol, modelRow;
if ( dict != NULL && getCellRect( board, col, row, &cellRect ) ) {
/* We want to invert EITHER the current pending tiles OR the most recent
* move. So if the tray is visible AND there are tiles missing from it,
* show them. Otherwise show the most recent move.
*/
XP_U16 selPlayer = board->selPlayer;
XP_U16 curCount = model_getCurrentMoveCount( model, selPlayer );
XP_Bool showPending = board->trayVisState == TRAY_REVEALED
&& curCount > 0;
flipIf( board, col, row, &modelCol, &modelRow );
/* This 'while' is only here so I can 'break' below */
while ( board->trayVisState == TRAY_HIDDEN ||
!rectContainsRect( &board->trayBounds, &cellRect ) ) {
XP_UCHAR ch[4] = {'\0'};
XP_S16 owner = -1;
XP_Bool invert = XP_FALSE;
XP_Bitmaps bitmaps;
XP_Bitmaps* bptr = NULL;
const XP_UCHAR* textP = NULL;
HintAtts hintAtts;
CellFlags flags = CELL_NONE;
XP_Bool isOrigin;
isEmpty = !model_getTile( model, modelCol, modelRow, showPending,
selPlayer, &tile, &isBlank,
&pending, &recent );
if ( dragDropIsBeingDragged( board, col, row, &isOrigin ) ) {
flags |= isOrigin? CELL_DRAGSRC : CELL_DRAGCUR;
if ( isEmpty && !isOrigin ) {
dragDropTileInfo( board, &tile, &isBlank );
isEmpty = XP_FALSE;
}
showPending = pending = XP_TRUE;
}
if ( isEmpty ) {
isBlank = XP_FALSE;
flags |= CELL_ISEMPTY;
} else if ( isBlank && skipBlanks ) {
break;
} else {
if ( board->showColors ) {
owner = (XP_S16)model_getCellOwner( model, modelCol,
modelRow );
}
invert = showPending? pending : recent;
if ( board->showCellValues ) {
Tile valTile = isBlank? dict_getBlankTile( dict ) : tile;
XP_U16 val = dict_getTileValue( dict, valTile );
XP_SNPRINTF( ch, VSIZE(ch), "%d", val );
textP = ch;
} else {
if ( dict_faceIsBitmap( dict, tile ) ) {
dict_getFaceBitmaps( dict, tile, &bitmaps );
bptr = &bitmaps;
}
textP = dict_getTileString( dict, tile );
}
}
bonus = util_getSquareBonus( board->util, model, col, row );
hintAtts = figureHintAtts( board, col, row );
if ( (col==board->star_row) && (row==board->star_row) ) {
flags |= CELL_ISSTAR;
}
if ( invert ) {
flags |= CELL_HIGHLIGHT;
}
if ( isBlank ) {
flags |= CELL_ISBLANK;
}
#ifdef KEYBOARD_NAV
if ( cellFocused( board, col, row ) ) {
flags |= CELL_ISCURSOR;
}
#endif
success = draw_drawCell( board->draw, &cellRect, textP, bptr,
tile, owner, bonus, hintAtts, flags );
break;
}
}
return success;
} /* drawCell */
#ifdef KEYBOARD_NAV
DrawFocusState
dfsFor( BoardCtxt* board, BoardObjectType obj )
{
DrawFocusState dfs;
if ( (board->focussed == obj) && !board->hideFocus ) {
if ( board->focusHasDived ) {
dfs = DFS_DIVED;
} else {
dfs = DFS_TOP;
}
} else {
dfs = DFS_NONE;
}
return dfs;
} /* dfsFor */
static XP_Bool
cellFocused( const BoardCtxt* board, XP_U16 col, XP_U16 row )
{
XP_Bool focussed = XP_FALSE;
if ( (board->focussed == OBJ_BOARD) && !board->hideFocus ) {
if ( board->focusHasDived ) {
if ( (col == board->selInfo->bdCursor.col)
&& (row == board->selInfo->bdCursor.row) ) {
focussed = XP_TRUE;
}
} else {
#ifdef PERIMETER_FOCUS
focussed = (col == 0)
|| (col == model_numCols(board->model) - 1)
|| (row == board->yOffset)
|| (row == board->lastVisibleRow);
#else
focussed = XP_TRUE;
#endif
}
}
return focussed;
} /* cellFocused */
#endif
#ifdef POINTER_SUPPORT
static void
drawDragTileIf( BoardCtxt* board )
{
if ( dragDropInProgress( board ) ) {
XP_U16 col, row;
if ( dragDropGetBoardTile( board, &col, &row ) ) {
XP_Rect rect;
Tile tile;
XP_Bool isBlank;
const XP_UCHAR* face;
XP_Bitmaps bitmaps;
XP_S16 value;
CellFlags flags;
getDragCellRect( board, col, row, &rect );
dragDropTileInfo( board, &tile, &isBlank );
face = getTileDrawInfo( board, tile, isBlank, &bitmaps,
&value );
flags = CELL_DRAGCUR;
if ( isBlank ) {
flags |= CELL_ISBLANK;
}
if ( board->hideValsInTray && !board->showCellValues ) {
flags |= CELL_VALHIDDEN;
}
draw_drawTileMidDrag( board->draw, &rect, face,
bitmaps.nBitmaps > 0 ? &bitmaps : NULL,
value, board->selPlayer, flags );
}
}
} /* drawDragTileIf */
#endif
static void
scrollIfCan( BoardCtxt* board )
{
if ( board->yOffset != board->prevYScrollOffset ) {
XP_Rect scrollR = board->boardBounds;
XP_Bool scrolled;
XP_S16 dist;
#ifdef PERIMETER_FOCUS
if ( (board->focussed == OBJ_BOARD)
&& !board->focusHasDived
&& !board->hideFocus ) {
invalOldPerimeter( board );
}
#endif
invalSelTradeWindow( board );
dist = (board->yOffset - board->prevYScrollOffset)
* board->boardVScale;
scrolled = draw_vertScrollBoard( board->draw, &scrollR, dist,
dfsFor( board, OBJ_BOARD ) );
if ( scrolled ) {
/* inval the rows that have been scrolled into view. I'm cheating
making the client figure the inval rect, but Palm's the first
client and it does it so well.... */
invalCellsUnderRect( board, &scrollR );
} else {
board_invalAll( board );
}
board->prevYScrollOffset = board->yOffset;
}
} /* scrollIfCan */
static void
drawTradeWindowIf( BoardCtxt* board )
{
if ( board->tradingMiniWindowInvalid &&
TRADE_IN_PROGRESS(board) && board->trayVisState == TRAY_REVEALED ) {
MiniWindowStuff* stuff;
makeMiniWindowForTrade( board );
stuff = &board->miniWindowStuff[MINIWINDOW_TRADING];
draw_drawMiniWindow( board->draw, stuff->text,
&stuff->rect, (void**)NULL );
board->tradingMiniWindowInvalid = XP_FALSE;
}
} /* drawTradeWindowIf */
XP_Bool
board_draw( BoardCtxt* board )
{
if ( board->boardBounds.width > 0 ) {
drawScoreBoard( board );
drawTray( board );
drawBoard( board );
}
return !board->needsDrawing;
} /* board_draw */
#ifdef CPLUS
}
#endif