3373 lines
116 KiB
C++
3373 lines
116 KiB
C++
/*
|
|
* Dillo Widget
|
|
*
|
|
* Copyright 2005-2007, 2012-2014 Sebastian Geerken <sgeerken@dillo.org>
|
|
* Copyright 2023-2024 Rodrigo Arias Mallo <rodarima@gmail.com>
|
|
*
|
|
* 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 3 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, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
|
|
#include "textblock.hh"
|
|
#include "../lout/msg.h"
|
|
#include "../lout/misc.hh"
|
|
#include "../lout/unicode.hh"
|
|
#include "../lout/debug.hh"
|
|
|
|
#include <stdio.h>
|
|
#include <math.h> // remove again?
|
|
#include <limits.h>
|
|
|
|
/*
|
|
* Local variables
|
|
*/
|
|
/* The tooltip under mouse pointer in current textblock. No ref. hold.
|
|
* (having one per view looks not worth the extra clutter). */
|
|
static dw::core::style::Tooltip *hoverTooltip = NULL;
|
|
|
|
|
|
using namespace lout;
|
|
using namespace lout::misc;
|
|
using namespace lout::unicode;
|
|
|
|
namespace dw {
|
|
|
|
Textblock::WordImgRenderer::WordImgRenderer (Textblock *textblock,
|
|
int wordNo)
|
|
{
|
|
//printf ("new WordImgRenderer %p\n", this);
|
|
|
|
this->textblock = textblock;
|
|
this->wordNo = wordNo;
|
|
dataSet = false;
|
|
}
|
|
|
|
Textblock::WordImgRenderer::~WordImgRenderer ()
|
|
{
|
|
//printf ("delete WordImgRenderer %p\n", this);
|
|
}
|
|
|
|
void Textblock::WordImgRenderer::setData (int xWordWidget, int lineNo)
|
|
{
|
|
dataSet = true;
|
|
this->xWordWidget = xWordWidget;
|
|
this->lineNo = lineNo;
|
|
}
|
|
|
|
bool Textblock::WordImgRenderer::readyToDraw ()
|
|
{
|
|
//print ();
|
|
//printf ("\n");
|
|
|
|
return dataSet && textblock->wasAllocated ()
|
|
&& wordNo < textblock->words->size()
|
|
&& lineNo < textblock->lines->size();
|
|
}
|
|
|
|
void Textblock::WordImgRenderer::getBgArea (int *x, int *y, int *width,
|
|
int *height)
|
|
{
|
|
// TODO Subtract margin and border (padding box)?
|
|
Line *line = textblock->lines->getRef (lineNo);
|
|
*x = textblock->allocation.x + this->xWordWidget;
|
|
*y = textblock->lineYOffsetCanvas (line);
|
|
*width = textblock->words->getRef(wordNo)->size.width;
|
|
*height = line->borderAscent + line->borderDescent;
|
|
}
|
|
|
|
void Textblock::WordImgRenderer::getRefArea (int *xRef, int *yRef,
|
|
int *widthRef, int *heightRef)
|
|
{
|
|
// See comment in Widget::drawBox about the reference area.
|
|
textblock->getPaddingArea (xRef, yRef, widthRef, heightRef);
|
|
}
|
|
|
|
core::style::Style *Textblock::WordImgRenderer::getStyle ()
|
|
{
|
|
return textblock->words->getRef(wordNo)->style;
|
|
}
|
|
|
|
void Textblock::WordImgRenderer::draw (int x, int y, int width, int height)
|
|
{
|
|
textblock->queueDrawArea (x - textblock->allocation.x,
|
|
y - textblock->allocation.y, width, height);
|
|
|
|
}
|
|
|
|
void Textblock::SpaceImgRenderer::getBgArea (int *x, int *y, int *width,
|
|
int *height)
|
|
{
|
|
WordImgRenderer::getBgArea (x, y, width, height);
|
|
*x += *width;
|
|
*width = textblock->words->getRef(wordNo)->effSpace;
|
|
}
|
|
|
|
core::style::Style *Textblock::SpaceImgRenderer::getStyle ()
|
|
{
|
|
return textblock->words->getRef(wordNo)->spaceStyle;
|
|
}
|
|
|
|
// ----------------------------------------------------------------------
|
|
|
|
Textblock::DivChar Textblock::divChars[NUM_DIV_CHARS] = {
|
|
// soft hyphen (U+00AD)
|
|
{ "\xc2\xad", true, false, true, PENALTY_HYPHEN, -1 },
|
|
// simple hyphen-minus: same penalties like automatic or soft hyphens
|
|
{ "-", false, true, true, -1, PENALTY_HYPHEN },
|
|
// (unconditional) hyphen (U+2010): handled exactly like minus-hyphen.
|
|
{ "\xe2\x80\x90", false, true, true, -1, PENALTY_HYPHEN },
|
|
// em dash (U+2014): breaks on both sides are allowed (but see below).
|
|
{ "\xe2\x80\x94", false, true, false,
|
|
PENALTY_EM_DASH_LEFT, PENALTY_EM_DASH_RIGHT }
|
|
};
|
|
|
|
// Standard values are defined here. The values are already multiplied
|
|
// with 100.
|
|
//
|
|
// Some examples (details are described in doc/dw-line-breaking.doc):
|
|
//
|
|
// 0 = Perfect line; as penalty used for normal spaces.
|
|
|
|
// 1 (100 here) = A justified line with spaces having 150% or 67% of
|
|
// the ideal space width has this as badness.
|
|
//
|
|
// 8 (800 here) = A justified line with spaces twice as wide as
|
|
// ideally has this as badness.
|
|
//
|
|
// The second value is used when the line before ends with a hyphen,
|
|
// dash etc.
|
|
|
|
int Textblock::penalties[PENALTY_NUM][2] = {
|
|
// Penalties for all hyphens.
|
|
{ 100, 800 },
|
|
// Penalties for a break point *left* of an em-dash: rather large,
|
|
// so that a break on the *right* side is preferred.
|
|
{ 800, 800 },
|
|
// Penalties for a break point *right* of an em-dash: like hyphens.
|
|
{ 100, 800 }
|
|
};
|
|
|
|
int Textblock::stretchabilityFactor = 100;
|
|
|
|
/**
|
|
* The character which is used to draw a hyphen at the end of a line,
|
|
* either caused by automatic hyphenation, or by soft hyphens.
|
|
*
|
|
* Initially, soft hyphens were used, but they are not drawn on some
|
|
* platforms. Also, unconditional hyphens (U+2010) are not available
|
|
* in many fonts; so, a simple hyphen-minus is used.
|
|
*/
|
|
const char *Textblock::hyphenDrawChar = "-";
|
|
|
|
void Textblock::setPenaltyHyphen (int penaltyHyphen)
|
|
{
|
|
penalties[PENALTY_HYPHEN][0] = penaltyHyphen;
|
|
}
|
|
|
|
void Textblock::setPenaltyHyphen2 (int penaltyHyphen2)
|
|
{
|
|
penalties[PENALTY_HYPHEN][1] = penaltyHyphen2;
|
|
}
|
|
|
|
void Textblock::setPenaltyEmDashLeft (int penaltyLeftEmDash)
|
|
{
|
|
penalties[PENALTY_EM_DASH_LEFT][0] = penaltyLeftEmDash;
|
|
penalties[PENALTY_EM_DASH_LEFT][1] = penaltyLeftEmDash;
|
|
}
|
|
|
|
void Textblock::setPenaltyEmDashRight (int penaltyRightEmDash)
|
|
{
|
|
penalties[PENALTY_EM_DASH_RIGHT][0] = penaltyRightEmDash;
|
|
}
|
|
|
|
void Textblock::setPenaltyEmDashRight2 (int penaltyRightEmDash2)
|
|
{
|
|
penalties[PENALTY_EM_DASH_RIGHT][1] = penaltyRightEmDash2;
|
|
}
|
|
|
|
void Textblock::setStretchabilityFactor (int stretchabilityFactor)
|
|
{
|
|
Textblock::stretchabilityFactor = stretchabilityFactor;
|
|
}
|
|
|
|
Textblock::Textblock (bool limitTextWidth, bool treatAsInline)
|
|
{
|
|
DBG_OBJ_CREATE ("dw::Textblock");
|
|
registerName ("dw::Textblock", typeid(*this));
|
|
setButtonSensitive(true);
|
|
|
|
hasListitemValue = false;
|
|
leftInnerPadding = 0;
|
|
line1Offset = 0;
|
|
ignoreLine1OffsetSometimes = false;
|
|
mustQueueResize = false;
|
|
DBG_OBJ_SET_BOOL ("mustQueueResize", mustQueueResize);
|
|
redrawY = 0;
|
|
DBG_OBJ_SET_NUM ("redrawY", redrawY);
|
|
lastWordDrawn = -1;
|
|
DBG_OBJ_SET_NUM ("lastWordDrawn", lastWordDrawn);
|
|
|
|
DBG_OBJ_ASSOC_CHILD (&sizeRequestParams);
|
|
|
|
/*
|
|
* The initial sizes of lines and words should not be
|
|
* too high, since this will waste much memory with tables
|
|
* containing many small cells. The few more calls to realloc
|
|
* should not decrease the speed considerably.
|
|
* (Current setting is for minimal memory usage. An interesting fact
|
|
* is that high values decrease speed due to memory handling overhead!)
|
|
* TODO: Some tests would be useful.
|
|
*/
|
|
paragraphs = new misc::SimpleVector <Paragraph> (1);
|
|
lines = new misc::SimpleVector <Line> (1);
|
|
nonTemporaryLines = 0;
|
|
words = new misc::NotSoSimpleVector <Word> (1);
|
|
|
|
wrapRefLines = wrapRefParagraphs = -1;
|
|
wrapRefLinesFCX = wrapRefLinesFCY = -1;
|
|
|
|
DBG_OBJ_SET_NUM ("lines.size", lines->size ());
|
|
DBG_OBJ_SET_NUM ("words.size", words->size ());
|
|
DBG_OBJ_SET_NUM ("wrapRefLines", wrapRefLines);
|
|
DBG_OBJ_SET_NUM ("wrapRefParagraphs", wrapRefParagraphs);
|
|
DBG_OBJ_SET_NUM ("wrapRefLinesFCX", wrapRefLinesFCX);
|
|
DBG_OBJ_SET_NUM ("wrapRefLinesFCY", wrapRefLinesFCY);
|
|
|
|
hoverLink = -1;
|
|
|
|
// -1 means undefined.
|
|
lineBreakWidth = -1;
|
|
DBG_OBJ_SET_NUM ("lineBreakWidth", lineBreakWidth);
|
|
|
|
this->limitTextWidth = limitTextWidth;
|
|
this->treatAsInline = treatAsInline;
|
|
|
|
for (int layer = 0; layer < core::HIGHLIGHT_NUM_LAYERS; layer++) {
|
|
/* hlStart[layer].index > hlEnd[layer].index means no highlighting */
|
|
hlStart[layer].index = 1;
|
|
hlStart[layer].nChar = 0;
|
|
hlEnd[layer].index = 0;
|
|
hlEnd[layer].nChar = 0;
|
|
|
|
DBG_OBJ_ARRATTRSET_NUM ("hlStart", layer, "index", hlStart[layer].index);
|
|
DBG_OBJ_ARRATTRSET_NUM ("hlStart", layer, "nChar", hlStart[layer].nChar);
|
|
DBG_OBJ_ARRATTRSET_NUM ("hlEnd", layer, "index", hlEnd[layer].index);
|
|
DBG_OBJ_ARRATTRSET_NUM ("hlEnd", layer, "nChar", hlEnd[layer].nChar);
|
|
}
|
|
|
|
numSizeReferences = 0;
|
|
|
|
initNewLine ();
|
|
}
|
|
|
|
Textblock::~Textblock ()
|
|
{
|
|
/* make sure not to call a free'd tooltip (very fast overkill) */
|
|
hoverTooltip = NULL;
|
|
|
|
for (int i = 0; i < words->size(); i++)
|
|
cleanupWord (i);
|
|
|
|
for( const Anchor &anchor: anchors )
|
|
{
|
|
/* This also frees the names (see removeAnchor() and related). */
|
|
removeAnchor(std::string{ anchor.name });
|
|
}
|
|
|
|
delete paragraphs;
|
|
delete lines;
|
|
delete words;
|
|
|
|
/* Make sure we don't own widgets anymore. Necessary before call of
|
|
parent class destructor. (???) */
|
|
words = NULL;
|
|
|
|
DBG_OBJ_DELETE ();
|
|
}
|
|
|
|
/**
|
|
* The ascent of a textblock is the ascent of the first line, plus
|
|
* padding/border/margin. This can be used to align the first lines
|
|
* of several textblocks in a horizontal line.
|
|
*/
|
|
void Textblock::sizeRequestImpl (core::Requisition *requisition, int numPos,
|
|
Widget **references, int *x, int *y)
|
|
{
|
|
DBG_OBJ_ENTER ("resize", 0, "sizeRequestImpl", "%d, ...", numPos);
|
|
|
|
sizeRequestParams.fill (numPos, references, x, y);
|
|
|
|
// We have to rewrap the whole textblock, if (i) the available width (which
|
|
// is the line break width) has changed, or (ii) if the position within the
|
|
// float container, and so possibly borders relative to this textblock, have
|
|
// changed.
|
|
//
|
|
// (The latter is a simplification: an over-correct implementation would test
|
|
// all OOFMs on whether affectsLeftBorder() or affectsRightBorder() returns
|
|
// true. Also, this may be optimized by distinguishing between floats
|
|
// generated by this textblock (which would not make rewrapping necessary)
|
|
// and floats generated by other textblocks (which would).)
|
|
|
|
int newLineBreakWidth = getAvailWidth (true);
|
|
int newFCX, newFCY;
|
|
bool fcDefined = findSizeRequestReference (OOFM_FLOATS, &newFCX, &newFCY);
|
|
|
|
if (newLineBreakWidth != lineBreakWidth ||
|
|
(fcDefined && (newFCX != wrapRefLinesFCX ||
|
|
newFCY != wrapRefLinesFCY))) {
|
|
lineBreakWidth = newLineBreakWidth;
|
|
wrapRefLines = 0;
|
|
DBG_OBJ_SET_NUM ("lineBreakWidth", lineBreakWidth);
|
|
DBG_OBJ_SET_NUM ("wrapRefLines", wrapRefLines);
|
|
|
|
if (!fcDefined) {
|
|
wrapRefLinesFCX = newFCX;
|
|
wrapRefLinesFCY = newFCY;
|
|
DBG_OBJ_SET_NUM ("wrapRefLinesFCX", wrapRefLinesFCX);
|
|
DBG_OBJ_SET_NUM ("wrapRefLinesFCY", wrapRefLinesFCY);
|
|
}
|
|
}
|
|
|
|
rewrap ();
|
|
showMissingLines ();
|
|
|
|
if (lines->size () > 0) {
|
|
Line *firstLine = lines->getRef(0), *lastLine = lines->getLastRef ();
|
|
|
|
// Note: the breakSpace of the last line is ignored, so breaks
|
|
// at the end of a textblock are not visible.
|
|
|
|
requisition->width =
|
|
lastLine->maxLineWidth + leftInnerPadding + boxDiffWidth ();
|
|
|
|
// Also regard collapsing of this widget top margin and the top
|
|
// margin of the first line box:
|
|
requisition->ascent = calcVerticalBorder (getStyle()->padding.top,
|
|
getStyle()->borderWidth.top,
|
|
getStyle()->margin.top
|
|
+ extraSpace.top,
|
|
firstLine->borderAscent,
|
|
firstLine->marginAscent);
|
|
|
|
// And here, regard collapsing of this widget bottom margin and the
|
|
// bottom margin of the last line box:
|
|
requisition->descent =
|
|
// (BTW, this line:
|
|
lastLine->top - firstLine->borderAscent + lastLine->borderAscent +
|
|
// ... is 0 for a block with one line, so special handling
|
|
// for this case is not necessary.)
|
|
calcVerticalBorder (getStyle()->padding.bottom,
|
|
getStyle()->borderWidth.bottom,
|
|
getStyle()->margin.bottom + extraSpace.bottom,
|
|
lastLine->borderDescent, lastLine->marginDescent);
|
|
} else {
|
|
requisition->width = leftInnerPadding + boxDiffWidth ();
|
|
requisition->ascent = boxOffsetY ();
|
|
requisition->descent = boxRestHeight ();
|
|
}
|
|
|
|
if (usesMaxGeneratorWidth ()) {
|
|
DBG_OBJ_MSGF ("resize", 1,
|
|
"before considering lineBreakWidth (= %d): %d * (%d + %d)",
|
|
lineBreakWidth, requisition->width, requisition->ascent,
|
|
requisition->descent);
|
|
if (requisition->width < lineBreakWidth)
|
|
requisition->width = lineBreakWidth;
|
|
} else
|
|
DBG_OBJ_MSG ("resize", 1, "lineBreakWidth needs no consideration");
|
|
|
|
DBG_OBJ_MSGF ("resize", 1, "before correction: %d * (%d + %d)",
|
|
requisition->width, requisition->ascent, requisition->descent);
|
|
|
|
correctRequisition (requisition, core::splitHeightPreserveAscent, true,
|
|
false);
|
|
|
|
// Dealing with parts out of flow, which may overlap the borders of
|
|
// the text block. Base lines are ignored here: they do not play a
|
|
// role (currently) and caring about them (for the future) would
|
|
// cause too much problems.
|
|
|
|
// Notice that the order is not typical: correctRequisition should
|
|
// be the last call. However, calling correctRequisition after
|
|
// outOfFlowMgr->getSize may result again in a size which is too
|
|
// small for floats, so triggering again (and again) the resize
|
|
// idle function resulting in CPU hogging. See also
|
|
// getExtremesImpl.
|
|
//
|
|
// Is this really what we want? An alternative could be that
|
|
// OutOfFlowMgr::getSize honours CSS attributes an corrected sizes.
|
|
|
|
correctRequisitionByOOF (requisition, core::splitHeightPreserveAscent);
|
|
|
|
DBG_OBJ_MSGF ("resize", 1, "final: %d * (%d + %d)",
|
|
requisition->width, requisition->ascent, requisition->descent);
|
|
DBG_OBJ_LEAVE ();
|
|
}
|
|
|
|
int Textblock::numSizeRequestReferences ()
|
|
{
|
|
return numSizeReferences;
|
|
}
|
|
|
|
core::Widget *Textblock::sizeRequestReference (int index)
|
|
{
|
|
return sizeReferences[index];
|
|
}
|
|
|
|
int Textblock::calcVerticalBorder (int widgetPadding, int widgetBorder,
|
|
int widgetMargin, int lineBorderTotal,
|
|
int lineMarginTotal)
|
|
{
|
|
DBG_OBJ_ENTER ("resize", 0, "calcVerticalBorder", "%d, %d, %d, %d, %d",
|
|
widgetPadding, widgetBorder, widgetMargin, lineBorderTotal,
|
|
lineMarginTotal);
|
|
|
|
int result;
|
|
|
|
if (widgetPadding == 0 && widgetBorder == 0) {
|
|
if (lineMarginTotal - lineBorderTotal >= widgetMargin)
|
|
result = lineMarginTotal;
|
|
else
|
|
result = widgetMargin + lineBorderTotal;
|
|
} else
|
|
result = lineMarginTotal + widgetPadding + widgetBorder + widgetMargin;
|
|
|
|
DBG_OBJ_LEAVE_VAL ("%d", result);
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Get the extremes of a word within a textblock.
|
|
*/
|
|
void Textblock::getWordExtremes (Word *word, core::Extremes *extremes)
|
|
{
|
|
if (word->content.type == core::Content::WIDGET_IN_FLOW)
|
|
word->content.widget->getExtremes (extremes);
|
|
else
|
|
extremes->minWidth = extremes->minWidthIntrinsic = extremes->maxWidth =
|
|
extremes->maxWidthIntrinsic = extremes->adjustmentWidth =
|
|
word->size.width;
|
|
}
|
|
|
|
void Textblock::getExtremesSimpl (core::Extremes *extremes)
|
|
{
|
|
DBG_OBJ_ENTER0 ("resize", 0, "getExtremesSimpl");
|
|
|
|
fillParagraphs ();
|
|
|
|
if (paragraphs->size () == 0) {
|
|
/* empty page */
|
|
extremes->minWidth = 0;
|
|
extremes->minWidthIntrinsic = 0;
|
|
extremes->maxWidth = 0;
|
|
extremes->maxWidthIntrinsic = 0;
|
|
extremes->adjustmentWidth = 0;
|
|
} else {
|
|
Paragraph *lastPar = paragraphs->getLastRef ();
|
|
extremes->minWidth = lastPar->maxParMin;
|
|
extremes->minWidthIntrinsic = lastPar->maxParMinIntrinsic;
|
|
extremes->maxWidth = lastPar->maxParMax;
|
|
extremes->maxWidthIntrinsic = lastPar->maxParMaxIntrinsic;
|
|
extremes->adjustmentWidth = lastPar->maxParAdjustmentWidth;
|
|
|
|
DBG_OBJ_MSGF ("resize", 1, "paragraphs[%d]->maxParMin = %d (%d)",
|
|
paragraphs->size () - 1, lastPar->maxParMin,
|
|
lastPar->maxParMinIntrinsic);
|
|
DBG_OBJ_MSGF ("resize", 1, "paragraphs[%d]->maxParMax = %d (%d)",
|
|
paragraphs->size () - 1, lastPar->maxParMax,
|
|
lastPar->maxParMaxIntrinsic);
|
|
}
|
|
|
|
DBG_OBJ_MSGF ("resize", 0, "after considering paragraphs: %d (%d) / %d (%d)",
|
|
extremes->minWidth, extremes->minWidthIntrinsic,
|
|
extremes->maxWidth, extremes->maxWidthIntrinsic);
|
|
|
|
int diff = leftInnerPadding + boxDiffWidth ();
|
|
extremes->minWidth += diff;
|
|
extremes->minWidthIntrinsic += diff;
|
|
extremes->maxWidth += diff;
|
|
extremes->maxWidthIntrinsic += diff;
|
|
extremes->adjustmentWidth += diff;
|
|
|
|
DBG_OBJ_MSGF ("resize", 0, "after adding diff: %d (%d) / %d (%d)",
|
|
extremes->minWidth, extremes->minWidthIntrinsic,
|
|
extremes->maxWidth, extremes->maxWidthIntrinsic);
|
|
|
|
// For the order, see similar reasoning in sizeRequestImpl.
|
|
|
|
correctExtremes (extremes, true);
|
|
|
|
DBG_OBJ_MSGF ("resize", 0, "after correction: %d (%d) / %d (%d)",
|
|
extremes->minWidth, extremes->minWidthIntrinsic,
|
|
extremes->maxWidth, extremes->maxWidthIntrinsic);
|
|
|
|
correctExtremesByOOF (extremes);
|
|
|
|
DBG_OBJ_MSGF ("resize", 0,
|
|
"finally, after considering OOFM: %d (%d) / %d (%d)",
|
|
extremes->minWidth, extremes->minWidthIntrinsic,
|
|
extremes->maxWidth, extremes->maxWidthIntrinsic);
|
|
DBG_OBJ_LEAVE ();
|
|
}
|
|
|
|
int Textblock::numGetExtremesReferences ()
|
|
{
|
|
return numSizeReferences;
|
|
}
|
|
|
|
core::Widget *Textblock::getExtremesReference (int index)
|
|
{
|
|
return sizeReferences[index];
|
|
}
|
|
|
|
void Textblock::notifySetAsTopLevel ()
|
|
{
|
|
OOFAwareWidget::notifySetAsTopLevel ();
|
|
|
|
numSizeReferences = 0;
|
|
DBG_OBJ_SET_NUM ("numSizeReferences", numSizeReferences);
|
|
}
|
|
|
|
void Textblock::notifySetParent ()
|
|
{
|
|
OOFAwareWidget::notifySetParent ();
|
|
|
|
numSizeReferences = 0;
|
|
for (int i = 0; i < NUM_OOFM; i++) {
|
|
if (oofContainer[i] != this) {
|
|
// avoid duplicates
|
|
bool found = false;
|
|
for (int j = 0; !found && j < numSizeReferences; j++)
|
|
if (oofContainer[i] == oofContainer[j])
|
|
found = true;
|
|
|
|
if (!found)
|
|
sizeReferences[numSizeReferences++] = oofContainer[i];
|
|
}
|
|
}
|
|
|
|
DBG_OBJ_SET_NUM ("numSizeReferences", numSizeReferences);
|
|
for (int i = 0; i < numSizeReferences; i++)
|
|
DBG_OBJ_ARRSET_PTR ("sizeReferences", i, sizeReferences[i]);
|
|
}
|
|
|
|
void Textblock::sizeAllocateImpl (core::Allocation *allocation)
|
|
{
|
|
DBG_OBJ_ENTER ("resize", 0, "sizeAllocateImpl", "%d, %d; %d * (%d + %d)",
|
|
allocation->x, allocation->y, allocation->width,
|
|
allocation->ascent, allocation->descent);
|
|
|
|
showMissingLines ();
|
|
|
|
sizeAllocateStart (allocation);
|
|
|
|
int lineIndex, wordIndex;
|
|
Line *line;
|
|
Word *word;
|
|
int xCursor;
|
|
core::Allocation childAllocation;
|
|
core::Allocation *oldChildAllocation;
|
|
|
|
if (allocation->x != this->allocation.x ||
|
|
allocation->y != this->allocation.y ||
|
|
allocation->width != this->allocation.width) {
|
|
redrawY = 0;
|
|
DBG_OBJ_SET_NUM ("redrawY", redrawY);
|
|
}
|
|
|
|
DBG_OBJ_MSG_START ();
|
|
|
|
for (lineIndex = 0; lineIndex < lines->size (); lineIndex++) {
|
|
DBG_OBJ_MSGF ("resize", 1, "line %d", lineIndex);
|
|
DBG_OBJ_MSG_START ();
|
|
|
|
// Especially for floats, allocation->width may be different
|
|
// from the line break width, so that for centered and right
|
|
// text, the offsets have to be recalculated again. However, if
|
|
// the allocation width is greater than the line break width,
|
|
// due to wide unbreakable lines (large image etc.), use the
|
|
// original line break width.
|
|
//
|
|
// TODO: test case?
|
|
|
|
calcTextOffset (lineIndex, std::min (allocation->width, lineBreakWidth));
|
|
|
|
line = lines->getRef (lineIndex);
|
|
xCursor = line->textOffset;
|
|
|
|
DBG_OBJ_MSGF ("resize", 1, "xCursor = %d (initially)", xCursor);
|
|
|
|
for (wordIndex = line->firstWord; wordIndex <= line->lastWord;
|
|
wordIndex++) {
|
|
word = words->getRef (wordIndex);
|
|
|
|
if (wordIndex == lastWordDrawn + 1) {
|
|
redrawY = std::min (redrawY, lineYOffsetWidget (line, allocation));
|
|
DBG_OBJ_SET_NUM ("redrawY", redrawY);
|
|
}
|
|
|
|
if (word->content.type == core::Content::WIDGET_IN_FLOW) {
|
|
DBG_OBJ_MSGF ("resize", 1,
|
|
"allocating widget in flow: line %d, word %d",
|
|
lineIndex, wordIndex);
|
|
|
|
// TODO For word->flags & Word::TOPLEFT_OF_LINE, make
|
|
// allocation consistent with calcSizeOfWidgetInFlow():
|
|
|
|
childAllocation.x = xCursor + allocation->x;
|
|
|
|
/** \todo Justification within the line is done here. */
|
|
/* align=top:
|
|
childAllocation.y = line->top + allocation->y;
|
|
*/
|
|
/* align=bottom (base line) */
|
|
/* Commented lines break the n2 and n3 test cases at
|
|
* https://dillo-browser.github.io/old/test/img/ */
|
|
childAllocation.y = lineYOffsetCanvas (line, allocation)
|
|
+ (line->borderAscent - word->size.ascent);
|
|
|
|
childAllocation.width = word->size.width;
|
|
childAllocation.ascent = word->size.ascent;
|
|
childAllocation.descent = word->size.descent;
|
|
|
|
oldChildAllocation = word->content.widget->getAllocation();
|
|
|
|
if (childAllocation.x != oldChildAllocation->x ||
|
|
childAllocation.y != oldChildAllocation->y ||
|
|
childAllocation.width != oldChildAllocation->width) {
|
|
/* The child widget has changed its position or its width
|
|
* so we need to redraw from this line onwards.
|
|
*/
|
|
redrawY =
|
|
std::min (redrawY, lineYOffsetWidget (line, allocation));
|
|
DBG_OBJ_SET_NUM ("redrawY", redrawY);
|
|
if (word->content.widget->wasAllocated ()) {
|
|
redrawY = std::min (redrawY,
|
|
oldChildAllocation->y - this->allocation.y);
|
|
DBG_OBJ_SET_NUM ("redrawY", redrawY);
|
|
}
|
|
|
|
} else if (childAllocation.ascent + childAllocation.descent !=
|
|
oldChildAllocation->ascent + oldChildAllocation->descent) {
|
|
/* The child widget has changed its height. We need to redraw
|
|
* from where it changed.
|
|
* It's important not to draw from the line base, because the
|
|
* child might be a table covering the whole page so we would
|
|
* end up redrawing the whole screen over and over.
|
|
* The drawing of the child content is left to the child itself.
|
|
* However this optimization is only possible if the widget is
|
|
* the only word in the line apart from an optional BREAK.
|
|
* Otherwise the height change of the widget could change the
|
|
* position of other words in the line, requiring a
|
|
* redraw of the complete line.
|
|
*/
|
|
if (line->lastWord == line->firstWord ||
|
|
(line->lastWord == line->firstWord + 1 &&
|
|
words->getRef (line->lastWord)->content.type ==
|
|
core::Content::BREAK)) {
|
|
|
|
int childChangedY =
|
|
std::min(childAllocation.y - allocation->y +
|
|
childAllocation.ascent + childAllocation.descent,
|
|
oldChildAllocation->y - this->allocation.y +
|
|
oldChildAllocation->ascent +
|
|
oldChildAllocation->descent);
|
|
|
|
redrawY = std::min (redrawY, childChangedY);
|
|
DBG_OBJ_SET_NUM ("redrawY", redrawY);
|
|
} else {
|
|
redrawY =
|
|
std::min (redrawY, lineYOffsetWidget (line, allocation));
|
|
DBG_OBJ_SET_NUM ("redrawY", redrawY);
|
|
}
|
|
}
|
|
word->content.widget->sizeAllocate (&childAllocation);
|
|
}
|
|
|
|
xCursor += (word->size.width + word->effSpace);
|
|
DBG_OBJ_MSGF ("resize", 1, "xCursor = %d (after word %d)",
|
|
xCursor, wordIndex);
|
|
DBG_MSG_WORD ("resize", 1, "<i>that is:</i> ", wordIndex, "");
|
|
}
|
|
|
|
DBG_OBJ_MSG_END ();
|
|
}
|
|
|
|
DBG_OBJ_MSG_END ();
|
|
|
|
sizeAllocateEnd ();
|
|
|
|
for (int i = 0; i < anchors.size(); i++) {
|
|
Anchor *anchor = &anchors.at(i);
|
|
int y;
|
|
|
|
if (anchor->wordIndex >= words->size() ||
|
|
// Also regard not-yet-existing lines.
|
|
lines->size () <= 0 ||
|
|
anchor->wordIndex > lines->getLastRef()->lastWord) {
|
|
y = allocation->y + allocation->ascent + allocation->descent;
|
|
} else {
|
|
Line *line = lines->getRef(findLineOfWord (anchor->wordIndex));
|
|
y = lineYOffsetCanvas (line, allocation);
|
|
}
|
|
changeAnchor (std::string{ anchor->name }, y);
|
|
}
|
|
|
|
DBG_OBJ_LEAVE ();
|
|
}
|
|
|
|
void Textblock::calcExtraSpaceImpl (int numPos, Widget **references, int *x,
|
|
int *y)
|
|
{
|
|
DBG_OBJ_ENTER0 ("resize", 0, "Textblock::calcExtraSpaceImpl");
|
|
|
|
sizeRequestParams.fill (numPos, references, x, y);
|
|
|
|
OOFAwareWidget::calcExtraSpaceImpl (numPos, references, x, y);
|
|
|
|
int clearPosition = 0;
|
|
for (int i = 0; i < NUM_OOFM; i++)
|
|
if (searchOutOfFlowMgr (i) && findSizeRequestReference (i, NULL, NULL))
|
|
clearPosition =
|
|
std::max (clearPosition,
|
|
searchOutOfFlowMgr(i)->getClearPosition (this));
|
|
|
|
extraSpace.top = std::max (extraSpace.top, clearPosition);
|
|
|
|
DBG_OBJ_LEAVE ();
|
|
}
|
|
|
|
int Textblock::getAvailWidthOfChild (Widget *child, bool forceValue)
|
|
{
|
|
DBG_OBJ_ENTER ("resize", 0, "Textblock::getAvailWidthOfChild", "%p, %s",
|
|
child, forceValue ? "true" : "false");
|
|
|
|
int width;
|
|
|
|
if (isWidgetOOF (child) && getWidgetOutOfFlowMgr(child) &&
|
|
getWidgetOutOfFlowMgr(child)->dealingWithSizeOfChild (child))
|
|
width =
|
|
getWidgetOutOfFlowMgr(child)->getAvailWidthOfChild (child,forceValue);
|
|
else {
|
|
if (child->getStyle()->width == core::style::LENGTH_AUTO) {
|
|
// No width specified: similar to standard implementation (see
|
|
// there), but "leftInnerPadding" has to be considered, too.
|
|
DBG_OBJ_MSG ("resize", 1, "no specification");
|
|
if (forceValue) {
|
|
width = std::max (getAvailWidth (true) - boxDiffWidth ()
|
|
- leftInnerPadding,
|
|
0);
|
|
|
|
if (width != -1) {
|
|
/* Clamp to min-width and max-width if given, taking into
|
|
* account leftInnerPadding. */
|
|
int maxWidth = child->calcWidth (child->getStyle()->maxWidth,
|
|
-1, this, -1, false);
|
|
if (maxWidth != -1 && width > maxWidth - leftInnerPadding)
|
|
width = maxWidth - leftInnerPadding;
|
|
|
|
int minWidth = child->calcWidth (child->getStyle()->minWidth,
|
|
-1, this, -1, false);
|
|
if (minWidth != -1 && width < minWidth - leftInnerPadding)
|
|
width = minWidth - leftInnerPadding;
|
|
}
|
|
|
|
} else {
|
|
width = -1;
|
|
}
|
|
} else
|
|
width = Widget::getAvailWidthOfChild (child, forceValue);
|
|
|
|
if (forceValue && this == child->getContainer () &&
|
|
!usesMaxGeneratorWidth ()) {
|
|
core::Extremes extremes;
|
|
getExtremes (&extremes);
|
|
if (width > extremes.maxWidth - boxDiffWidth () - leftInnerPadding)
|
|
width = extremes.maxWidth - boxDiffWidth () - leftInnerPadding;
|
|
}
|
|
}
|
|
|
|
DBG_OBJ_LEAVE_VAL ("%d", width);
|
|
return width;
|
|
}
|
|
|
|
int Textblock::getAvailHeightOfChild (core::Widget *child, bool forceValue)
|
|
{
|
|
if (isWidgetOOF(child) && getWidgetOutOfFlowMgr(child) &&
|
|
getWidgetOutOfFlowMgr(child)->dealingWithSizeOfChild (child))
|
|
return getWidgetOutOfFlowMgr(child)->getAvailHeightOfChild (child,
|
|
forceValue);
|
|
else
|
|
return Widget::getAvailHeightOfChild (child, forceValue);
|
|
}
|
|
|
|
void Textblock::containerSizeChangedForChildren ()
|
|
{
|
|
DBG_OBJ_ENTER0 ("resize", 0, "containerSizeChangedForChildren");
|
|
|
|
for (int i = 0; i < words->size (); i++) {
|
|
Word *word = words->getRef (i);
|
|
if (word->content.type == core::Content::WIDGET_IN_FLOW)
|
|
word->content.widget->containerSizeChanged ();
|
|
}
|
|
|
|
containerSizeChangedForChildrenOOF ();
|
|
|
|
DBG_OBJ_LEAVE ();
|
|
}
|
|
|
|
bool Textblock::affectsSizeChangeContainerChild (Widget *child)
|
|
{
|
|
DBG_OBJ_ENTER ("resize", 0,
|
|
"Textblock/affectsSizeChangeContainerChild", "%p", child);
|
|
|
|
// See Textblock::getAvailWidthOfChild() and Textblock::oofSizeChanged():
|
|
// Extremes changes affect the size of the child, too:
|
|
bool ret;
|
|
if (!usesMaxGeneratorWidth () &&
|
|
(extremesQueued () || extremesChanged ()))
|
|
ret = true;
|
|
else
|
|
ret = Widget::affectsSizeChangeContainerChild (child);
|
|
|
|
DBG_OBJ_LEAVE_VAL ("%s", boolToStr(ret));
|
|
return ret;
|
|
}
|
|
|
|
bool Textblock::usesAvailWidth ()
|
|
{
|
|
return true;
|
|
}
|
|
|
|
void Textblock::resizeDrawImpl ()
|
|
{
|
|
DBG_OBJ_ENTER0 ("draw", 0, "resizeDrawImpl");
|
|
|
|
queueDrawArea (0, redrawY, allocation.width, getHeight () - redrawY);
|
|
if (lines->size () > 0) {
|
|
Line *lastLine = lines->getRef (lines->size () - 1);
|
|
/* Remember the last word that has been drawn so we can ensure to
|
|
* draw any new added words (see sizeAllocateImpl()).
|
|
*/
|
|
lastWordDrawn = lastLine->lastWord;
|
|
DBG_OBJ_SET_NUM ("lastWordDrawn", lastWordDrawn);
|
|
}
|
|
|
|
redrawY = getHeight ();
|
|
DBG_OBJ_SET_NUM ("redrawY", redrawY);
|
|
|
|
DBG_OBJ_LEAVE ();
|
|
}
|
|
|
|
void Textblock::markSizeChange (int ref)
|
|
{
|
|
DBG_OBJ_ENTER ("resize", 0, "markSizeChange", "%d", ref);
|
|
|
|
if (isParentRefOOF (ref))
|
|
getParentRefOutOfFlowMgr(ref)
|
|
->markSizeChange (getParentRefOOFSubRef (ref));
|
|
else {
|
|
/* By the way: ref == -1 may have two different causes: (i) flush()
|
|
calls "queueResize (-1, true)", when no rewrapping is necessary;
|
|
and (ii) a word may have parentRef == -1 , when it is not yet
|
|
added to a line. In the latter case, nothing has to be done
|
|
now, but addLine(...) will do everything necessary. */
|
|
if (ref != -1) {
|
|
if (wrapRefLines == -1)
|
|
wrapRefLines = getParentRefInFlowSubRef (ref);
|
|
else
|
|
wrapRefLines = std::min (wrapRefLines,
|
|
getParentRefInFlowSubRef (ref));
|
|
}
|
|
|
|
DBG_OBJ_SET_NUM ("wrapRefLines", wrapRefLines);
|
|
|
|
// It seems that sometimes (even without floats) the lines
|
|
// structure is changed, so that wrapRefLines may refers to a
|
|
// line which does not exist anymore. Should be examined
|
|
// again. Until then, setting wrapRefLines to the same value is
|
|
// a workaround.
|
|
markExtremesChange (ref);
|
|
}
|
|
|
|
DBG_OBJ_LEAVE ();
|
|
}
|
|
|
|
void Textblock::markExtremesChange (int ref)
|
|
{
|
|
DBG_OBJ_ENTER ("resize", 1, "markExtremesChange", "%d", ref);
|
|
|
|
if (isParentRefOOF (ref))
|
|
getParentRefOutOfFlowMgr(ref)
|
|
->markExtremesChange (getParentRefOOFSubRef (ref));
|
|
else {
|
|
/* By the way: ref == -1 may have two different causes: (i) flush()
|
|
calls "queueResize (-1, true)", when no rewrapping is necessary;
|
|
and (ii) a word may have parentRef == -1 , when it is not yet
|
|
added to a line. In the latter case, nothing has to be done
|
|
now, but addLine(...) will do everything necessary. */
|
|
if (ref != -1) {
|
|
if (wrapRefParagraphs == -1)
|
|
wrapRefParagraphs = getParentRefInFlowSubRef (ref);
|
|
else
|
|
wrapRefParagraphs =
|
|
std::min (wrapRefParagraphs, getParentRefInFlowSubRef (ref));
|
|
}
|
|
|
|
DBG_OBJ_SET_NUM ("wrapRefParagraphs", wrapRefParagraphs);
|
|
}
|
|
|
|
DBG_OBJ_LEAVE ();
|
|
}
|
|
|
|
bool Textblock::isBlockLevel ()
|
|
{
|
|
return true;
|
|
}
|
|
|
|
bool Textblock::buttonPressImpl (core::EventButton *event)
|
|
{
|
|
return sendSelectionEvent (core::SelectionState::BUTTON_PRESS, event);
|
|
}
|
|
|
|
bool Textblock::buttonReleaseImpl (core::EventButton *event)
|
|
{
|
|
return sendSelectionEvent (core::SelectionState::BUTTON_RELEASE, event);
|
|
}
|
|
|
|
/*
|
|
* Handle motion inside the widget
|
|
* (special care is necessary when switching from another widget,
|
|
* because hoverLink and hoverTooltip are meaningless then).
|
|
*/
|
|
bool Textblock::motionNotifyImpl (core::EventMotion *event)
|
|
{
|
|
if (event->state & core::BUTTON1_MASK)
|
|
return sendSelectionEvent (core::SelectionState::BUTTON_MOTION, event);
|
|
else {
|
|
bool inSpace;
|
|
int linkOld = hoverLink;
|
|
core::style::Tooltip *tooltipOld = hoverTooltip;
|
|
const Word *word = findWord (event->xWidget, event->yWidget, &inSpace);
|
|
|
|
// cursor from word or widget style
|
|
if (word == NULL) {
|
|
setCursor (getStyle()->cursor);
|
|
hoverLink = -1;
|
|
hoverTooltip = NULL;
|
|
} else {
|
|
core::style::Style *style = inSpace ? word->spaceStyle : word->style;
|
|
setCursor (style->cursor);
|
|
hoverLink = style->x_link;
|
|
hoverTooltip = style->x_tooltip;
|
|
}
|
|
|
|
// Show/hide tooltip
|
|
if (tooltipOld != hoverTooltip) {
|
|
if (tooltipOld)
|
|
tooltipOld->onLeave ();
|
|
if (hoverTooltip)
|
|
hoverTooltip->onEnter ();
|
|
} else if (hoverTooltip)
|
|
hoverTooltip->onMotion ();
|
|
|
|
_MSG("MN tb=%p tooltipOld=%p hoverTooltip=%p\n",
|
|
this, tooltipOld, hoverTooltip);
|
|
if (hoverLink != linkOld) {
|
|
/* LinkEnter with hoverLink == -1 is the same as LinkLeave */
|
|
return layout->emitLinkEnter (this, hoverLink, -1, -1, -1);
|
|
} else {
|
|
return hoverLink != -1;
|
|
}
|
|
}
|
|
}
|
|
|
|
void Textblock::enterNotifyImpl (core::EventCrossing *event)
|
|
{
|
|
_MSG(" tb=%p, ENTER NotifyImpl hoverTooltip=%p\n", this, hoverTooltip);
|
|
/* reset hoverLink so linkEnter is detected */
|
|
hoverLink = -2;
|
|
}
|
|
|
|
void Textblock::leaveNotifyImpl (core::EventCrossing *event)
|
|
{
|
|
_MSG(" tb=%p, LEAVE NotifyImpl: hoverTooltip=%p\n", this, hoverTooltip);
|
|
|
|
/* leaving the viewport can't be handled by motionNotifyImpl() */
|
|
if (hoverLink >= 0)
|
|
layout->emitLinkEnter (this, -1, -1, -1, -1);
|
|
|
|
if (hoverTooltip) {
|
|
hoverTooltip->onLeave();
|
|
hoverTooltip = NULL;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* \brief Send event to selection.
|
|
*/
|
|
bool Textblock::sendSelectionEvent (core::SelectionState::EventType eventType,
|
|
core::MousePositionEvent *event)
|
|
{
|
|
DBG_OBJ_ENTER0 ("events", 0, "sendSelectionEvent");
|
|
|
|
core::Iterator *it;
|
|
int wordIndex;
|
|
int charPos = 0, link = -1;
|
|
bool r;
|
|
|
|
if (words->size () == 0) {
|
|
wordIndex = -1;
|
|
} else {
|
|
Line *lastLine = lines->getRef (lines->size () - 1);
|
|
int yFirst = lineYOffsetCanvas (0);
|
|
int yLast = lineYOffsetCanvas (lastLine) + lastLine->borderAscent +
|
|
lastLine->borderDescent;
|
|
if (event->yCanvas < yFirst) {
|
|
// Above the first line: take the first word.
|
|
wordIndex = 0;
|
|
} else if (event->yCanvas >= yLast) {
|
|
// Below the last line: take the last word.
|
|
wordIndex = words->size () - 1;
|
|
charPos = core::SelectionState::END_OF_WORD;
|
|
} else {
|
|
Line *line =
|
|
lines->getRef (findLineIndexWhenAllocated (event->yWidget));
|
|
|
|
// Pointer within the break space?
|
|
if (event->yWidget > (lineYOffsetWidget (line) +
|
|
line->borderAscent + line->borderDescent)) {
|
|
// Choose this break.
|
|
wordIndex = line->lastWord;
|
|
charPos = core::SelectionState::END_OF_WORD;
|
|
} else if (event->xWidget < line->textOffset) {
|
|
// Left of the first word in the line.
|
|
wordIndex = line->firstWord;
|
|
} else {
|
|
int nextWordStartX = line->textOffset;
|
|
|
|
for (wordIndex = line->firstWord;
|
|
wordIndex <= line->lastWord;
|
|
wordIndex++) {
|
|
Word *word = words->getRef (wordIndex);
|
|
int wordStartX = nextWordStartX;
|
|
|
|
nextWordStartX += word->size.width + word->effSpace;
|
|
|
|
if (event->xWidget >= wordStartX &&
|
|
event->xWidget < nextWordStartX) {
|
|
// We have found the word.
|
|
int yWidgetBase =
|
|
lineYOffsetWidget (line) + line->borderAscent;
|
|
|
|
if (event->xWidget >= nextWordStartX - word->effSpace) {
|
|
charPos = core::SelectionState::END_OF_WORD;
|
|
if (wordIndex < line->lastWord &&
|
|
(words->getRef(wordIndex + 1)->content.type !=
|
|
core::Content::BREAK) &&
|
|
(event->yWidget <=
|
|
yWidgetBase + word->spaceStyle->font->descent) &&
|
|
(event->yWidget >
|
|
yWidgetBase - word->spaceStyle->font->ascent)) {
|
|
link = word->spaceStyle->x_link;
|
|
}
|
|
} else {
|
|
if (event->yWidget <= yWidgetBase + word->size.descent &&
|
|
event->yWidget > yWidgetBase - word->size.ascent) {
|
|
link = word->style->x_link;
|
|
}
|
|
if (word->content.type == core::Content::TEXT) {
|
|
int glyphX = wordStartX;
|
|
int isStartWord = word->flags & Word::WORD_START;
|
|
int isEndWord = word->flags & Word::WORD_END;
|
|
|
|
while (1) {
|
|
int nextCharPos =
|
|
layout->nextGlyph (word->content.text, charPos);
|
|
// TODO The width of a text is not the sum
|
|
// of the widths of the glyphs, because of
|
|
// ligatures, kerning etc., so textWidth
|
|
// should be applied to the text from 0 to
|
|
// nextCharPos. (Or not? See comment below.)
|
|
|
|
int glyphWidth =
|
|
textWidth (word->content.text, charPos,
|
|
nextCharPos - charPos, word->style,
|
|
isStartWord && charPos == 0,
|
|
isEndWord &&
|
|
word->content.text[nextCharPos] == 0);
|
|
if (event->xWidget > glyphX + glyphWidth) {
|
|
glyphX += glyphWidth;
|
|
charPos = nextCharPos;
|
|
continue;
|
|
} else if (event->xWidget >= glyphX + glyphWidth/2){
|
|
// On the right half of a character;
|
|
// now just look for combining chars
|
|
charPos = nextCharPos;
|
|
while (word->content.text[charPos]) {
|
|
nextCharPos =
|
|
layout->nextGlyph (word->content.text,
|
|
charPos);
|
|
if (textWidth (word->content.text, charPos,
|
|
nextCharPos - charPos,
|
|
word->style,
|
|
isStartWord && charPos == 0,
|
|
isEndWord &&
|
|
word->content.text[nextCharPos]
|
|
== 0))
|
|
break;
|
|
charPos = nextCharPos;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
} else {
|
|
// Depends on whether the pointer is within the left or
|
|
// right half of the (non-text) word.
|
|
if (event->xWidget >= (wordStartX + nextWordStartX) /2)
|
|
charPos = core::SelectionState::END_OF_WORD;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
if (wordIndex > line->lastWord) {
|
|
// No word found in this line (i.e. we are on the right side),
|
|
// take the last of this line.
|
|
wordIndex = line->lastWord;
|
|
charPos = core::SelectionState::END_OF_WORD;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
DBG_OBJ_MSGF ("events", 1, "wordIndex = %d", wordIndex);
|
|
DBG_MSG_WORD ("events", 1, "<i>this is:</i> ", wordIndex, "");
|
|
|
|
it = TextblockIterator::createWordIndexIterator
|
|
(this, core::Content::maskForSelection (true), wordIndex);
|
|
r = selectionHandleEvent (eventType, it, charPos, link, event);
|
|
it->unref ();
|
|
|
|
DBG_OBJ_LEAVE_VAL ("%s", boolToStr(r));
|
|
return r;
|
|
}
|
|
|
|
void Textblock::removeChild (Widget *child)
|
|
{
|
|
/** \bug Not implemented. */
|
|
}
|
|
|
|
core::Iterator *Textblock::iterator (core::Content::Type mask, bool atEnd)
|
|
{
|
|
return new TextblockIterator (this, mask, atEnd);
|
|
}
|
|
|
|
/*
|
|
* Draw the decorations on a word.
|
|
*/
|
|
void Textblock::decorateText (core::View *view, core::style::Style *style,
|
|
core::style::Color::Shading shading,
|
|
int x, int yBase, int width)
|
|
{
|
|
int y, height;
|
|
|
|
height = 1 + style->font->xHeight / 12;
|
|
if (style->textDecoration & core::style::TEXT_DECORATION_UNDERLINE) {
|
|
y = yBase + style->font->descent / 3;
|
|
view->drawRectangle (style->color, shading, true, x, y, width, height);
|
|
}
|
|
if (style->textDecoration & core::style::TEXT_DECORATION_OVERLINE) {
|
|
y = yBase - style->font->ascent;
|
|
view->drawRectangle (style->color, shading, true, x, y, width, height);
|
|
}
|
|
if (style->textDecoration & core::style::TEXT_DECORATION_LINE_THROUGH) {
|
|
y = yBase + (style->font->descent - style->font->ascent) / 2 +
|
|
style->font->descent / 4;
|
|
view->drawRectangle (style->color, shading, true, x, y, width, height);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Draw a string of text
|
|
*
|
|
* Arguments: ... "isStart" and "isEnd" are true, when the text
|
|
* start/end represents the start/end of a "real" text word (before
|
|
* hyphenation). This has an effect on text transformation. ("isEnd"
|
|
* is not used yet, but here for symmetry.)
|
|
*/
|
|
void Textblock::drawText(core::View *view, core::style::Style *style,
|
|
core::style::Color::Shading shading, int x, int y,
|
|
const char *text, int start, int len, bool isStart,
|
|
bool isEnd)
|
|
{
|
|
if (len > 0) {
|
|
char *str = NULL;
|
|
|
|
switch (style->textTransform) {
|
|
case core::style::TEXT_TRANSFORM_NONE:
|
|
default:
|
|
break;
|
|
case core::style::TEXT_TRANSFORM_UPPERCASE:
|
|
str = layout->textToUpper(text + start, len);
|
|
break;
|
|
case core::style::TEXT_TRANSFORM_LOWERCASE:
|
|
str = layout->textToLower(text + start, len);
|
|
break;
|
|
case core::style::TEXT_TRANSFORM_CAPITALIZE:
|
|
// If "isStart" is false, the first letter of "text" is
|
|
// not the first letter of the "real" text word, so no
|
|
// transformation is necessary.
|
|
if (isStart) {
|
|
/* \bug No way to know about non-ASCII punctuation. */
|
|
bool initial_seen = false;
|
|
|
|
for (int i = 0; i < start; i++)
|
|
if (!ispunct(text[i]))
|
|
initial_seen = true;
|
|
if (initial_seen)
|
|
break;
|
|
|
|
int after = 0;
|
|
text += start;
|
|
while (ispunct(text[after]))
|
|
after++;
|
|
if (text[after])
|
|
after = layout->nextGlyph(text, after);
|
|
if (after > len)
|
|
after = len;
|
|
|
|
char *initial = layout->textToUpper(text, after);
|
|
int newlen = strlen(initial) + len-after;
|
|
str = (char *)malloc(newlen + 1);
|
|
strcpy(str, initial);
|
|
strncpy(str + strlen(str), text+after, len-after);
|
|
str[newlen] = '\0';
|
|
free(initial);
|
|
}
|
|
break;
|
|
}
|
|
|
|
view->drawText(style->font, style->color, shading, x, y,
|
|
str ? str : text + start, str ? strlen(str) : len);
|
|
if (str)
|
|
free(str);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Draw a word of text.
|
|
*
|
|
* Since hyphenated words consist of multiple instance of
|
|
* dw::Textblock::Word, which should be drawn as a whole (to preserve
|
|
* kerning etc.; see \ref dw-line-breaking), two indices are
|
|
* passed. See also drawLine(), where drawWord() is called.
|
|
*/
|
|
void Textblock::drawWord (Line *line, int wordIndex1, int wordIndex2,
|
|
core::View *view, core::Rectangle *area,
|
|
int xWidget, int yWidgetBase)
|
|
{
|
|
core::style::Style *style = words->getRef(wordIndex1)->style;
|
|
bool drawHyphen = wordIndex2 == line->lastWord
|
|
&& (words->getRef(wordIndex2)->flags & Word::DIV_CHAR_AT_EOL);
|
|
|
|
if (style->hasBackground ()) {
|
|
int w = 0;
|
|
for (int i = wordIndex1; i <= wordIndex2; i++)
|
|
w += words->getRef(i)->size.width;
|
|
w += words->getRef(wordIndex2)->hyphenWidth;
|
|
drawBox (view, style, area, xWidget, yWidgetBase - line->borderAscent,
|
|
w, line->borderAscent + line->borderDescent, false);
|
|
}
|
|
|
|
if (wordIndex1 == wordIndex2 && !drawHyphen) {
|
|
// Simple case, where copying in one buffer is not needed.
|
|
Word *word = words->getRef (wordIndex1);
|
|
drawWord0 (wordIndex1, wordIndex2, word->content.text, word->size.width,
|
|
false, style, view, area, xWidget, yWidgetBase);
|
|
} else {
|
|
// Concatenate all words in a new buffer.
|
|
int l = 0, totalWidth = 0;
|
|
for (int i = wordIndex1; i <= wordIndex2; i++) {
|
|
Word *w = words->getRef (i);
|
|
l += strlen (w->content.text);
|
|
totalWidth += w->size.width;
|
|
}
|
|
|
|
char *text = new char[l + (drawHyphen ? strlen (hyphenDrawChar) : 0) + 1];
|
|
int p = 0;
|
|
for (int i = wordIndex1; i <= wordIndex2; i++) {
|
|
const char * t = words->getRef(i)->content.text;
|
|
strcpy (text + p, t);
|
|
p += strlen (t);
|
|
}
|
|
|
|
if(drawHyphen) {
|
|
for (int i = 0; hyphenDrawChar[i]; i++)
|
|
text[p++] = hyphenDrawChar[i];
|
|
text[p++] = 0;
|
|
}
|
|
|
|
drawWord0 (wordIndex1, wordIndex2, text, totalWidth, drawHyphen,
|
|
style, view, area, xWidget, yWidgetBase);
|
|
|
|
delete[] text;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* TODO Comment
|
|
*/
|
|
void Textblock::drawWord0 (int wordIndex1, int wordIndex2,
|
|
const char *text, int totalWidth, bool drawHyphen,
|
|
core::style::Style *style, core::View *view,
|
|
core::Rectangle *area, int xWidget, int yWidgetBase)
|
|
{
|
|
int xWorld = allocation.x + xWidget;
|
|
int yWorldBase;
|
|
|
|
/* Adjust the text baseline if the word is <SUP>-ed or <SUB>-ed. */
|
|
if (style->valign == core::style::VALIGN_SUB)
|
|
yWidgetBase += style->font->ascent / 3;
|
|
else if (style->valign == core::style::VALIGN_SUPER) {
|
|
yWidgetBase -= style->font->ascent / 2;
|
|
}
|
|
yWorldBase = yWidgetBase + allocation.y;
|
|
|
|
bool isStartTotal = words->getRef(wordIndex1)->flags & Word::WORD_START;
|
|
bool isEndTotal = words->getRef(wordIndex2)->flags & Word::WORD_START;
|
|
drawText (view, style, core::style::Color::SHADING_NORMAL, xWorld,
|
|
yWorldBase, text, 0, strlen (text), isStartTotal, isEndTotal);
|
|
|
|
if (style->textDecoration)
|
|
decorateText(view, style, core::style::Color::SHADING_NORMAL, xWorld,
|
|
yWorldBase, totalWidth);
|
|
|
|
for (int layer = 0; layer < core::HIGHLIGHT_NUM_LAYERS; layer++) {
|
|
if (wordIndex1 <= hlEnd[layer].index &&
|
|
wordIndex2 >= hlStart[layer].index) {
|
|
const int wordLen = strlen (text);
|
|
int xStart, width;
|
|
int firstCharIdx;
|
|
int lastCharIdx;
|
|
|
|
if (hlStart[layer].index < wordIndex1)
|
|
firstCharIdx = 0;
|
|
else {
|
|
firstCharIdx =
|
|
std::min (hlStart[layer].nChar,
|
|
(int)strlen (words->getRef(hlStart[layer].index)
|
|
->content.text));
|
|
for (int i = wordIndex1; i < hlStart[layer].index; i++)
|
|
// It can be assumed that all words from wordIndex1 to
|
|
// wordIndex2 have content type TEXT.
|
|
firstCharIdx += strlen (words->getRef(i)->content.text);
|
|
}
|
|
|
|
if (hlEnd[layer].index > wordIndex2)
|
|
lastCharIdx = wordLen;
|
|
else {
|
|
lastCharIdx =
|
|
std::min (hlEnd[layer].nChar,
|
|
(int)strlen (words->getRef(hlEnd[layer].index)
|
|
->content.text));
|
|
for (int i = wordIndex1; i < hlEnd[layer].index; i++)
|
|
// It can be assumed that all words from wordIndex1 to
|
|
// wordIndex2 have content type TEXT.
|
|
lastCharIdx += strlen (words->getRef(i)->content.text);
|
|
}
|
|
|
|
xStart = xWorld;
|
|
if (firstCharIdx)
|
|
xStart += textWidth (text, 0, firstCharIdx, style,
|
|
isStartTotal,
|
|
isEndTotal && text[firstCharIdx] == 0);
|
|
// With a hyphen, the width is a bit longer than totalWidth,
|
|
// and so, the optimization to use totalWidth is not correct.
|
|
if (!drawHyphen && firstCharIdx == 0 && lastCharIdx == wordLen)
|
|
width = totalWidth;
|
|
else
|
|
width = textWidth (text, firstCharIdx,
|
|
lastCharIdx - firstCharIdx, style,
|
|
isStartTotal && firstCharIdx == 0,
|
|
isEndTotal && text[lastCharIdx] == 0);
|
|
if (width > 0) {
|
|
/* Highlight text */
|
|
core::style::Color *wordBgColor;
|
|
|
|
if (!(wordBgColor = style->backgroundColor))
|
|
wordBgColor = getBgColor();
|
|
|
|
/* Draw background for highlighted text. */
|
|
view->drawRectangle (
|
|
wordBgColor, core::style::Color::SHADING_INVERSE, true, xStart,
|
|
yWorldBase - style->font->ascent, width,
|
|
style->font->ascent + style->font->descent);
|
|
|
|
/* Highlight the text. */
|
|
drawText (view, style, core::style::Color::SHADING_INVERSE, xStart,
|
|
yWorldBase, text, firstCharIdx,
|
|
lastCharIdx - firstCharIdx,
|
|
isStartTotal && firstCharIdx == 0,
|
|
isEndTotal && lastCharIdx == wordLen);
|
|
|
|
if (style->textDecoration)
|
|
decorateText(view, style, core::style::Color::SHADING_INVERSE,
|
|
xStart, yWorldBase, width);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Draw a space.
|
|
*/
|
|
void Textblock::drawSpace(int wordIndex, core::View *view,
|
|
core::Rectangle *area, int xWidget, int yWidgetBase)
|
|
{
|
|
Word *word = words->getRef(wordIndex);
|
|
int xWorld = allocation.x + xWidget;
|
|
int yWorldBase;
|
|
core::style::Style *style = word->spaceStyle;
|
|
bool highlight = false;
|
|
|
|
/* Adjust the space baseline if it is <SUP>-ed or <SUB>-ed */
|
|
if (style->valign == core::style::VALIGN_SUB)
|
|
yWidgetBase += style->font->ascent / 3;
|
|
else if (style->valign == core::style::VALIGN_SUPER) {
|
|
yWidgetBase -= style->font->ascent / 2;
|
|
}
|
|
yWorldBase = allocation.y + yWidgetBase;
|
|
|
|
for (int layer = 0; layer < core::HIGHLIGHT_NUM_LAYERS; layer++) {
|
|
if (hlStart[layer].index <= wordIndex &&
|
|
hlEnd[layer].index > wordIndex) {
|
|
highlight = true;
|
|
break;
|
|
}
|
|
}
|
|
if (highlight) {
|
|
core::style::Color *spaceBgColor;
|
|
|
|
if (!(spaceBgColor = style->backgroundColor))
|
|
spaceBgColor = getBgColor();
|
|
|
|
view->drawRectangle (
|
|
spaceBgColor, core::style::Color::SHADING_INVERSE, true, xWorld,
|
|
yWorldBase - style->font->ascent, word->effSpace,
|
|
style->font->ascent + style->font->descent);
|
|
}
|
|
if (style->textDecoration) {
|
|
core::style::Color::Shading shading = highlight ?
|
|
core::style::Color::SHADING_INVERSE :
|
|
core::style::Color::SHADING_NORMAL;
|
|
|
|
decorateText(view, style, shading, xWorld, yWorldBase, word->effSpace);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Paint a line
|
|
* - x and y are toplevel dw coordinates (Question: what Dw? Changed. Test!)
|
|
* - area is used always (ev. set it to event->area)
|
|
* - event is only used when is_expose
|
|
*/
|
|
void Textblock::drawLine (Line *line, core::View *view, core::Rectangle *area,
|
|
core::DrawingContext *context)
|
|
{
|
|
DBG_OBJ_ENTER ("draw", 0, "drawLine", "..., [%d, %d, %d * %d]",
|
|
area->x, area->y, area->width, area->height);
|
|
|
|
int xWidget = line->textOffset;
|
|
int yWidgetBase = lineYOffsetWidget (line) + line->borderAscent;
|
|
|
|
DBG_OBJ_MSGF ("draw", 1, "line from %d to %d (%d words), at (%d, %d)",
|
|
line->firstWord, line->lastWord, words->size (),
|
|
xWidget, yWidgetBase);
|
|
DBG_MSG_WORD ("draw", 1, "<i>line starts with: </i>", line->firstWord, "");
|
|
DBG_MSG_WORD ("draw", 1, "<i>line ends with: </i>", line->lastWord, "");
|
|
|
|
DBG_OBJ_MSG_START ();
|
|
|
|
for (int wordIndex = line->firstWord;
|
|
wordIndex <= line->lastWord && xWidget < area->x + area->width;
|
|
wordIndex++) {
|
|
DBG_MSG_WORD ("draw", 2, "<i>drawing: </i>", wordIndex, "");
|
|
|
|
Word *word = words->getRef (wordIndex);
|
|
int wordSize = word->size.width;
|
|
|
|
if (xWidget + wordSize + word->hyphenWidth + word->effSpace >= area->x) {
|
|
if (word->content.type == core::Content::TEXT ||
|
|
word->content.type == core::Content::WIDGET_IN_FLOW) {
|
|
|
|
if (word->size.width > 0) {
|
|
if (word->content.type == core::Content::WIDGET_IN_FLOW) {
|
|
core::Widget *child = word->content.widget;
|
|
core::Rectangle childArea;
|
|
if (!core::StackingContextMgr::handledByStackingContextMgr
|
|
(child) &&
|
|
child->intersects (this, area, &childArea))
|
|
child->draw (view, &childArea, context);
|
|
} else {
|
|
int wordIndex2 = wordIndex;
|
|
while (wordIndex2 < line->lastWord &&
|
|
(words->getRef(wordIndex2)->flags
|
|
& Word::DRAW_AS_ONE_TEXT) &&
|
|
word->style == words->getRef(wordIndex2 + 1)->style)
|
|
wordIndex2++;
|
|
|
|
drawWord(line, wordIndex, wordIndex2, view, area,
|
|
xWidget, yWidgetBase);
|
|
wordSize = 0;
|
|
for (int i = wordIndex; i <= wordIndex2; i++)
|
|
wordSize += words->getRef(i)->size.width;
|
|
|
|
wordIndex = wordIndex2;
|
|
word = words->getRef(wordIndex);
|
|
}
|
|
}
|
|
|
|
if (word->effSpace > 0 && wordIndex < line->lastWord &&
|
|
words->getRef(wordIndex + 1)->content.type !=
|
|
core::Content::BREAK) {
|
|
if (word->spaceStyle->hasBackground ())
|
|
drawBox (view, word->spaceStyle, area,
|
|
xWidget + wordSize,
|
|
yWidgetBase - line->borderAscent, word->effSpace,
|
|
line->borderAscent + line->borderDescent, false);
|
|
drawSpace (wordIndex, view, area, xWidget + wordSize,
|
|
yWidgetBase);
|
|
}
|
|
|
|
}
|
|
}
|
|
xWidget += wordSize + word->effSpace;
|
|
}
|
|
|
|
DBG_OBJ_MSG_END ();
|
|
|
|
DBG_OBJ_LEAVE ();
|
|
}
|
|
|
|
/**
|
|
* Find the first line index that includes y, which is given in widget
|
|
* coordinates.
|
|
*/
|
|
int Textblock::findLineIndex (int y)
|
|
{
|
|
return wasAllocated () ?
|
|
findLineIndexWhenAllocated (y) : findLineIndexWhenNotAllocated (y);
|
|
}
|
|
|
|
int Textblock::findLineIndexWhenNotAllocated (int y)
|
|
{
|
|
if (lines->size() == 0)
|
|
return -1;
|
|
else
|
|
return
|
|
findLineIndex (y, calcVerticalBorder (getStyle()->padding.top,
|
|
getStyle()->borderWidth.top,
|
|
getStyle()->margin.top
|
|
+ extraSpace.top,
|
|
lines->getRef(0)->borderAscent,
|
|
lines->getRef(0)->marginAscent));
|
|
}
|
|
|
|
int Textblock::findLineIndexWhenAllocated (int y)
|
|
{
|
|
assert (wasAllocated ());
|
|
return findLineIndex (y, allocation.ascent);
|
|
}
|
|
|
|
int Textblock::findLineIndex (int y, int ascent)
|
|
{
|
|
DBG_OBJ_ENTER ("events", 0, "findLineIndex", "%d, %d", y, ascent);
|
|
|
|
core::Allocation alloc;
|
|
alloc.ascent = ascent; // More is not needed.
|
|
|
|
int maxIndex = lines->size () - 1;
|
|
int step, index, low = 0;
|
|
|
|
step = (lines->size() + 1) >> 1;
|
|
while ( step > 1 ) {
|
|
index = low + step;
|
|
if (index <= maxIndex && lineYOffsetWidget (index, &alloc) <= y)
|
|
low = index;
|
|
step = (step + 1) >> 1;
|
|
}
|
|
|
|
if (low < maxIndex && lineYOffsetWidget (low + 1, &alloc) <= y)
|
|
low++;
|
|
|
|
/*
|
|
* This new routine returns the line number between (top) and
|
|
* (top + size.ascent + size.descent + breakSpace): the space
|
|
* _below_ the line is considered part of the line. Old routine
|
|
* returned line number between (top - previous_line->breakSpace)
|
|
* and (top + size.ascent + size.descent): the space _above_ the
|
|
* line was considered part of the line. This is important for
|
|
* Dw_page_find_link() --EG
|
|
* That function has now been inlined into Dw_page_motion_notify() --JV
|
|
*/
|
|
|
|
DBG_OBJ_LEAVE_VAL ("%d", low);
|
|
return low;
|
|
}
|
|
|
|
/**
|
|
* \brief Find the line of word \em wordIndex.
|
|
*/
|
|
int Textblock::findLineOfWord (int wordIndex)
|
|
{
|
|
if (wordIndex < 0 || wordIndex >= words->size () ||
|
|
// Also regard not-yet-existing lines.
|
|
lines->size () <= 0 || wordIndex > lines->getLastRef()->lastWord)
|
|
return -1;
|
|
|
|
int high = lines->size () - 1, index, low = 0;
|
|
|
|
while (true) {
|
|
index = (low + high) / 2;
|
|
if (wordIndex >= lines->getRef(index)->firstWord) {
|
|
if (wordIndex <= lines->getRef(index)->lastWord)
|
|
return index;
|
|
else
|
|
low = index + 1;
|
|
} else
|
|
high = index - 1;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* \brief Find the paragraph of word \em wordIndex.
|
|
*/
|
|
int Textblock::findParagraphOfWord (int wordIndex)
|
|
{
|
|
int high = paragraphs->size () - 1, index, low = 0;
|
|
|
|
if (wordIndex < 0 || wordIndex >= words->size () ||
|
|
// It may be that the paragraphs list is incomplete. But look
|
|
// also at fillParagraphs, where this method is called.
|
|
(paragraphs->size () == 0 ||
|
|
wordIndex > paragraphs->getLastRef()->lastWord))
|
|
return -1;
|
|
|
|
while (true) {
|
|
index = (low + high) / 2;
|
|
if (wordIndex >= paragraphs->getRef(index)->firstWord) {
|
|
if (wordIndex <= paragraphs->getRef(index)->lastWord)
|
|
return index;
|
|
else
|
|
low = index + 1;
|
|
} else
|
|
high = index - 1;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* \brief Find the index of the word, or -1.
|
|
*/
|
|
Textblock::Word *Textblock::findWord (int x, int y, bool *inSpace)
|
|
{
|
|
int lineIndex, wordIndex;
|
|
int xCursor, lastXCursor, yWidgetBase;
|
|
Line *line;
|
|
Word *word;
|
|
|
|
*inSpace = false;
|
|
|
|
if ((lineIndex = findLineIndexWhenAllocated (y)) >= lines->size ())
|
|
return NULL;
|
|
line = lines->getRef (lineIndex);
|
|
yWidgetBase = lineYOffsetWidget (line) + line->borderAscent;
|
|
if (yWidgetBase + line->borderDescent <= y)
|
|
return NULL;
|
|
|
|
xCursor = line->textOffset;
|
|
for (wordIndex = line->firstWord; wordIndex <= line->lastWord;wordIndex++) {
|
|
word = words->getRef (wordIndex);
|
|
lastXCursor = xCursor;
|
|
xCursor += word->size.width + word->effSpace;
|
|
if (lastXCursor <= x && xCursor > x) {
|
|
if (x >= xCursor - word->effSpace) {
|
|
if (wordIndex < line->lastWord &&
|
|
(words->getRef(wordIndex + 1)->content.type !=
|
|
core::Content::BREAK) &&
|
|
y > yWidgetBase - word->spaceStyle->font->ascent &&
|
|
y <= yWidgetBase + word->spaceStyle->font->descent) {
|
|
*inSpace = true;
|
|
return word;
|
|
}
|
|
} else {
|
|
if (y > yWidgetBase - word->size.ascent &&
|
|
y <= yWidgetBase + word->size.descent)
|
|
return word;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
void Textblock::drawLevel (core::View *view, core::Rectangle *area,
|
|
int level, core::DrawingContext *context)
|
|
{
|
|
DBG_OBJ_ENTER ("draw", 0, "Textblock::drawLevel", "(%d, %d, %d * %d), %s",
|
|
area->x, area->y, area->width, area->height,
|
|
stackingLevelText (level));
|
|
|
|
switch (level) {
|
|
case SL_IN_FLOW:
|
|
for (int lineIndex = findLineIndexWhenAllocated (area->y);
|
|
lineIndex < lines->size (); lineIndex++) {
|
|
Line *line = lines->getRef (lineIndex);
|
|
if (lineYOffsetWidget (line) >= area->y + area->height)
|
|
break;
|
|
|
|
DBG_OBJ_MSGF ("draw", 0, "line %d (of %d)", lineIndex, lines->size ());
|
|
drawLine (line, view, area, context);
|
|
}
|
|
break;
|
|
|
|
case SL_OOF_REF:
|
|
// TODO Inefficient. Perhaps store OOF references in separate
|
|
// (much smaller!) list.
|
|
for (int oofmIndex = 0; oofmIndex < NUM_OOFM; oofmIndex++) {
|
|
for (int wordIndex = 0; wordIndex < words->size (); wordIndex++) {
|
|
Word *word = words->getRef (wordIndex);
|
|
if (word->content.type == core::Content::WIDGET_OOF_REF &&
|
|
getOOFMIndex (word->content.widgetReference->widget)
|
|
== oofmIndex &&
|
|
doesWidgetOOFInterruptDrawing (word->content.widget))
|
|
word->content.widget->drawInterruption (view, area, context);
|
|
}
|
|
}
|
|
break;
|
|
|
|
default:
|
|
OOFAwareWidget::drawLevel (view, area, level, context);
|
|
break;
|
|
}
|
|
|
|
DBG_OBJ_LEAVE ();
|
|
}
|
|
|
|
/**
|
|
* Add a new word (text, widget etc.) to a page.
|
|
*/
|
|
Textblock::Word *Textblock::addWord (int width, int ascent, int descent,
|
|
short flags, core::style::Style *style)
|
|
{
|
|
DBG_OBJ_ENTER ("construct.word", 0, "addWord", "%d * (%d + %d), %d, %p",
|
|
width, ascent, descent, flags, style);
|
|
|
|
if (lineBreakWidth == -1) {
|
|
lineBreakWidth = getAvailWidth (true);
|
|
DBG_OBJ_SET_NUM ("lineBreakWidth", lineBreakWidth);
|
|
}
|
|
|
|
words->increase ();
|
|
DBG_OBJ_SET_NUM ("words.size", words->size ());
|
|
int wordNo = words->size () - 1;
|
|
initWord (wordNo);
|
|
fillWord (wordNo, width, ascent, descent, flags, style);
|
|
|
|
DBG_OBJ_LEAVE ();
|
|
return words->getRef (wordNo);
|
|
}
|
|
|
|
/**
|
|
* Basic initialization, which is necessary before fillWord.
|
|
*/
|
|
void Textblock::initWord (int wordNo)
|
|
{
|
|
Word *word = words->getRef (wordNo);
|
|
|
|
word->style = word->spaceStyle = NULL;
|
|
word->wordImgRenderer = NULL;
|
|
word->spaceImgRenderer = NULL;
|
|
}
|
|
|
|
void Textblock::cleanupWord (int wordNo)
|
|
{
|
|
Word *word = words->getRef (wordNo);
|
|
|
|
if (word->content.type == core::Content::WIDGET_IN_FLOW)
|
|
delete word->content.widget;
|
|
if (word->content.type == core::Content::WIDGET_OOF_REF)
|
|
delete word->content.widgetReference;
|
|
/** \todo What about texts? */
|
|
|
|
removeWordImgRenderer (wordNo);
|
|
removeSpaceImgRenderer (wordNo);
|
|
|
|
word->style->unref ();
|
|
word->spaceStyle->unref ();
|
|
}
|
|
|
|
void Textblock::removeWordImgRenderer (int wordNo)
|
|
{
|
|
Word *word = words->getRef (wordNo);
|
|
|
|
if (word->style && word->wordImgRenderer) {
|
|
word->style->backgroundImage->removeExternalImgRenderer
|
|
(word->wordImgRenderer);
|
|
delete word->wordImgRenderer;
|
|
word->wordImgRenderer = NULL;
|
|
}
|
|
}
|
|
|
|
void Textblock::setWordImgRenderer (int wordNo)
|
|
{
|
|
Word *word = words->getRef (wordNo);
|
|
|
|
if (word->style->backgroundImage) {
|
|
word->wordImgRenderer= std::make_unique< WordImgRenderer >( this, wordNo ).release();
|
|
word->style->backgroundImage->putExternalImgRenderer
|
|
( std::unique_ptr< WordImgRenderer >{ word->wordImgRenderer } );
|
|
} else
|
|
word->wordImgRenderer = NULL;
|
|
}
|
|
|
|
void Textblock::removeSpaceImgRenderer (int wordNo)
|
|
{
|
|
Word *word = words->getRef (wordNo);
|
|
|
|
if (word->spaceStyle && word->spaceImgRenderer) {
|
|
word->spaceStyle->backgroundImage->removeExternalImgRenderer
|
|
(word->spaceImgRenderer);
|
|
delete word->spaceImgRenderer;
|
|
word->spaceImgRenderer = NULL;
|
|
}
|
|
}
|
|
|
|
void Textblock::setSpaceImgRenderer (int wordNo)
|
|
{
|
|
Word *word = words->getRef (wordNo);
|
|
|
|
if (word->spaceStyle->backgroundImage) {
|
|
word->spaceImgRenderer= std::make_unique< SpaceImgRenderer >( this, wordNo ).release();
|
|
word->spaceStyle->backgroundImage->putExternalImgRenderer
|
|
( std::unique_ptr< SpaceImgRenderer>{ word->spaceImgRenderer } );
|
|
} else
|
|
word->spaceImgRenderer = NULL;
|
|
}
|
|
|
|
void Textblock::fillWord (int wordNo, int width, int ascent, int descent,
|
|
short flags, core::style::Style *style)
|
|
{
|
|
Word *word = words->getRef (wordNo);
|
|
|
|
word->size.width = width;
|
|
word->size.ascent = ascent;
|
|
word->size.descent = descent;
|
|
DBG_SET_WORD_SIZE (wordNo);
|
|
word->origSpace = word->effSpace = 0;
|
|
word->hyphenWidth = 0;
|
|
word->badnessAndPenalty.setPenalty (PENALTY_PROHIBIT_BREAK);
|
|
word->content.space = false;
|
|
word->flags = flags;
|
|
|
|
removeWordImgRenderer (wordNo);
|
|
removeSpaceImgRenderer (wordNo);
|
|
|
|
word->style = style;
|
|
word->spaceStyle = style;
|
|
|
|
setWordImgRenderer (wordNo);
|
|
setSpaceImgRenderer (wordNo);
|
|
|
|
style->ref ();
|
|
style->ref ();
|
|
}
|
|
|
|
/*
|
|
* Get the width of a string of text.
|
|
*
|
|
* For "isStart" and "isEnd" see drawText.
|
|
*/
|
|
int Textblock::textWidth(const char *text, int start, int len,
|
|
core::style::Style *style, bool isStart, bool isEnd)
|
|
{
|
|
int ret = 0;
|
|
|
|
if (len > 0) {
|
|
char *str = NULL;
|
|
|
|
switch (style->textTransform) {
|
|
case core::style::TEXT_TRANSFORM_NONE:
|
|
default:
|
|
ret = layout->textWidth(style->font, text+start, len);
|
|
break;
|
|
case core::style::TEXT_TRANSFORM_UPPERCASE:
|
|
str = layout->textToUpper(text+start, len);
|
|
ret = layout->textWidth(style->font, str, strlen(str));
|
|
break;
|
|
case core::style::TEXT_TRANSFORM_LOWERCASE:
|
|
str = layout->textToLower(text+start, len);
|
|
ret = layout->textWidth(style->font, str, strlen(str));
|
|
break;
|
|
case core::style::TEXT_TRANSFORM_CAPITALIZE:
|
|
if (isStart) {
|
|
/* \bug No way to know about non-ASCII punctuation. */
|
|
bool initial_seen = false;
|
|
|
|
for (int i = 0; i < start; i++)
|
|
if (!ispunct(text[i]))
|
|
initial_seen = true;
|
|
if (initial_seen) {
|
|
ret = layout->textWidth(style->font, text+start, len);
|
|
} else {
|
|
int after = 0;
|
|
|
|
text += start;
|
|
while (ispunct(text[after]))
|
|
after++;
|
|
if (text[after])
|
|
after = layout->nextGlyph(text, after);
|
|
if (after > len)
|
|
after = len;
|
|
str = layout->textToUpper(text, after);
|
|
ret = layout->textWidth(style->font, str, strlen(str)) +
|
|
layout->textWidth(style->font, text+after, len-after);
|
|
}
|
|
} else
|
|
ret = layout->textWidth(style->font, text+start, len);
|
|
break;
|
|
}
|
|
|
|
if (str)
|
|
free(str);
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
/**
|
|
* Calculate the size of a text word.
|
|
*
|
|
* For "isStart" and "isEnd" see textWidth and drawText.
|
|
*/
|
|
void Textblock::calcTextSize (const char *text, size_t len,
|
|
core::style::Style *style,
|
|
core::Requisition *size, bool isStart, bool isEnd)
|
|
{
|
|
size->width = textWidth (text, 0, len, style, isStart, isEnd);
|
|
size->ascent = style->font->ascent;
|
|
size->descent = style->font->descent;
|
|
|
|
/*
|
|
* For 'normal' line height, just use ascent and descent from font.
|
|
* For absolute/percentage, line height is relative to font size, which
|
|
* is (irritatingly) smaller than ascent+descent.
|
|
*/
|
|
if (style->lineHeight != core::style::LENGTH_AUTO) {
|
|
int height, leading;
|
|
float factor = style->font->size;
|
|
|
|
factor /= (style->font->ascent + style->font->descent);
|
|
|
|
size->ascent = lout::misc::roundInt(size->ascent * factor);
|
|
size->descent = lout::misc::roundInt(size->descent * factor);
|
|
|
|
/* TODO: The containing block's line-height property gives a minimum
|
|
* height for the line boxes. (Even when it's set to 'normal', i.e.,
|
|
* AUTO? Apparently.) Once all block elements make Textblocks or
|
|
* something, this can be handled.
|
|
*/
|
|
if (core::style::isAbsLength (style->lineHeight))
|
|
height = core::style::absLengthVal(style->lineHeight);
|
|
else
|
|
height =
|
|
core::style::multiplyWithPerLengthRounded (style->font->size,
|
|
style->lineHeight);
|
|
leading = height - style->font->size;
|
|
|
|
size->ascent += leading / 2;
|
|
size->descent += leading - (leading / 2);
|
|
}
|
|
|
|
/* In case of a sub or super script we increase the word's height and
|
|
* potentially the line's height.
|
|
*/
|
|
if (style->valign == core::style::VALIGN_SUB) {
|
|
int requiredDescent = style->font->descent + style->font->ascent / 3;
|
|
size->descent = std::max (size->descent, requiredDescent);
|
|
} else if (style->valign == core::style::VALIGN_SUPER) {
|
|
int requiredAscent = style->font->ascent + style->font->ascent / 2;
|
|
size->ascent = std::max (size->ascent, requiredAscent);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add a word to the page structure. If it contains dividing
|
|
* characters (hard or soft hyphens, em-dashes, etc.), it is divided.
|
|
*/
|
|
void Textblock::addText (const char *text, size_t len,
|
|
core::style::Style *style)
|
|
{
|
|
DBG_OBJ_ENTER ("construct.word", 0, "addText", "..., %d, %p",
|
|
(int)len, style);
|
|
|
|
// Count dividing characters.
|
|
int numParts = 1;
|
|
|
|
for (const char *s = text; s; s = nextUtf8Char (s, text + len - s)) {
|
|
int foundDiv = -1;
|
|
for (int j = 0; foundDiv == -1 && j < NUM_DIV_CHARS; j++) {
|
|
int lDiv = strlen (divChars[j].s);
|
|
if (s <= text + len - lDiv) {
|
|
if (memcmp (s, divChars[j].s, lDiv * sizeof (char)) == 0)
|
|
foundDiv = j;
|
|
}
|
|
}
|
|
|
|
if (foundDiv != -1) {
|
|
if (divChars[foundDiv].penaltyIndexLeft != -1)
|
|
numParts ++;
|
|
if (divChars[foundDiv].penaltyIndexRight != -1)
|
|
numParts ++;
|
|
}
|
|
}
|
|
|
|
if (numParts == 1) {
|
|
// Simple (and common) case: no dividing characters. May still
|
|
// be hyphenated automatically.
|
|
core::Requisition size;
|
|
calcTextSize (text, len, style, &size, true, true);
|
|
addText0 (text, len,
|
|
Word::CAN_BE_HYPHENATED | Word::WORD_START | Word::WORD_END,
|
|
style, &size);
|
|
|
|
//printf ("[%p] %d: added simple word: ", this, words->size() - 1);
|
|
//printWordWithFlags (words->getLastRef());
|
|
//printf ("\n");
|
|
} else {
|
|
PRINTF ("HYPHENATION: '");
|
|
for (size_t i = 0; i < len; i++)
|
|
PUTCHAR(text[i]);
|
|
PRINTF ("', with %d parts\n", numParts);
|
|
|
|
// Store hyphen positions.
|
|
int n = 0, totalLenCharRemoved = 0;
|
|
int *partPenaltyIndex = new int[numParts - 1];
|
|
int *partStart = new int[numParts];
|
|
int *partEnd = new int[numParts];
|
|
bool *charRemoved = new bool[numParts - 1];
|
|
bool *canBeHyphenated = new bool[numParts + 1];
|
|
bool *permDivChar = new bool[numParts - 1];
|
|
bool *unbreakableForMinWidth = new bool[numParts - 1];
|
|
canBeHyphenated[0] = canBeHyphenated[numParts] = true;
|
|
partStart[0] = 0;
|
|
partEnd[numParts - 1] = len;
|
|
|
|
for (const char *s = text; s; s = nextUtf8Char (s, text + len - s)) {
|
|
int foundDiv = -1;
|
|
for (int j = 0; foundDiv == -1 && j < NUM_DIV_CHARS; j++) {
|
|
int lDiv = strlen (divChars[j].s);
|
|
if (s <= text + len - lDiv) {
|
|
if (memcmp (s, divChars[j].s, lDiv * sizeof (char)) == 0)
|
|
foundDiv = j;
|
|
}
|
|
}
|
|
|
|
if (foundDiv != -1) {
|
|
int lDiv = strlen (divChars[foundDiv].s);
|
|
|
|
if (divChars[foundDiv].charRemoved) {
|
|
assert (divChars[foundDiv].penaltyIndexLeft != -1);
|
|
assert (divChars[foundDiv].penaltyIndexRight == -1);
|
|
|
|
partPenaltyIndex[n] = divChars[foundDiv].penaltyIndexLeft;
|
|
charRemoved[n] = true;
|
|
permDivChar[n] = false;
|
|
unbreakableForMinWidth[n] =
|
|
divChars[foundDiv].unbreakableForMinWidth;
|
|
canBeHyphenated[n + 1] = divChars[foundDiv].canBeHyphenated;
|
|
partEnd[n] = s - text;
|
|
partStart[n + 1] = s - text + lDiv;
|
|
n++;
|
|
totalLenCharRemoved += lDiv;
|
|
} else {
|
|
assert (divChars[foundDiv].penaltyIndexLeft != -1 ||
|
|
divChars[foundDiv].penaltyIndexRight != -1);
|
|
|
|
if (divChars[foundDiv].penaltyIndexLeft != -1) {
|
|
partPenaltyIndex[n] = divChars[foundDiv].penaltyIndexLeft;
|
|
charRemoved[n] = false;
|
|
permDivChar[n] = false;
|
|
unbreakableForMinWidth[n] =
|
|
divChars[foundDiv].unbreakableForMinWidth;
|
|
canBeHyphenated[n + 1] = divChars[foundDiv].canBeHyphenated;
|
|
partEnd[n] = s - text;
|
|
partStart[n + 1] = s - text;
|
|
n++;
|
|
}
|
|
|
|
if (divChars[foundDiv].penaltyIndexRight != -1) {
|
|
partPenaltyIndex[n] = divChars[foundDiv].penaltyIndexRight;
|
|
charRemoved[n] = false;
|
|
permDivChar[n] = true;
|
|
unbreakableForMinWidth[n] =
|
|
divChars[foundDiv].unbreakableForMinWidth;
|
|
canBeHyphenated[n + 1] = divChars[foundDiv].canBeHyphenated;
|
|
partEnd[n] = s - text + lDiv;
|
|
partStart[n + 1] = s - text + lDiv;
|
|
n++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Get text without removed characters, e. g. hyphens.
|
|
const char *textWithoutHyphens;
|
|
char *textWithoutHyphensBuf = new char[len - totalLenCharRemoved];
|
|
int *breakPosWithoutHyphens;
|
|
int *breakPosWithoutHyphensBuf = new int[numParts - 1];
|
|
|
|
if (totalLenCharRemoved == 0) {
|
|
// No removed characters: take original arrays.
|
|
textWithoutHyphens = text;
|
|
// Ends are also break positions, except the last end, which
|
|
// is superfluous, but does not harm (since arrays in C/C++
|
|
// does not have an implicit length).
|
|
breakPosWithoutHyphens = partEnd;
|
|
} else {
|
|
// Copy into special buffers.
|
|
textWithoutHyphens = textWithoutHyphensBuf;
|
|
breakPosWithoutHyphens = breakPosWithoutHyphensBuf;
|
|
|
|
int n = 0;
|
|
for (int i = 0; i < numParts; i++) {
|
|
memmove (textWithoutHyphensBuf + n, text + partStart[i],
|
|
partEnd[i] - partStart[i]);
|
|
n += partEnd[i] - partStart[i];
|
|
if (i < numParts - 1)
|
|
breakPosWithoutHyphensBuf[i] = n;
|
|
}
|
|
}
|
|
|
|
PRINTF("H... without hyphens: '");
|
|
for (size_t i = 0; i < len - totalLenCharRemoved; i++)
|
|
PUTCHAR(textWithoutHyphens[i]);
|
|
PRINTF("'\n");
|
|
|
|
core::Requisition *wordSize = new core::Requisition[numParts];
|
|
calcTextSizes (textWithoutHyphens, len - totalLenCharRemoved, style,
|
|
numParts - 1, breakPosWithoutHyphens, wordSize);
|
|
|
|
// Finished!
|
|
for (int i = 0; i < numParts; i++) {
|
|
short flags = 0;
|
|
|
|
// If this parts adjoins at least one division characters,
|
|
// for which canBeHyphenated is set to false (this is the
|
|
// case for soft hyphens), do not hyphenate.
|
|
if (canBeHyphenated[i] && canBeHyphenated[i + 1])
|
|
flags |= Word::CAN_BE_HYPHENATED;
|
|
|
|
if(i < numParts - 1) {
|
|
if (charRemoved[i])
|
|
flags |= Word::DIV_CHAR_AT_EOL;
|
|
|
|
if (permDivChar[i])
|
|
flags |= Word::PERM_DIV_CHAR;
|
|
if (unbreakableForMinWidth[i])
|
|
flags |= Word::UNBREAKABLE_FOR_MIN_WIDTH;
|
|
|
|
flags |= Word::DRAW_AS_ONE_TEXT;
|
|
}
|
|
|
|
if (i == 0)
|
|
flags |= Word::WORD_START;
|
|
if (i == numParts - 1)
|
|
flags |= Word::WORD_END;
|
|
|
|
addText0 (text + partStart[i], partEnd[i] - partStart[i],
|
|
flags, style, &wordSize[i]);
|
|
|
|
//printf ("[%p] %d: added word part: ", this, words->size() - 1);
|
|
//printWordWithFlags (words->getLastRef());
|
|
//printf ("\n");
|
|
|
|
//PRINTF("H... [%d] '", i);
|
|
//for (int j = partStart[i]; j < partEnd[i]; j++)
|
|
// PUTCHAR(text[j]);
|
|
//PRINTF("' added\n");
|
|
|
|
if(i < numParts - 1) {
|
|
Word *word = words->getLastRef();
|
|
|
|
setBreakOption (word, style, penalties[partPenaltyIndex[i]][0],
|
|
penalties[partPenaltyIndex[i]][1], false);
|
|
DBG_SET_WORD (words->size () - 1);
|
|
|
|
if (charRemoved[i])
|
|
// Currently, only unconditional hyphens (UTF-8:
|
|
// "\xe2\x80\x90") can be used. See also drawWord, last
|
|
// section "if (drawHyphen)".
|
|
// Could be extended by adding respective members to
|
|
// DivChar and Word.
|
|
word->hyphenWidth =
|
|
layout->textWidth (word->style->font, hyphenDrawChar,
|
|
strlen (hyphenDrawChar));
|
|
|
|
accumulateWordData (words->size() - 1);
|
|
correctLastWordExtremes ();
|
|
}
|
|
}
|
|
|
|
delete[] partPenaltyIndex;
|
|
delete[] partStart;
|
|
delete[] partEnd;
|
|
delete[] charRemoved;
|
|
delete[] canBeHyphenated;
|
|
delete[] permDivChar;
|
|
delete[] unbreakableForMinWidth;
|
|
delete[] textWithoutHyphensBuf;
|
|
delete[] breakPosWithoutHyphensBuf;
|
|
delete[] wordSize;
|
|
}
|
|
|
|
DBG_OBJ_LEAVE ();
|
|
}
|
|
|
|
void Textblock::calcTextSizes (const char *text, size_t textLen,
|
|
core::style::Style *style,
|
|
int numBreaks, int *breakPos,
|
|
core::Requisition *wordSize)
|
|
{
|
|
// The size of the last part is calculated in a simple way.
|
|
int lastStart = breakPos[numBreaks - 1];
|
|
calcTextSize (text + lastStart, textLen - lastStart, style,
|
|
&wordSize[numBreaks], true, true);
|
|
|
|
PRINTF("H... [%d] '", numBreaks);
|
|
for (size_t i = 0; i < textLen - lastStart; i++)
|
|
PUTCHAR(text[i + lastStart]);
|
|
PRINTF("' -> %d\n", wordSize[numBreaks].width);
|
|
|
|
// The rest is more complicated. See dw-line-breaking, section
|
|
// "Hyphens".
|
|
for (int i = numBreaks - 1; i >= 0; i--) {
|
|
int start = (i == 0) ? 0 : breakPos[i - 1];
|
|
calcTextSize (text + start, textLen - start, style, &wordSize[i],
|
|
i == 0, i == numBreaks - 1);
|
|
|
|
PRINTF("H... [%d] '", i);
|
|
for (size_t j = 0; j < textLen - start; j++)
|
|
PUTCHAR(text[j + start]);
|
|
PRINTF("' -> %d\n", wordSize[i].width);
|
|
|
|
for (int j = i + 1; j < numBreaks + 1; j++) {
|
|
wordSize[i].width -= wordSize[j].width;
|
|
PRINTF("H... - %d = %d\n", wordSize[j].width, wordSize[i].width);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Calculate the size of a widget, and return whether it has to be
|
|
* positioned at the top of the line.
|
|
*/
|
|
bool Textblock::calcSizeOfWidgetInFlow (int wordIndex, Widget *widget,
|
|
core::Requisition *size)
|
|
{
|
|
DBG_OBJ_ENTER ("resize", 0, "calcSizeOfWidgetInFlow", "%d, %p, ...",
|
|
wordIndex, widget);
|
|
|
|
bool result, firstWordOfLine;
|
|
|
|
if (hasListitemValue)
|
|
// For list items, the word #0 at the beginning (bullet, number ...) has
|
|
// to be considered;
|
|
firstWordOfLine = wordIndex == 1 ||
|
|
(wordIndex > 0 &&
|
|
words->getRef(wordIndex - 1)->content.type == core::Content::BREAK);
|
|
else
|
|
firstWordOfLine = wordIndex == 0 ||
|
|
words->getRef(wordIndex - 1)->content.type == core::Content::BREAK;
|
|
|
|
DBG_OBJ_MSGF ("resize", 1, "firstWordOfLine = %s",
|
|
boolToStr (firstWordOfLine));
|
|
|
|
if (firstWordOfLine &&
|
|
(widget->getStyle()->display == core::style::DISPLAY_BLOCK ||
|
|
widget->getStyle()->display == core::style::DISPLAY_LIST_ITEM ||
|
|
widget->getStyle()->display == core::style::DISPLAY_TABLE)) {
|
|
// pass positions
|
|
DBG_OBJ_MSG ("resize", 1, "pass position");
|
|
int lastWord = lines->empty () ? -1 : lines->getLastRef()->lastWord;
|
|
assert (wordIndex > lastWord);
|
|
|
|
// The position passed to sizeRequest must be equivalent to the position
|
|
// passed later to sizeAllocate. This is more complicated for widgets
|
|
// which are centered or aligned to the right: here, we have to know the
|
|
// width to calculate the horizontal position. Both are calculated in an
|
|
// iterative way; initially, the left border is used (like for other,
|
|
// simpler alignments).
|
|
|
|
// Since the child widget will regard floats, we do not have to include
|
|
// floats when calculating left and right border.
|
|
|
|
// TODO Actually line1OffsetEff should be used instead of line1Offset, but
|
|
// it may not initialized here. Anyway, since ignoreLine1OffsetSometimes
|
|
// is not used, line1OffsetEff is always equal to line1Offset.
|
|
|
|
int leftBorder = boxOffsetX () + leftInnerPadding
|
|
+ (lines->size () == 0 ? line1Offset /* ...Eff, actually */ : 0);
|
|
int rightBorder = boxRestWidth ();
|
|
|
|
int lastMargin, yLine = yOffsetOfLineToBeCreated (&lastMargin);
|
|
int yRel = yLine - std::min (lastMargin, widget->getStyle()->margin.top);
|
|
|
|
DBG_OBJ_MSGF ("resize", 1,
|
|
"leftBorder = %d + %d + (%d == 0 ? %d : 0) = %d, "
|
|
"rightBorder = %d, yRel = %d - min (%d, %d) = %d",
|
|
boxOffsetX (), leftInnerPadding , lines->size (),
|
|
line1OffsetEff, leftBorder, rightBorder, yLine, lastMargin,
|
|
widget->getStyle()->margin.top, yRel);
|
|
|
|
core::SizeParams childParams;
|
|
DBG_OBJ_ASSOC_CHILD (&childParams);
|
|
|
|
int oldXRel = leftBorder;
|
|
|
|
sizeRequestParams.forChild (this, widget, oldXRel, yRel, &childParams);
|
|
widget->sizeRequest (size, childParams.getNumPos (),
|
|
childParams.getReferences (), childParams.getX (),
|
|
childParams.getY ());
|
|
|
|
DBG_OBJ_MSG_START ();
|
|
|
|
while (true) {
|
|
DBG_OBJ_MSG_START ();
|
|
|
|
int xRel;
|
|
|
|
switch(widget->getStyle()->textAlign) {
|
|
case core::style::TEXT_ALIGN_LEFT:
|
|
case core::style::TEXT_ALIGN_STRING: // see comment in alignLine()
|
|
case core::style::TEXT_ALIGN_JUSTIFY: // equivalent for only one word
|
|
default: // compiler happiness
|
|
xRel = leftBorder;
|
|
break;
|
|
|
|
case core::style::TEXT_ALIGN_RIGHT:
|
|
xRel = lineBreakWidth - rightBorder - size->width;
|
|
break;
|
|
|
|
case core::style::TEXT_ALIGN_CENTER:
|
|
xRel =
|
|
(leftBorder + lineBreakWidth - rightBorder - size->width) / 2;
|
|
break;
|
|
}
|
|
|
|
// Cf. Textblock::calcTextOffset().
|
|
if (xRel < leftBorder)
|
|
xRel = leftBorder;
|
|
|
|
DBG_OBJ_MSGF ("resize", 2, "xRel = %d, oldXRel = %d", xRel, oldXRel);
|
|
|
|
// Stop when the value of xRel has not changed during the last
|
|
// iteration.
|
|
if (xRel == oldXRel)
|
|
break;
|
|
|
|
sizeRequestParams.forChild (this, widget, xRel, yRel, &childParams);
|
|
widget->sizeRequest (size, childParams.getNumPos (),
|
|
childParams.getReferences (), childParams.getX (),
|
|
childParams.getY ());
|
|
|
|
oldXRel = xRel;
|
|
|
|
DBG_OBJ_MSG_END ();
|
|
}
|
|
|
|
DBG_OBJ_MSG_END ();
|
|
|
|
result = true;
|
|
} else {
|
|
// do not pass positions (inline elements etc)
|
|
DBG_OBJ_MSG ("resize", 1, "do not pass position");
|
|
widget->sizeRequest (size);
|
|
result = false;
|
|
}
|
|
|
|
DBG_OBJ_LEAVE_VAL ("%s", boolToStr (result));
|
|
return result;
|
|
}
|
|
|
|
bool Textblock::findSizeRequestReference (Widget *reference, int *xRef,
|
|
int *yRef)
|
|
{
|
|
if (reference == this) {
|
|
if (xRef)
|
|
*xRef = 0;
|
|
if (yRef)
|
|
*yRef = 0;
|
|
return true;
|
|
} else
|
|
return sizeRequestParams.findReference (reference, xRef, yRef);
|
|
}
|
|
|
|
/**
|
|
* Add a word (without hyphens) to the page structure.
|
|
*/
|
|
void Textblock::addText0 (const char *text, size_t len, short flags,
|
|
core::style::Style *style, core::Requisition *size)
|
|
{
|
|
DBG_OBJ_ENTER ("construct.word", 0, "addText0",
|
|
"..., %d, %s:%s:%s:%s:%s:%s:%s:%s, %p, %d * (%d + %d)",
|
|
(int)len,
|
|
(flags & Word::CAN_BE_HYPHENATED) ? "h?" : "--",
|
|
(flags & Word::DIV_CHAR_AT_EOL) ? "de" : "--",
|
|
(flags & Word::PERM_DIV_CHAR) ? "dp" : "--",
|
|
(flags & Word::DRAW_AS_ONE_TEXT) ? "t1" : "--",
|
|
(flags & Word::UNBREAKABLE_FOR_MIN_WIDTH) ? "um" : "--",
|
|
(flags & Word::WORD_START) ? "st" : "--",
|
|
(flags & Word::WORD_END) ? "en" : "--",
|
|
(flags & Word::TOPLEFT_OF_LINE) ? "00" : "--",
|
|
style, size->width, size->ascent, size->descent);
|
|
|
|
//printf("[%p] addText0 ('", this);
|
|
//for (size_t i = 0; i < len; i++)
|
|
// putchar(text[i]);
|
|
//printf("', ");
|
|
//printWordFlags (flags);
|
|
//printf (", ...)\n");
|
|
|
|
Word *word = addWord (size->width, size->ascent, size->descent,
|
|
flags, style);
|
|
DBG_OBJ_ASSOC_CHILD (style);
|
|
word->content.type = core::Content::TEXT;
|
|
word->content.text = layout->textZone->strndup(text, len);
|
|
|
|
DBG_SET_WORD (words->size () - 1);
|
|
|
|
// The following debug message may be useful to identify the
|
|
// different textblocks.
|
|
|
|
//if (words->size() == 1)
|
|
// printf ("[%p] first word: '%s'\n", this, text);
|
|
|
|
processWord (words->size () - 1);
|
|
|
|
DBG_OBJ_LEAVE ();
|
|
}
|
|
|
|
/**
|
|
* Add a widget (word type) to the page.
|
|
*/
|
|
void Textblock::addWidget (core::Widget *widget, core::style::Style *style)
|
|
{
|
|
DBG_OBJ_ENTER ("construct.word", 0, "addWidget", "%p, %p", widget, style);
|
|
|
|
/* We first assign -1 as parent_ref, since the call of widget->size_request
|
|
* will otherwise let this Textblock be rewrapped from the beginning.
|
|
* (parent_ref is actually undefined, but likely has the value 0.) At the,
|
|
* end of this function, the correct value is assigned. */
|
|
widget->parentRef = -1;
|
|
DBG_OBJ_SET_NUM_O (widget, "parentRef", widget->parentRef);
|
|
|
|
widget->setStyle (style);
|
|
|
|
initOutOfFlowMgrs ();
|
|
|
|
if (testWidgetOutOfFlow (widget)) {
|
|
int oofmIndex = getOOFMIndex (widget);
|
|
DBG_OBJ_MSGF ("construct.word", 1, "ouf of flow: oofmIndex = %d (%s)",
|
|
oofmIndex, OOFM_NAME[oofmIndex]);
|
|
|
|
widget->setParent (oofContainer[oofmIndex]);
|
|
widget->setGenerator (this);
|
|
|
|
oof::OutOfFlowMgr *oofm = searchOutOfFlowMgr (oofmIndex);
|
|
int oofmSubRef = oofm->addWidgetOOF (widget, this, words->size ());
|
|
widget->parentRef = makeParentRefOOF (oofmIndex, oofmSubRef);
|
|
|
|
DBG_OBJ_MSGF ("construct.word", 1, "oofmSubRef = %d => parentRef = %d",
|
|
oofmSubRef, widget->parentRef);
|
|
|
|
core::Requisition size;
|
|
oofm->calcWidgetRefSize (widget, &size);
|
|
Word *word = addWord (size.width, size.ascent, size.descent, 0, style);
|
|
word->content.type = core::Content::WIDGET_OOF_REF;
|
|
word->content.widgetReference = new core::WidgetReference (widget);
|
|
widget->setWidgetReference (word->content.widgetReference);
|
|
|
|
// After a out-of-flow reference, breaking is allowed. (This avoids some
|
|
// problems with breaking near float definitions.)
|
|
setBreakOption (word, style, 0, 0, false);
|
|
} else {
|
|
DBG_OBJ_MSG ("construct.word", 1, "in flow");
|
|
|
|
widget->setParent (this);
|
|
|
|
// TODO Replace (perhaps) later "textblock" by "OOF aware widget".
|
|
if (auto *const tb= dynamic_cast< Textblock * >( widget )) {
|
|
for (int i = 0; i < NUM_OOFM; i++)
|
|
searchOutOfFlowMgr(i)->addWidgetInFlow (tb, this,
|
|
words->size ());
|
|
}
|
|
|
|
core::Requisition size;
|
|
short flags = calcSizeOfWidgetInFlow (words->size (), widget, &size) ?
|
|
Word::TOPLEFT_OF_LINE : 0;
|
|
Word *word =
|
|
addWord (size.width, size.ascent, size.descent, flags, style);
|
|
word->content.type = core::Content::WIDGET_IN_FLOW;
|
|
word->content.widget = widget;
|
|
}
|
|
|
|
DBG_SET_WORD (words->size () - 1);
|
|
|
|
processWord (words->size () - 1);
|
|
//DBG_OBJ_SET_NUM (word->content.widget, "parent_ref",
|
|
// word->content.widget->parent_ref);
|
|
|
|
//DEBUG_MSG(DEBUG_REWRAP_LEVEL,
|
|
// "Assigning parent_ref = %d to added word %d, "
|
|
// "in page with %d word(s)\n",
|
|
// lines->size () - 1, words->size() - 1, words->size());
|
|
|
|
DBG_OBJ_LEAVE ();
|
|
}
|
|
|
|
/**
|
|
* Add an anchor to the page. "name" is copied, so no dStrdup is necessary for
|
|
* the caller.
|
|
*
|
|
* Return true on success, and false, when this anchor had already been
|
|
* added to the widget tree.
|
|
*/
|
|
bool Textblock::addAnchor (const std::string &name, core::style::Style *style)
|
|
{
|
|
DBG_OBJ_ENTER ("construct.word", 0, "addAnchor", "\"%s\", %p", name.c_str(), style);
|
|
|
|
std::optional< std::string > copy;
|
|
int y;
|
|
bool result;
|
|
|
|
// Since an anchor does not take any space, it is safe to call
|
|
// addAnchor already here.
|
|
if (wasAllocated ()) {
|
|
if (lines->size () == 0)
|
|
y = allocation.y;
|
|
else
|
|
y = allocation.y + lineYOffsetWidget (lines->size () - 1);
|
|
copy = Widget::addAnchor (name, y);
|
|
} else
|
|
copy = Widget::addAnchor (name);
|
|
|
|
if (not copy.has_value())
|
|
/**
|
|
* \todo It may be necessary for future uses to save the anchor in
|
|
* some way, e.g. when parts of the widget tree change.
|
|
*/
|
|
result = false;
|
|
else {
|
|
anchors.emplace_back();
|
|
Anchor &anchor= anchors.back();
|
|
|
|
anchor.name = copy.value();
|
|
anchor.wordIndex = words->size();
|
|
result = true;
|
|
}
|
|
|
|
DBG_OBJ_LEAVE_VAL ("%s", boolToStr(result));
|
|
return result;
|
|
}
|
|
|
|
|
|
/**
|
|
* ?
|
|
*/
|
|
void Textblock::addSpace (core::style::Style *style)
|
|
{
|
|
DBG_OBJ_ENTER ("construct.word", 0, "addSpace", "%p", style);
|
|
|
|
int wordIndex = words->size () - 1;
|
|
if (wordIndex >= 0) {
|
|
fillSpace (wordIndex, style);
|
|
DBG_SET_WORD (wordIndex);
|
|
accumulateWordData (wordIndex);
|
|
correctLastWordExtremes ();
|
|
}
|
|
|
|
DBG_OBJ_LEAVE ();
|
|
}
|
|
|
|
/**
|
|
* Add a break option (see setBreakOption() for details). Used instead
|
|
* of addSpace for ideographic characters.
|
|
*
|
|
* When "forceBreak" is true, a break is even possible within PRE etc.
|
|
*/
|
|
void Textblock::addBreakOption (core::style::Style *style, bool forceBreak)
|
|
{
|
|
DBG_OBJ_ENTER ("construct.word", 0, "addBreakOption", "%p, %s",
|
|
style, forceBreak ? "true" : "false");
|
|
|
|
int wordIndex = words->size () - 1;
|
|
if (wordIndex >= 0) {
|
|
setBreakOption (words->getRef(wordIndex), style, 0, 0, forceBreak);
|
|
DBG_SET_WORD (wordIndex);
|
|
// Call of accumulateWordData() is not needed here.
|
|
correctLastWordExtremes ();
|
|
}
|
|
|
|
DBG_OBJ_LEAVE ();
|
|
}
|
|
|
|
void Textblock::fillSpace (int wordNo, core::style::Style *style)
|
|
{
|
|
DBG_OBJ_ENTER ("construct.word", 0, "fillSpace", "%d, ...", wordNo);
|
|
|
|
DBG_OBJ_MSGF ("construct.word", 1, "style.white-space = %s",
|
|
style->whiteSpace == core::style::WHITE_SPACE_NORMAL ? "normal"
|
|
: style->whiteSpace == core::style::WHITE_SPACE_PRE ? "pre"
|
|
: style->whiteSpace == core::style::WHITE_SPACE_NOWRAP ?
|
|
"nowrap"
|
|
: style->whiteSpace == core::style::WHITE_SPACE_PRE_WRAP ?
|
|
"pre-wrap"
|
|
: style->whiteSpace == core::style::WHITE_SPACE_PRE_LINE ?
|
|
"pre-line" : "???");
|
|
|
|
// Old comment:
|
|
//
|
|
// According to
|
|
// http://www.w3.org/TR/CSS2/text.html#white-space-model: "line
|
|
// breaking opportunities are determined based on the text
|
|
// prior to the white space collapsing steps".
|
|
//
|
|
// So we call addBreakOption () for each Textblock::addSpace ()
|
|
// call. This is important e.g. to be able to break between
|
|
// foo and bar in: <span style="white-space:nowrap">foo </span>
|
|
// bar
|
|
//
|
|
// TODO: Re-evaluate again.
|
|
|
|
Word *word = words->getRef (wordNo);
|
|
|
|
// TODO: This line does not work: addBreakOption (word, style);
|
|
|
|
if (// Do not override a previously set break penalty:
|
|
!word->content.space &&
|
|
// OOF references are considered specially, and must not have a space:
|
|
word->content.type != core::Content::WIDGET_OOF_REF) {
|
|
setBreakOption (word, style, 0, 0, false);
|
|
|
|
word->content.space = true;
|
|
word->origSpace = word->effSpace =
|
|
style->font->spaceWidth + style->wordSpacing;
|
|
|
|
removeSpaceImgRenderer (wordNo);
|
|
|
|
word->spaceStyle->unref ();
|
|
word->spaceStyle = style;
|
|
style->ref ();
|
|
|
|
setSpaceImgRenderer (wordNo);
|
|
}
|
|
|
|
DBG_OBJ_LEAVE ();
|
|
}
|
|
|
|
/**
|
|
* Set a break option, if allowed by the style. Called by fillSpace
|
|
* (and so addSpace), but may be called, via addBreakOption(), as an
|
|
* alternative, e. g. for ideographic characters.
|
|
*/
|
|
void Textblock::setBreakOption (Word *word, core::style::Style *style,
|
|
int breakPenalty1, int breakPenalty2,
|
|
bool forceBreak)
|
|
{
|
|
DBG_OBJ_ENTER ("construct.word", 0, "setBreakOption", "..., %d, %d, %s",
|
|
breakPenalty1, breakPenalty2, forceBreak ? "true" : "false");
|
|
|
|
// TODO: lineMustBeBroken should be independent of the penalty
|
|
// index? Otherwise, examine the last line.
|
|
if (!word->badnessAndPenalty.lineMustBeBroken(0)) {
|
|
if (forceBreak || isBreakAllowed (style))
|
|
word->badnessAndPenalty.setPenalties (breakPenalty1, breakPenalty2);
|
|
else
|
|
word->badnessAndPenalty.setPenalty (PENALTY_PROHIBIT_BREAK);
|
|
}
|
|
|
|
DBG_OBJ_LEAVE ();
|
|
}
|
|
|
|
bool Textblock::isBreakAllowed (core::style::Style *style)
|
|
{
|
|
switch (style->whiteSpace) {
|
|
case core::style::WHITE_SPACE_NORMAL:
|
|
case core::style::WHITE_SPACE_PRE_LINE:
|
|
case core::style::WHITE_SPACE_PRE_WRAP:
|
|
return true;
|
|
|
|
case core::style::WHITE_SPACE_PRE:
|
|
case core::style::WHITE_SPACE_NOWRAP:
|
|
return false;
|
|
|
|
default:
|
|
// compiler happiness
|
|
lout::misc::assertNotReached ();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Cause a paragraph break
|
|
*/
|
|
void Textblock::addParbreak (int space, core::style::Style *style)
|
|
{
|
|
DBG_OBJ_ENTER ("construct.word", 0, "addParbreak", "%d, %p",
|
|
space, style);
|
|
DBG_OBJ_MSG ("construct.word", 0,
|
|
"<i>No nesting! Stack trace may be incomplete.</i>");
|
|
DBG_OBJ_LEAVE ();
|
|
|
|
Word *word;
|
|
|
|
/* A break may not be the first word of a page, or directly after
|
|
the bullet/number (which is the first word) in a list item. (See
|
|
also comment in sizeRequest.) */
|
|
if (words->size () == 0 ||
|
|
(hasListitemValue && words->size () == 1)) {
|
|
/* This is a bit hackish: If a break is added as the
|
|
first/second word of a page, and the parent widget is also a
|
|
Textblock, and there is a break before -- this is the case when
|
|
a widget is used as a text box (lists, blockquotes, list
|
|
items etc) -- then we simply adjust the break before, in a
|
|
way that the space is in any case visible. */
|
|
/* Find the widget where to adjust the breakSpace. (Only
|
|
consider normal flow, no floats etc.) */
|
|
Textblock *textblock2= nullptr;
|
|
for (Widget *widget = this;
|
|
( textblock2= dynamic_cast< Textblock * >( widget->getParent() ) ) &&
|
|
!isWidgetOOF (widget);
|
|
widget = widget->getParent ()) {
|
|
int index = textblock2->hasListitemValue ? 1 : 0;
|
|
bool isfirst = (textblock2->words->getRef(index)->content.type
|
|
== core::Content::WIDGET_IN_FLOW
|
|
&& textblock2->words->getRef(index)->content.widget
|
|
== widget);
|
|
if (!isfirst) {
|
|
/* The text block we searched for has been found. */
|
|
Word *word2;
|
|
int lineno = getWidgetInFlowSubRef (widget);
|
|
|
|
if (lineno > 0 &&
|
|
(word2 =
|
|
textblock2->words->getRef(textblock2->lines
|
|
->getRef(lineno - 1)->firstWord)) &&
|
|
word2->content.type == core::Content::BREAK) {
|
|
if (word2->content.breakSpace < space) {
|
|
word2->content.breakSpace = space;
|
|
textblock2->queueResize (makeParentRefInFlow (lineno), false);
|
|
textblock2->mustQueueResize = false;
|
|
DBG_OBJ_SET_BOOL_O (textblock2, "mustQueueResize",
|
|
textblock2->mustQueueResize);
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
/* Otherwise continue to examine parents. */
|
|
}
|
|
|
|
/* Return in any case. */
|
|
return;
|
|
}
|
|
|
|
/* Another break before? */
|
|
if ((word = words->getRef(words->size () - 1)) &&
|
|
word->content.type == core::Content::BREAK) {
|
|
Line *lastLine = lines->getRef (lines->size () - 1);
|
|
|
|
word->content.breakSpace =
|
|
std::max (word->content.breakSpace, space);
|
|
lastLine->breakSpace =
|
|
std::max( std::max( word->content.breakSpace,
|
|
lastLine->marginDescent - lastLine->borderDescent ),
|
|
lastLine->breakSpace);
|
|
return;
|
|
}
|
|
|
|
word = addWord (0, 0, 0, 0, style);
|
|
DBG_OBJ_ASSOC_CHILD (style);
|
|
word->content.type = core::Content::BREAK;
|
|
word->badnessAndPenalty.setPenalty (PENALTY_FORCE_BREAK);
|
|
word->content.breakSpace = space;
|
|
|
|
DBG_SET_WORD (words->size () - 1);
|
|
|
|
breakAdded ();
|
|
processWord (words->size () - 1);
|
|
}
|
|
|
|
/*
|
|
* Cause a line break.
|
|
*/
|
|
void Textblock::addLinebreak (core::style::Style *style)
|
|
{
|
|
DBG_OBJ_ENTER ("construct.word", 0, "addLinebreak", "%p", style);
|
|
|
|
Word *word;
|
|
|
|
if (words->size () == 0 ||
|
|
words->getRef(words->size () - 1)->content.type == core::Content::BREAK)
|
|
// An <BR> in an empty line gets the height of the current font
|
|
// (why would someone else place it here?), ...
|
|
word =
|
|
addWord (0, style->font->ascent, style->font->descent, 0, style);
|
|
else
|
|
// ... otherwise, it has no size (and does not enlarge the line).
|
|
word = addWord (0, 0, 0, 0, style);
|
|
|
|
DBG_OBJ_ASSOC_CHILD (style);
|
|
|
|
word->content.type = core::Content::BREAK;
|
|
word->badnessAndPenalty.setPenalty (PENALTY_FORCE_BREAK);
|
|
word->content.breakSpace = 0;
|
|
|
|
DBG_SET_WORD (words->size () - 1);
|
|
|
|
breakAdded ();
|
|
processWord (words->size () - 1);
|
|
|
|
DBG_OBJ_LEAVE ();
|
|
}
|
|
|
|
/**
|
|
* Called directly after a (line or paragraph) break has been added.
|
|
*/
|
|
void Textblock::breakAdded ()
|
|
{
|
|
assert (words->size () >= 1);
|
|
assert (words->getRef(words->size () - 1)->content.type
|
|
== core::Content::BREAK);
|
|
|
|
// Any space before is removed. It is not used; on the other hand,
|
|
// this snippet (an example from a real-world debugging session)
|
|
// would cause problems:
|
|
//
|
|
// <input style="width: 100%" .../>
|
|
// <button ...>...</button>
|
|
//
|
|
// (Notice the space between <input> and <button>, and also that
|
|
// the HTML parser will insert a BREAK between them.) The <input>
|
|
// would be given the available width ("width: 100%"), but the
|
|
// actual width (Word::totalWidth) would include the space, so that
|
|
// the width of the line is larger than the available width.
|
|
|
|
if (words->size () >= 2)
|
|
words->getRef(words->size () - 2)->origSpace =
|
|
words->getRef(words->size () - 2)->effSpace = 0;
|
|
}
|
|
|
|
core::Widget *Textblock::getWidgetAtPointLevel (int x, int y, int level,
|
|
core::GettingWidgetAtPointContext
|
|
*context)
|
|
{
|
|
DBG_OBJ_ENTER ("events", 0, "Textblock::getWidgetAtPointLevel", "%d, %d, %s",
|
|
x, y, stackingLevelText (level));
|
|
|
|
Widget *widgetAtPoint = NULL;
|
|
|
|
switch (level) {
|
|
case SL_IN_FLOW:
|
|
{
|
|
int lineIndex = findLineIndexWhenAllocated (y - allocation.y);
|
|
|
|
if (lineIndex >= 0 && lineIndex < lines->size ()) {
|
|
Line *line = lines->getRef (lineIndex);
|
|
|
|
for (int wordIndex = line->lastWord;
|
|
widgetAtPoint == NULL && wordIndex >= line->firstWord;
|
|
wordIndex--) {
|
|
Word *word = words->getRef (wordIndex);
|
|
if (word->content.type == core::Content::WIDGET_IN_FLOW &&
|
|
!core::StackingContextMgr::handledByStackingContextMgr
|
|
(word->content.widget))
|
|
widgetAtPoint =
|
|
word->content.widget->getWidgetAtPoint (x, y, context);
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
|
|
case SL_OOF_REF:
|
|
// TODO Inefficient. Perhaps store OOF references in separate
|
|
// (much smaller!) list.
|
|
for (int oofmIndex = NUM_OOFM; widgetAtPoint == NULL && oofmIndex >= 0;
|
|
oofmIndex--) {
|
|
for (int wordIndex = words->size () - 1;
|
|
widgetAtPoint == NULL && wordIndex >= 0; wordIndex--) {
|
|
Word *word = words->getRef (wordIndex);
|
|
if (word->content.type == core::Content::WIDGET_OOF_REF &&
|
|
getOOFMIndex (word->content.widgetReference->widget)
|
|
== oofmIndex &&
|
|
doesWidgetOOFInterruptDrawing (word->content.widgetReference
|
|
->widget))
|
|
widgetAtPoint =
|
|
word->content.widgetReference->widget
|
|
->getWidgetAtPointInterrupted (x, y, context);
|
|
}
|
|
}
|
|
break;
|
|
|
|
default:
|
|
widgetAtPoint =
|
|
OOFAwareWidget::getWidgetAtPointLevel (x, y, level, context);
|
|
break;
|
|
}
|
|
|
|
DBG_OBJ_LEAVE_VAL ("%p", widgetAtPoint);
|
|
return widgetAtPoint;
|
|
}
|
|
|
|
/**
|
|
* This function "hands" the last break of a page "over" to a parent
|
|
* page. This is used for "collapsing spaces".
|
|
*/
|
|
void Textblock::handOverBreak (core::style::Style *style)
|
|
{
|
|
if (lines->size() > 0) {
|
|
Widget *parent;
|
|
Line *lastLine = lines->getRef (lines->size () - 1);
|
|
|
|
if (Textblock *textblock2;
|
|
lastLine->breakSpace != 0 && (parent = getParent()) &&
|
|
( textblock2= dynamic_cast< Textblock * >( parent ) ) &&
|
|
parent->getStyle()->display != core::style::DISPLAY_BLOCK) {
|
|
textblock2->addParbreak(lastLine->breakSpace, style);
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Any words added by addWord() are not immediately (queued to
|
|
* be) drawn, instead, this function must be called. This saves some
|
|
* calls to queueResize().
|
|
*
|
|
*/
|
|
void Textblock::flush ()
|
|
{
|
|
DBG_OBJ_ENTER0 ("resize", 0, "flush");
|
|
|
|
if (mustQueueResize) {
|
|
DBG_OBJ_MSG ("resize", 0, "mustQueueResize set");
|
|
|
|
queueResize (-1, true);
|
|
mustQueueResize = false;
|
|
DBG_OBJ_SET_BOOL ("mustQueueResize", mustQueueResize);
|
|
}
|
|
|
|
DBG_OBJ_LEAVE ();
|
|
}
|
|
|
|
|
|
// next: Dw_page_find_word
|
|
|
|
void Textblock::changeLinkColor (int link, int newColor)
|
|
{
|
|
for (int lineIndex = 0; lineIndex < lines->size(); lineIndex++) {
|
|
bool changed = false;
|
|
Line *line = lines->getRef (lineIndex);
|
|
int wordIdx;
|
|
|
|
for (wordIdx = line->firstWord; wordIdx <= line->lastWord; wordIdx++){
|
|
Word *word = words->getRef(wordIdx);
|
|
|
|
if (word->style->x_link == link) {
|
|
core::style::StyleAttrs styleAttrs;
|
|
|
|
switch (word->content.type) {
|
|
case core::Content::TEXT:
|
|
{ core::style::Style *old_style = word->style;
|
|
styleAttrs = *old_style;
|
|
styleAttrs.color = core::style::Color::create (layout,
|
|
newColor);
|
|
word->style = core::style::Style::create (&styleAttrs);
|
|
old_style->unref();
|
|
old_style = word->spaceStyle;
|
|
styleAttrs = *old_style;
|
|
styleAttrs.color = core::style::Color::create (layout,
|
|
newColor);
|
|
word->spaceStyle = core::style::Style::create(&styleAttrs);
|
|
old_style->unref();
|
|
break;
|
|
}
|
|
case core::Content::WIDGET_IN_FLOW:
|
|
{ core::Widget *widget = word->content.widget;
|
|
styleAttrs = *widget->getStyle();
|
|
styleAttrs.color = core::style::Color::create (layout,
|
|
newColor);
|
|
styleAttrs.setBorderColor(
|
|
core::style::Color::create (layout, newColor));
|
|
widget->setStyle(core::style::Style::create (&styleAttrs));
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
changed = true;
|
|
}
|
|
}
|
|
if (changed)
|
|
queueDrawArea (0, lineYOffsetWidget(line), allocation.width,
|
|
line->borderAscent + line->borderDescent);
|
|
}
|
|
}
|
|
|
|
void Textblock::changeWordStyle (int from, int to, core::style::Style *style,
|
|
bool includeFirstSpace, bool includeLastSpace)
|
|
{
|
|
}
|
|
|
|
void Textblock::queueDrawRange (int index1, int index2)
|
|
{
|
|
DBG_OBJ_ENTER ("draw", 0, "queueDrawRange", "%d, %d", index1, index2);
|
|
|
|
int from = std::min (index1, index2);
|
|
int to = std::max (index1, index2);
|
|
|
|
from = std::min (from, words->size () - 1);
|
|
from = std::max (from, 0);
|
|
to = std::min (to, words->size () - 1);
|
|
to = std::max (to, 0);
|
|
|
|
int line1idx = findLineOfWord (from);
|
|
int line2idx = findLineOfWord (to);
|
|
|
|
if (line1idx >= 0 && line2idx >= 0) {
|
|
Line *line1 = lines->getRef (line1idx),
|
|
*line2 = lines->getRef (line2idx);
|
|
int y = lineYOffsetWidget (line1) + line1->borderAscent -
|
|
line1->contentAscent;
|
|
int h = lineYOffsetWidget (line2) + line2->borderAscent +
|
|
line2->contentDescent - y;
|
|
|
|
queueDrawArea (0, y, allocation.width, h);
|
|
}
|
|
|
|
DBG_OBJ_LEAVE ();
|
|
}
|
|
|
|
void Textblock::updateReference (int ref)
|
|
{
|
|
DBG_OBJ_ENTER ("resize", 0, "updateReference", "%d", ref);
|
|
|
|
// Only `queueResize` when there're words or float clearance
|
|
// (float clearance may change `extraSpace.top`).
|
|
if (words->size () > 0 || getStyle()->clear != core::style::CLEAR_NONE)
|
|
queueResize (ref, false);
|
|
|
|
DBG_OBJ_LEAVE ();
|
|
}
|
|
|
|
void Textblock::widgetRefSizeChanged (int externalIndex)
|
|
{
|
|
int lineNo = findLineOfWord (externalIndex);
|
|
if (lineNo >= 0 && lineNo < lines->size ())
|
|
queueResize (makeParentRefInFlow (lineNo), true);
|
|
}
|
|
|
|
void Textblock::oofSizeChanged (bool extremesChanged)
|
|
{
|
|
DBG_OBJ_ENTER ("resize", 0, "oofSizeChanged", "%s",
|
|
extremesChanged ? "true" : "false");
|
|
queueResize (-1, extremesChanged);
|
|
|
|
// See Textblock::getAvailWidthOfChild(): Extremes changes may become also
|
|
// relevant for the children, under certain conditions:
|
|
if (extremesChanged && !usesMaxGeneratorWidth ())
|
|
containerSizeChanged ();
|
|
|
|
DBG_OBJ_LEAVE ();
|
|
}
|
|
|
|
int Textblock::getGeneratorX (int oofmIndex)
|
|
{
|
|
DBG_OBJ_ENTER ("resize", 0, "Textblock::getGeneratorX", "%d", oofmIndex);
|
|
|
|
int x, xRef;
|
|
if (findSizeRequestReference (oofmIndex, &xRef, NULL))
|
|
x = xRef;
|
|
else {
|
|
// Only called for floats, so this should not happen:
|
|
assertNotReached ();
|
|
x = 0;
|
|
}
|
|
|
|
DBG_OBJ_LEAVE_VAL ("%d", x);
|
|
return x;
|
|
}
|
|
|
|
int Textblock::getGeneratorY (int oofmIndex)
|
|
{
|
|
DBG_OBJ_ENTER ("resize", 0, "Textblock::getGeneratorY", "%d", oofmIndex);
|
|
|
|
int yRef, y;
|
|
if (findSizeRequestReference (oofmIndex, NULL, &yRef))
|
|
y = yRef;
|
|
else {
|
|
// Only called for floats, so this should not happen:
|
|
assertNotReached ();
|
|
y = 0;
|
|
}
|
|
|
|
DBG_OBJ_LEAVE_VAL ("%d", y);
|
|
return y;
|
|
}
|
|
|
|
int Textblock::getGeneratorRest (int oofmIndex)
|
|
{
|
|
DBG_OBJ_ENTER ("resize", 0, "Textblock::getGeneratorRest", "%d", oofmIndex);
|
|
|
|
int xRef, rest;
|
|
OOFAwareWidget *container = oofContainer[oofmIndex];
|
|
|
|
if (container != NULL && findSizeRequestReference (container, &xRef, NULL))
|
|
rest = container->getGeneratorWidth () - (xRef + getGeneratorWidth ());
|
|
else {
|
|
// Only callend for floats, so this should not happen:
|
|
assertNotReached ();
|
|
rest = 0;
|
|
}
|
|
|
|
DBG_OBJ_LEAVE_VAL ("%d", rest);
|
|
return rest;
|
|
}
|
|
|
|
int Textblock::getGeneratorWidth ()
|
|
{
|
|
DBG_OBJ_ENTER0 ("resize", 0, "Textblock::getGeneratorWidth");
|
|
|
|
// Cf. sizeRequestImpl.
|
|
if (usesMaxGeneratorWidth ()) {
|
|
DBG_OBJ_LEAVE_VAL ("%d", lineBreakWidth);
|
|
return lineBreakWidth;
|
|
} else {
|
|
// In some cases (especially when called from sizeRequest for an
|
|
// ancestor), the value is not up to date, since content from children is
|
|
// not yet added to lines. Moreover, this leads to inconsistencies between
|
|
// this widget and ancestors (as in Textblock::getGeneratorRest). For this
|
|
// reason, the children are examined recursively.
|
|
//
|
|
// Test case:
|
|
//
|
|
// <div style="float:left">
|
|
// <div div style="float:right">float</div>
|
|
// <div>abcdefghijkl mnopqrstuvwx</div>
|
|
// </div>
|
|
|
|
int wChild = 0;
|
|
int firstWordAfterLastLine =
|
|
lines->size() > 0 ? lines->getLastRef()->lastWord + 1 : 0;
|
|
for (int i = firstWordAfterLastLine; i < words->size(); i++) {
|
|
Word *word = words->getRef(i);
|
|
int xRel;
|
|
// We only examine instances of dw::Textblock, since they are relevant
|
|
// for floats, for which this method is only called.
|
|
if(Textblock *tbChild= nullptr;
|
|
word->content.type == core::Content::WIDGET_IN_FLOW &&
|
|
( tbChild= dynamic_cast< Textblock * >( word->content.widget ) )) {
|
|
if(tbChild->findSizeRequestReference(this, &xRel, NULL))
|
|
wChild = std::max(wChild, xRel + tbChild->getGeneratorWidth());
|
|
}
|
|
}
|
|
|
|
DBG_OBJ_MSGF ("resize", 1, "wChild = %d", wChild);
|
|
|
|
int w0 = lines->size () > 0 ? lines->getLastRef()->maxLineWidth : 0;
|
|
DBG_OBJ_MSGF ("resize", 1, "w0 = %d", w0);
|
|
int wThis = std::min(w0 + leftInnerPadding + boxDiffWidth (), lineBreakWidth);
|
|
DBG_OBJ_MSGF ("resize", 1, "wThis = min(%d + %d + %d, %d) = %d",
|
|
w0, leftInnerPadding, boxDiffWidth (), lineBreakWidth,
|
|
wThis);
|
|
int w = std::max(wThis, wChild);
|
|
DBG_OBJ_LEAVE_VAL ("max(%d, %d) = %d", wThis, wChild, w);
|
|
return w;
|
|
}
|
|
}
|
|
|
|
int Textblock::getMaxGeneratorWidth ()
|
|
{
|
|
DBG_OBJ_ENTER0 ("resize", 0, "Textblock::getMaxGeneratorWidth");
|
|
DBG_OBJ_LEAVE_VAL ("%d", lineBreakWidth);
|
|
return lineBreakWidth;
|
|
}
|
|
|
|
bool Textblock::usesMaxGeneratorWidth ()
|
|
{
|
|
DBG_OBJ_ENTER0 ("resize", 0, "usesMaxGeneratorWidth");
|
|
|
|
bool result;
|
|
if (treatAsInline) {
|
|
DBG_OBJ_MSG ("resize", 1, "treatAsInline set");
|
|
result = false;
|
|
} else {
|
|
bool toplevel = getParent () == NULL,
|
|
block = getStyle()->display == core::style::DISPLAY_BLOCK,
|
|
vloat = testWidgetFloat (this),
|
|
abspos = testWidgetAbsolutelyPositioned (this),
|
|
fixpos = testWidgetFixedlyPositioned (this);
|
|
DBG_OBJ_MSGF("resize", 1,
|
|
"toplevel: %s, block: %s, float: %s, abspos: %s, fixpos: %s",
|
|
boolToStr(toplevel), boolToStr(block), boolToStr(vloat),
|
|
boolToStr(abspos), boolToStr(fixpos));
|
|
|
|
// In detail, this depends on what the respective OOFM does with the
|
|
// child widget:
|
|
result = toplevel || (block && !(vloat || abspos || fixpos));
|
|
}
|
|
|
|
DBG_OBJ_LEAVE_VAL ("%s", boolToStr(result));
|
|
return result;
|
|
}
|
|
|
|
bool Textblock::isPossibleOOFContainer (int oofmIndex)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
bool Textblock::isPossibleOOFContainerParent (int oofmIndex)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
RegardingBorder *Textblock::getWidgetRegardingBorderForLine (Line *line)
|
|
{
|
|
return getWidgetRegardingBorderForLine (line->firstWord, line->lastWord);
|
|
}
|
|
|
|
RegardingBorder *Textblock::getWidgetRegardingBorderForLine (int lineNo)
|
|
{
|
|
// Can also be used for a line not yet existing.
|
|
int firstWord = lineNo == 0 ? 0 : lines->getRef(lineNo - 1)->lastWord + 1;
|
|
int lastWord = lineNo < lines->size() ?
|
|
lines->getRef(lineNo)->lastWord : words->size() - 1;
|
|
return getWidgetRegardingBorderForLine (firstWord, lastWord);
|
|
}
|
|
|
|
RegardingBorder *Textblock::getWidgetRegardingBorderForLine (int firstWord,
|
|
int lastWord)
|
|
{
|
|
DBG_OBJ_ENTER ("resize", 0, "getWidgetRegardingBorderForLine", "%d, %d",
|
|
firstWord, lastWord);
|
|
DBG_OBJ_MSGF ("resize", 1, "words.size = %d", words->size ());
|
|
|
|
RegardingBorder *widgetRegardingBorder = NULL;
|
|
|
|
if (firstWord < words->size ()) {
|
|
// Any instance of a subclass of WidgetRegardingBorder is always
|
|
// between two line breaks, and so the first word of the line.
|
|
Word *word = words->getRef (firstWord);
|
|
|
|
DBG_MSG_WORD ("resize", 1, "<i>first word:</i> ", firstWord, "");
|
|
|
|
if (word->content.type == core::Content::WIDGET_IN_FLOW) {
|
|
Widget *widget = word->content.widget;
|
|
if (auto *const rb= dynamic_cast< RegardingBorder * >( widget );
|
|
rb &&
|
|
// Exclude cases where a textblock constitutes a new floats
|
|
// container.
|
|
!isOOFContainer (widget, OOFM_FLOATS))
|
|
widgetRegardingBorder = rb;
|
|
}
|
|
}
|
|
|
|
DBG_OBJ_LEAVE_VAL ("%p", widgetRegardingBorder);
|
|
return widgetRegardingBorder;
|
|
}
|
|
|
|
/**
|
|
* Includes margin, border, and padding.
|
|
*/
|
|
int Textblock::yOffsetOfLineToBeCreated (int *lastMargin)
|
|
{
|
|
// This method does not return an exact result: the position of the
|
|
// new line, which does not yet exist, cannot be calculated, since
|
|
// the top margin of the new line (which collapses either with the
|
|
// top margin of the textblock widget, or the bottom margin of the
|
|
// last line) must be taken into account. However, this method is
|
|
// only called for positioning floats; here, a slight incorrectness
|
|
// does not cause real harm.
|
|
|
|
// (Similar applies to the line *height*, which calculated in an
|
|
// iterative way; see wrapWordInFlow. Using the same approach for
|
|
// the *position* is possible, but not worth the increased
|
|
// complexity.)
|
|
|
|
DBG_OBJ_ENTER0 ("line.yoffset", 0, "yOffsetOfLineToBeCreated");
|
|
|
|
int result;
|
|
|
|
if (lines->size () == 0) {
|
|
result = calcVerticalBorder (getStyle()->padding.top,
|
|
getStyle()->borderWidth.top + extraSpace.top,
|
|
getStyle()->margin.top, 0, 0);
|
|
if (lastMargin)
|
|
*lastMargin = getStyle()->margin.top;
|
|
} else {
|
|
Line *firstLine = lines->getRef (0), *lastLine = lines->getLastRef ();
|
|
result = calcVerticalBorder (getStyle()->padding.top,
|
|
getStyle()->borderWidth.top,
|
|
getStyle()->margin.top + extraSpace.top,
|
|
firstLine->borderAscent,
|
|
firstLine->marginAscent)
|
|
- firstLine->borderAscent + lastLine->top + lastLine->totalHeight (0);
|
|
if (lastMargin)
|
|
*lastMargin = lastLine->marginDescent - lastLine->borderDescent;
|
|
}
|
|
|
|
if (lastMargin)
|
|
DBG_OBJ_LEAVE_VAL ("%d, %d", result, *lastMargin);
|
|
else
|
|
DBG_OBJ_LEAVE_VAL ("%d", result);
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Includes margin, border, and padding. Can be used without allocation.
|
|
*/
|
|
int Textblock::yOffsetOfLineCreated (Line *line)
|
|
{
|
|
// Similar applies (regarding exactness) as to yOffsetOfLineToBeCreated.
|
|
|
|
DBG_OBJ_ENTER0 ("line.yoffset", 0, "yOffsetOfLineToBeCreated");
|
|
|
|
int result;
|
|
|
|
Line *firstLine = lines->getRef (0);
|
|
result = calcVerticalBorder (getStyle()->padding.top,
|
|
getStyle()->borderWidth.top,
|
|
getStyle()->margin.top + extraSpace.top,
|
|
firstLine->borderAscent,
|
|
firstLine->marginAscent)
|
|
- firstLine->borderAscent + line->top;
|
|
|
|
DBG_OBJ_LEAVE_VAL ("%d", result);
|
|
return result;
|
|
}
|
|
|
|
} // namespace dw
|