/* ** Routines used to render HTML onto the screen for the Tk HTML widget. ** ** Copyright (C) 1997-2000 D. Richard Hipp ** ** This library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Library General Public ** License as published by the Free Software Foundation; either ** version 2 of the License, or (at your option) any later version. ** ** This library 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 ** Library General Public License for more details. ** ** You should have received a copy of the GNU Library General Public ** License along with this library; if not, write to the ** Free Software Foundation, Inc., 59 Temple Place - Suite 330, ** Boston, MA 02111-1307, USA. ** ** Author contact information: ** drh@acm.org ** http://www.hwaci.com/drh/ */ #include #include #include #include "htmldraw.h" /* #ifdef USE_TK_STUBS # include #endif */ #define USE_TK_DRAWCHARS 1 /* ** Allocate a new HtmlBlock structure. */ static HtmlBlock *AllocBlock(void){ HtmlBlock *pNew; pNew = HtmlAlloc( sizeof(HtmlBlock) ); if( pNew ){ memset(pNew, 0, sizeof(*pNew)); pNew->base.type = Html_Block; } return pNew; } /* ** Free an HtmlBlock structure. Assume that it is already unlinked ** from the element list and the block list. */ static void FreeBlock(HtmlBlock *pBlock){ if( pBlock ){ if( pBlock->z ){ HtmlFree(pBlock->z); } HtmlFree(pBlock); } } /* ** Destroy the given Block after first unlinking it from the ** element list. Note that this unlinks the block from the ** element list only -- not from the block list. */ static void UnlinkAndFreeBlock(HtmlWidget *htmlPtr, HtmlBlock *pBlock){ if( pBlock->base.pNext ){ pBlock->base.pNext->base.pPrev = pBlock->base.pPrev; TestPoint(0); }else{ htmlPtr->pLast = pBlock->base.pPrev; TestPoint(0); } if( pBlock->base.pPrev ){ pBlock->base.pPrev->base.pNext = pBlock->base.pNext; TestPoint(0); }else{ htmlPtr->pFirst = pBlock->base.pNext; TestPoint(0); } pBlock->base.pPrev = pBlock->base.pNext = 0; FreeBlock(pBlock); } /* ** Append a block to the block list and insert the block into the ** element list immediately prior to the element given. */ static void AppendBlock( HtmlWidget *htmlPtr, /* The HTML widget */ HtmlElement *pToken, /* The token that comes after pBlock */ HtmlBlock *pBlock /* The block to be appended */ ){ pBlock->base.pPrev = pToken->base.pPrev; pBlock->base.pNext = pToken; pBlock->pPrev = htmlPtr->lastBlock; pBlock->pNext = 0; if( htmlPtr->lastBlock ){ htmlPtr->lastBlock->pNext = pBlock; TestPoint(0); }else{ htmlPtr->firstBlock = pBlock; TestPoint(0); } htmlPtr->lastBlock = pBlock; if( pToken->base.pPrev ){ pToken->base.pPrev->base.pNext = (HtmlElement*)pBlock; TestPoint(0); }else{ htmlPtr->pFirst = (HtmlElement*)pBlock; TestPoint(0); } pToken->base.pPrev = (HtmlElement*)pBlock; } /* ** Print an ordered list index into the given buffer. Use numbering ** like this: ** ** A B C ... Y Z AA BB CC ... ZZ ** ** Revert to decimal for indices greater than 52. */ static void GetLetterIndex(char *zBuf, int index, int isUpper){ int seed; if( index<1 || index>52 ){ sprintf(zBuf,"%d",index); TestPoint(0); return; } if( isUpper ){ seed = 'A'; TestPoint(0); }else{ seed = 'a'; TestPoint(0); } index--; if( index<26 ){ zBuf[0] = seed + index; zBuf[1] = 0; TestPoint(0); }else{ index -= 26; zBuf[0] = seed + index; zBuf[1] = seed + index; zBuf[2] = 0; TestPoint(0); } strcat(zBuf,"."); } /* ** Print an ordered list index into the given buffer. Use roman ** numerals. For indices greater than a few thousand, revert to ** decimal. */ static void GetRomanIndex(char *zBuf, int index, int isUpper){ int i = 0; int j; static struct { int value; char *name; } values[] = { { 1000, "m" }, { 999, "im" }, { 990, "xm" }, { 900, "cm" }, { 500, "d" }, { 499, "id" }, { 490, "xd" }, { 400, "cd" }, { 100, "c" }, { 99, "ic" }, { 90, "xc" }, { 50, "l" }, { 49, "il" }, { 40, "xl" }, { 10, "x" }, { 9, "ix" }, { 5, "v" }, { 4, "iv" }, { 1, "i" }, }; if( index<1 || index>=5000 ){ sprintf(zBuf,"%d",index); TestPoint(0); return; } for(j=0; index>0 && j= values[j].value ){ for(k=0; values[j].name[k]; k++){ zBuf[i++] = values[j].name[k]; TestPoint(0); } index -= values[j].value; TestPoint(0); } } zBuf[i] = 0; if( isUpper ){ for(i=0; zBuf[i]; i++){ zBuf[i] += 'A' - 'a'; TestPoint(0); } }else{ TestPoint(0); } strcat(zBuf,"."); } /* Draw the selection background for the given block */ static void DrawSelectionBackground( HtmlWidget *htmlPtr, /* The HTML widget */ HtmlBlock *pBlock, /* The block whose background is drawn */ Drawable drawable, /* Draw the background on this drawable */ int x, int y /* Virtual coords of top-left of drawable */ ){ int xLeft, xRight; /* Left and right bounds of box to draw */ int yTop, yBottom; /* Top and bottom of box */ HtmlElement *p = 0; /* First element of the block */ Tk_Font font; /* Font */ GC gc; /* GC for drawing */ XRectangle xrec; /* Size of a filled rectangle to be drawn */ if( pBlock==0 || (pBlock->base.flags & HTML_Selected)==0 ){ TestPoint(0); return; } xLeft = pBlock->left - x; if( pBlock==htmlPtr->pSelStartBlock && htmlPtr->selStartIndex>0 ){ if( htmlPtr->selStartIndex >= pBlock->n ){ TestPoint(0); return; } p = pBlock->base.pNext; font = HtmlGetFont(htmlPtr, p->base.style.font); if( font==0 ) return; if( p->base.type==Html_Text ){ xLeft = p->text.x - x + Tk_TextWidth(font, pBlock->z, htmlPtr->selStartIndex); } } xRight = pBlock->right - x; if( pBlock==htmlPtr->pSelEndBlock && htmlPtr->selEndIndexn ){ if( p==0 ){ p = pBlock->base.pNext; font = HtmlGetFont(htmlPtr, p->base.style.font); if( font==0 ) return; } if( p->base.type==Html_Text ){ xRight = p->text.x - x + Tk_TextWidth(font, pBlock->z, htmlPtr->selEndIndex); } } yTop = pBlock->top - y; yBottom = pBlock->bottom - y; gc = HtmlGetGC(htmlPtr, COLOR_Selection, FONT_Any); xrec.x = xLeft; xrec.y = yTop; xrec.width = xRight - xLeft; xrec.height = yBottom - yTop; XFillRectangles(htmlPtr->display, drawable, gc, &xrec, 1); } /* ** Draw a rectangle. The rectangle will have a 3-D appearance if ** flat==0 and a flat appearance if flat==1. ** ** We don't use Tk_Draw3DRectangle() because it doesn't work well ** when the background color is close to pure white or pure black. */ static void HtmlDrawRect( HtmlWidget *htmlPtr, /* The HTML widget */ Drawable drawable, /* Draw it here */ HtmlElement *src, /* Element associated with drawing */ int x, int y, int w, int h, /* Coordinates of the rectangle */ int depth, /* Width of the relief, or the flat line */ int relief /* The relief. TK_RELIEF_FLAT omits 3d */ ){ if( depth>0 ){ int i; GC gcLight, gcDark; XRectangle xrec[1]; if( relief!=TK_RELIEF_FLAT ){ int iLight, iDark; iLight = HtmlGetLightShadowColor(htmlPtr, src->base.style.bgcolor); gcLight = HtmlGetGC(htmlPtr, iLight, FONT_Any); iDark = HtmlGetDarkShadowColor(htmlPtr, src->base.style.bgcolor); gcDark = HtmlGetGC(htmlPtr, iDark, FONT_Any); if( relief==TK_RELIEF_SUNKEN ){ GC gcTemp = gcLight; gcLight = gcDark; gcDark = gcTemp; } }else{ gcLight = HtmlGetGC(htmlPtr, src->base.style.color, FONT_Any); gcDark = gcLight; } xrec[0].x = x; xrec[0].y = y; xrec[0].width = depth; xrec[0].height = h; XFillRectangles(htmlPtr->display, drawable, gcLight, xrec, 1); xrec[0].x = x+w-depth; XFillRectangles(htmlPtr->display, drawable, gcDark, xrec, 1); for(i=0; idisplay, drawable, gcLight, x+i, y+i, x+w-i-1, y+i); XDrawLine(htmlPtr->display, drawable, gcDark, x+i, y+h-i-1, x+w-i-1, y+h-i-1); } } if( h>depth*2 && w>depth*2 ){ GC gcBg; XRectangle xrec[1]; gcBg = HtmlGetGC(htmlPtr, src->base.style.bgcolor, FONT_Any); xrec[0].x = x + depth; xrec[0].y = y + depth; xrec[0].width = w - depth*2; xrec[0].height = h - depth*2; XFillRectangles(htmlPtr->display, drawable, gcBg, xrec, 1); } } /* ** Display a single HtmlBlock. This is where all the drawing ** happens. */ void HtmlBlockDraw( HtmlWidget *htmlPtr, /* The main HTML widget */ HtmlBlock *pBlock, /* Block which needs to be drawn */ Drawable drawable, /* Draw the line on this */ int drawableLeft, /* Virtual coordinate of left edge of drawable */ int drawableTop, /* Virtual coordinate of top edge of drawable */ int drawableWidth, /* Width of the drawable */ int drawableHeight /* Height of the drawable */ ){ Tk_Font font; /* Font to use to render text */ GC gc; /* A graphics context */ HtmlElement *src; /* HtmlElement holding style information */ HtmlElement *pTable; /* The table (when drawing part of a table) */ int x, y; /* Where to draw */ if( pBlock==0 ){ TestPoint(0); return; } src = pBlock->base.pNext; while( src && (src->base.flags & HTML_Visible)==0 ){ src = src->base.pNext; TestPoint(0); } if( src==0 ){ TestPoint(0); return; } if( pBlock->n>0 ){ /* We must be dealing with plain old text */ if( src->base.type==Html_Text ){ x = src->text.x; y = src->text.y; TestPoint(0); }else{ CANT_HAPPEN; return; } if( pBlock->base.flags & HTML_Selected ){ HtmlLock(htmlPtr); DrawSelectionBackground(htmlPtr, pBlock, drawable, drawableLeft, drawableTop); if( HtmlUnlock(htmlPtr) ) return; } gc = HtmlGetGC(htmlPtr, src->base.style.color, src->base.style.font); font = HtmlGetFont(htmlPtr, src->base.style.font); if( font==0 ) return; Tk_DrawChars(htmlPtr->display, drawable, gc, font, pBlock->z, pBlock->n, x - drawableLeft, y - drawableTop); if( src->base.style.flags & STY_Underline ){ Tk_UnderlineChars(htmlPtr->display, drawable, gc, font, pBlock->z, x - drawableLeft, y-drawableTop, 0, pBlock->n); } if( src->base.style.flags & STY_StrikeThru ){ XRectangle xrec; xrec.x = pBlock->left - drawableLeft; xrec.y = (pBlock->top + pBlock->bottom)/2 - drawableTop; xrec.width = pBlock->right - pBlock->left; xrec.height = 1 + (pBlock->bottom - pBlock->top > 15); XFillRectangles(htmlPtr->display, drawable, gc, &xrec, 1); } if( pBlock==htmlPtr->pInsBlock && htmlPtr->insStatus>0 ){ int x; XRectangle xrec; if( htmlPtr->insIndex < pBlock->n ){ x = src->text.x - drawableLeft; x += Tk_TextWidth(font, pBlock->z, htmlPtr->insIndex); }else{ x = pBlock->right - drawableLeft; } if( x>0 ){ TestPoint(0); x--; } xrec.x = x; xrec.y = pBlock->top - drawableTop; xrec.width = 2; xrec.height = pBlock->bottom - pBlock->top; XFillRectangles(htmlPtr->display, drawable, gc, &xrec, 1); } }else{ /* We are dealing with a single HtmlElement which contains something ** other than plain text. */ int top=0; int btm=0; int cntr; int cnt, w; char zBuf[30]; switch( src->base.type ){ case Html_LI: x = src->li.x; y = src->li.y; cntr = (top+btm)/2; switch( src->li.type ){ case LI_TYPE_Enum_1: sprintf(zBuf,"%d.",src->li.cnt); TestPoint(0); break; case LI_TYPE_Enum_A: GetLetterIndex(zBuf,src->li.cnt,1); TestPoint(0); break; case LI_TYPE_Enum_a: GetLetterIndex(zBuf,src->li.cnt,0); TestPoint(0); break; case LI_TYPE_Enum_I: GetRomanIndex(zBuf,src->li.cnt,1); TestPoint(0); break; case LI_TYPE_Enum_i: GetRomanIndex(zBuf,src->li.cnt,0); TestPoint(0); break; default: zBuf[0] = 0; TestPoint(0); break; } gc = HtmlGetGC(htmlPtr, src->base.style.color, src->base.style.font); switch( src->li.type ){ case LI_TYPE_Undefined: case LI_TYPE_Bullet1: XFillArc(htmlPtr->display, drawable, gc, x - 7 - drawableLeft, y - 8 - drawableTop, 7, 7, 0, 360*64); TestPoint(0); break; case LI_TYPE_Bullet2: XDrawArc(htmlPtr->display, drawable, gc, x - 7 - drawableLeft, y - 8 - drawableTop, 7, 7, 0, 360*64); TestPoint(0); break; case LI_TYPE_Bullet3: XDrawRectangle(htmlPtr->display, drawable, gc, x - 7 - drawableLeft, y - 8 - drawableTop, 7, 7); TestPoint(0); break; case LI_TYPE_Enum_1: case LI_TYPE_Enum_A: case LI_TYPE_Enum_a: case LI_TYPE_Enum_I: case LI_TYPE_Enum_i: cnt = strlen(zBuf); font = HtmlGetFont(htmlPtr, src->base.style.font); if( font==0 ) return; w = Tk_TextWidth(font, zBuf, cnt); Tk_DrawChars(htmlPtr->display, drawable, gc, font, zBuf, cnt, x - w - drawableLeft, y - drawableTop); TestPoint(0); break; } break; case Html_HR: { int relief = htmlPtr->ruleRelief; switch( relief ){ case TK_RELIEF_RAISED: case TK_RELIEF_SUNKEN: break; default: relief = TK_RELIEF_FLAT; break; } HtmlDrawRect(htmlPtr, drawable, src, src->hr.x - drawableLeft, src->hr.y - drawableTop, src->hr.w, src->hr.h, 1, relief); break; } case Html_TABLE: { int relief = htmlPtr->tableRelief; switch( relief ){ case TK_RELIEF_RAISED: case TK_RELIEF_SUNKEN: break; default: relief = TK_RELIEF_FLAT; break; } HtmlDrawRect(htmlPtr, drawable, src, src->table.x - drawableLeft, src->table.y - drawableTop, src->table.w, src->table.h, src->table.borderWidth, relief); break; } case Html_TH: case Html_TD: { int depth, relief; pTable = src->cell.pTable; depth = pTable && pTable->table.borderWidth>0; switch( htmlPtr->tableRelief ){ case TK_RELIEF_RAISED: relief = TK_RELIEF_SUNKEN; break; case TK_RELIEF_SUNKEN: relief = TK_RELIEF_RAISED; break; default: relief = TK_RELIEF_FLAT; break; } HtmlDrawRect(htmlPtr, drawable, src, src->cell.x - drawableLeft, src->cell.y - drawableTop, src->cell.w, src->cell.h, depth, relief); break; } case Html_IMG: if( src->image.pImage ){ HtmlDrawImage(src, drawable, drawableLeft, drawableTop, drawableLeft + drawableWidth, drawableTop + drawableHeight); }else if( src->image.zAlt ){ gc = HtmlGetGC(htmlPtr, src->base.style.color, src->base.style.font); font = HtmlGetFont(htmlPtr, src->base.style.font); if( font==0 ) return; Tk_DrawChars(htmlPtr->display, drawable, gc, font, src->image.zAlt, strlen(src->image.zAlt), src->image.x - drawableLeft, src->image.y - drawableTop); TestPoint(0); } break; default: TestPoint(0); break; } } } /* ** Draw all or part of an image. */ void HtmlDrawImage( HtmlElement *pElem, /* The to be drawn */ Drawable drawable, /* Draw it here */ int drawableLeft, /* left edge of the drawable */ int drawableTop, /* Virtual canvas coordinate for top of drawable */ int drawableRight, /* right edge of the drawable */ int drawableBottom /* bottom edge of the drawable */ ){ int imageTop; /* virtual canvas coordinate for top of image */ int x, y; /* where to place image on the drawable */ int imageX, imageY; /* \__ Subset of image that fits */ int imageW, imageH; /* / on the drawable */ imageTop = pElem->image.y - pElem->image.ascent; y = imageTop - drawableTop; if( imageTop + pElem->image.h > drawableBottom ){ imageH = drawableBottom - imageTop; TestPoint(0); }else{ imageH = pElem->image.h; TestPoint(0); } if( y<0 ){ imageY = -y; imageH += y; y = 0; TestPoint(0); }else{ imageY = 0; TestPoint(0); } x = pElem->image.x - drawableLeft; if( pElem->image.x + pElem->image.w > drawableRight ){ imageW = drawableRight - pElem->image.x; TestPoint(0); }else{ imageW = pElem->image.w; TestPoint(0); } if( x<0 ){ imageX = -x; imageW += x; x = 0; TestPoint(0); }else{ imageX = 0; TestPoint(0); } Tk_RedrawImage(pElem->image.pImage->image, imageX, imageY, imageW, imageH, drawable, x, y); pElem->image.redrawNeeded = 0; } /* ** Recompute the following fields of the given block structure: ** ** base.count The number of elements described by this ** block structure. ** ** n The number of characters of text output ** associated with this block. If the block ** renders something other than text (ex: ) ** then set n to 0. ** ** z Pointer to malloced memory containing the ** text associated with this block. NULL if ** n is 0. ** ** Return a pointer to the first HtmlElement not covered by the ** block. */ static HtmlElement *FillOutBlock(HtmlWidget *htmlPtr, HtmlBlock *p){ HtmlElement *pElem; int go; int n; int x, y; int i; HtmlStyle style; int firstSelected; /* First selected character in this block */ int lastSelected; /* Last selected character in this block */ char zBuf[400]; /* ** Reset n and z */ if( p->n ){ p->n = 0; } if( p->z ){ HtmlFree(p->z); } firstSelected = 1000000; lastSelected = -1; /* ** Skip over HtmlElements that aren't directly displayed. */ pElem = p->base.pNext; p->base.count = 0; while( pElem && (pElem->base.flags & HTML_Visible)==0 ){ HtmlElement *pNext = pElem->pNext; if( pElem->base.type==Html_Block ){ UnlinkAndFreeBlock(htmlPtr, &pElem->block); TestPoint(0); }else{ p->base.count++; TestPoint(0); } pElem = pNext; } if( pElem==0 ){ TestPoint(0); return 0; } /* ** Handle "special" elements. */ if( pElem->base.type!=Html_Text ){ switch( pElem->base.type ){ case Html_HR: p->top = pElem->hr.y - pElem->hr.h; p->bottom = pElem->hr.y; p->left = pElem->hr.x; p->right = pElem->hr.x + pElem->hr.w; TestPoint(0); break; case Html_LI: p->top = pElem->li.y - pElem->li.ascent; p->bottom = pElem->li.y + pElem->li.descent; p->left = pElem->li.x - 10; p->right = pElem->li.x + 10; TestPoint(0); break; case Html_TD: case Html_TH: p->top = pElem->cell.y; p->bottom = pElem->cell.y + pElem->cell.h; p->left = pElem->cell.x; p->right = pElem->cell.x + pElem->cell.w; TestPoint(0); break; case Html_TABLE: p->top = pElem->table.y; p->bottom = pElem->table.y + pElem->table.h; p->left = pElem->table.x; p->right = pElem->table.x + pElem->table.w; TestPoint(0); break; case Html_IMG: p->top = pElem->image.y - pElem->image.ascent; p->bottom = pElem->image.y + pElem->image.descent; p->left = pElem->image.x; p->right = pElem->image.x + pElem->image.w; TestPoint(0); break; } p->base.count++; TestPoint(0); return pElem->pNext; } /* ** If we get this far, we must be dealing with text. */ n = 0; x = pElem->text.x; y = pElem->text.y; p->top = y - pElem->text.ascent; p->bottom = y + pElem->text.descent; p->left = x; style = pElem->base.style; go = 1; while( pElem ){ HtmlElement *pNext = pElem->pNext; switch( pElem->base.type ){ case Html_Text: if( pElem->base.style.flags & STY_Invisible ){ TestPoint(0); break; } if( pElem->text.spaceWidth <=0 ){ CANT_HAPPEN; break; } if( y != pElem->text.y || style.font != pElem->base.style.font || style.color != pElem->base.style.color || (style.flags & STY_FontMask) != (pElem->base.style.flags & STY_FontMask) ){ go = 0; TestPoint(0); }else{ int sw = pElem->text.spaceWidth; int nSpace = (pElem->text.x - x) / sw; if( nSpace * sw + x != pElem->text.x ){ go = 0; TestPoint(0); }else if( n + nSpace + pElem->base.count >= sizeof(zBuf) ){ go = 0; TestPoint(0); }else{ for(i=0; itext.zText); n += pElem->base.count; x = pElem->text.x + pElem->text.w; } } break; case Html_Space: if( pElem->base.style.font != style.font ){ pElem = pElem->pNext; go = 0; }else if( (style.flags & STY_Preformatted)!=0 && (pElem->base.flags & HTML_NewLine)!=0 ){ pElem = pElem->pNext; go = 0; } break; case Html_Block: UnlinkAndFreeBlock(htmlPtr,&pElem->block); break; case Html_A: case Html_EndA: go = 0; break; default: if( pElem->base.flags & HTML_Visible ) go = 0; TestPoint(0); break; } if( go==0 ) break; p->base.count++; pElem = pNext; } p->right = x; while( n>0 && zBuf[n-1]==' ' ){ TestPoint(0); n--; } p->z = HtmlAlloc( n ); strncpy(p->z, zBuf, n); p->n = n; return pElem; } /* ** Scan ahead looking for a place to put a block. Return a pointer ** to the element which should come immediately after the block. ** ** if pCnt!=0, then put the number of elements skipped in *pCnt. */ static HtmlElement *FindStartOfNextBlock( HtmlWidget *htmlPtr, /* The HTML widget */ HtmlElement *p, /* First candid for the start of a block */ int *pCnt /* Write number of elements skipped here */ ){ int cnt = 0; while( p && (p->base.flags & HTML_Visible)==0 ){ HtmlElement *pNext = p->pNext; if( p->base.type==Html_Block ){ UnlinkAndFreeBlock(htmlPtr, &p->block); }else{ cnt++; } p = pNext; } if( pCnt ){ *pCnt = cnt; } return p; } /* ** Add additional blocks to the block list in order to cover ** all elements on the element list. ** ** If any old blocks are found on the element list, they must ** be left over from a prior rendering. Unlink and delete them. */ void HtmlFormBlocks(HtmlWidget *htmlPtr){ HtmlElement *pElem; if( htmlPtr->lastBlock ){ pElem = FillOutBlock(htmlPtr, htmlPtr->lastBlock); }else{ pElem = htmlPtr->pFirst; } while( pElem ){ int cnt; pElem = FindStartOfNextBlock(htmlPtr, pElem, &cnt); if( pElem ){ HtmlBlock *pNew = AllocBlock(); if( htmlPtr->lastBlock ){ htmlPtr->lastBlock->base.count += cnt; } AppendBlock(htmlPtr, pElem, pNew); pElem = FillOutBlock(htmlPtr, pNew); } } }