diff options
Diffstat (limited to 'WebKit/android/nav')
| -rw-r--r-- | WebKit/android/nav/CacheBuilder.cpp | 65 | ||||
| -rw-r--r-- | WebKit/android/nav/CacheBuilder.h | 2 | ||||
| -rw-r--r-- | WebKit/android/nav/CachedFrame.cpp | 1 | ||||
| -rw-r--r-- | WebKit/android/nav/CachedInput.cpp | 7 | ||||
| -rw-r--r-- | WebKit/android/nav/CachedInput.h | 5 | ||||
| -rw-r--r-- | WebKit/android/nav/CachedNode.cpp | 3 | ||||
| -rw-r--r-- | WebKit/android/nav/CachedNode.h | 4 | ||||
| -rw-r--r-- | WebKit/android/nav/CachedNodeType.h | 4 | ||||
| -rw-r--r-- | WebKit/android/nav/CachedPrefix.h | 7 | ||||
| -rw-r--r-- | WebKit/android/nav/FindCanvas.cpp | 3 | ||||
| -rw-r--r-- | WebKit/android/nav/FindCanvas.h | 1 | ||||
| -rw-r--r-- | WebKit/android/nav/SelectText.cpp | 1282 | ||||
| -rw-r--r-- | WebKit/android/nav/SelectText.h | 59 | ||||
| -rw-r--r-- | WebKit/android/nav/WebView.cpp | 361 |
14 files changed, 1507 insertions, 297 deletions
diff --git a/WebKit/android/nav/CacheBuilder.cpp b/WebKit/android/nav/CacheBuilder.cpp index 7ee2a16..1c8af5e 100644 --- a/WebKit/android/nav/CacheBuilder.cpp +++ b/WebKit/android/nav/CacheBuilder.cpp @@ -412,7 +412,7 @@ void CacheBuilder::Debug::groups() { comma(scratch); Element* element = static_cast<Element*>(node); if (node->isElementNode() && element->hasID()) - wideString(element->getIDAttribute()); + wideString(element->getIdAttribute()); else if (node->isTextNode()) { #if 01 // set to one to abbreviate text that can be omitted from the address detection code if (rect.isEmpty() && node->textContent().length() > 100) { @@ -469,22 +469,48 @@ void CacheBuilder::Debug::groups() { } } } - count++; - newLine(); -#if USE(ACCELERATED_COMPOSITING) - if (renderer && layer) { + if (renderer) { + RenderStyle* style = renderer->style(); + snprintf(scratch, sizeof(scratch), "// renderStyle:" + " visibility=%s hasBackGround=%d" + " tapHighlightColor().alpha()=0x%02x", + style->visibility() == HIDDEN ? "HIDDEN" : "VISIBLE", + renderer->hasBackground(), style->tapHighlightColor().alpha()); + newLine(); + print(scratch); + RenderBlock* renderBlock = static_cast<RenderBlock*>(renderer); + if (renderer->isRenderBlock() && renderBlock->hasColumns()) { + const RenderBox* box = static_cast<RenderBox*>(renderer); + const IntRect& oRect = box->visibleOverflowRect(); + snprintf(scratch, sizeof(scratch), "// renderBlock:" + " columnRects=%d columnGap=%d direction=%d" + " hasOverflowClip=%d overflow=(%d,%d,w=%d,h=%d)", + renderBlock->columnRects(), renderBlock->columnGap(), + renderBlock->style()->direction(), renderer->hasOverflowClip(), + oRect.x(), oRect.y(), oRect.width(), oRect.height()); + newLine(); + print(scratch); + } + } + #if USE(ACCELERATED_COMPOSITING) + if (renderer && renderer->hasLayer()) { + RenderLayer* layer = toRenderBoxModelObject(renderer)->layer(); RenderLayerBacking* back = layer->backing(); GraphicsLayerAndroid* grLayer = static_cast <GraphicsLayerAndroid*>(back ? back->graphicsLayer() : 0); LayerAndroid* aLayer = grLayer ? grLayer->contentLayer() : 0; const SkPicture* pict = aLayer ? aLayer->picture() : 0; + const IntRect& r = renderer->absoluteBoundingBoxRect(); snprintf(scratch, sizeof(scratch), "// layer:%p back:%p" - " gLayer:%p aLayer:%p pict:%p", layer, back, grLayer, - aLayer, pict); - print(scratch); + " gLayer:%p aLayer:%p pict:%p r:(%d,%d,w=%d,h=%d)", + layer, back, grLayer, aLayer, pict, r.x(), r.y(), + r.width(), r.height()); newLine(); - } -#endif + print(scratch); + } + #endif + count++; + newLine(); } while ((node = node->traverseNextNode()) != NULL); DUMP_NAV_LOGD("}; // focusables = %d\n", count - 1); DUMP_NAV_LOGD("\n"); @@ -841,6 +867,7 @@ bool CacheBuilder::AnyIsClick(Node* node) void CacheBuilder::buildCache(CachedRoot* root) { Frame* frame = FrameAnd(this); + mPictureSetDisabled = false; BuildFrame(frame, frame, root, (CachedFrame*) root); root->finishInit(); // set up frame parent pointers, child pointers setData((CachedFrame*) root); @@ -1028,7 +1055,7 @@ void CacheBuilder::BuildFrame(Frame* root, Frame* frame, RenderStyle* style = nodeRenderer->style(); if (style->visibility() == HIDDEN) continue; - isTransparent = style->hasBackground() == false; + isTransparent = nodeRenderer->hasBackground() == false; #ifdef ANDROID_CSS_TAP_HIGHLIGHT_COLOR hasCursorRing = style->tapHighlightColor().alpha() > 0; #endif @@ -1105,6 +1132,8 @@ void CacheBuilder::BuildFrame(Frame* root, Frame* frame, if (node->hasTagName(HTMLNames::bodyTag)) bodyPos = originalAbsBounds.location(); + else if (node->hasTagName(HTMLNames::canvasTag)) + mPictureSetDisabled = true; if (checkForPluginViewThatWantsFocus(nodeRenderer)) { bounds = absBounds; isUnclipped = true; @@ -1112,13 +1141,18 @@ void CacheBuilder::BuildFrame(Frame* root, Frame* frame, type = PLUGIN_CACHEDNODETYPE; goto keepNode; } + // Only use the root contentEditable element + if (node->isContentEditable() && !node->parent()->isContentEditable()) { + bounds = absBounds; + takesFocus = true; + type = CONTENT_EDITABLE_CACHEDNODETYPE; + goto keepNode; + } if (nodeRenderer->isRenderBlock()) { RenderBlock* renderBlock = (RenderBlock*) nodeRenderer; if (renderBlock->hasColumns()) { columns = renderBlock->columnRects(); -#ifdef ANDROID_EXPOSE_COLUMN_GAP columnGap = renderBlock->columnGap(); -#endif direction = renderBlock->style()->direction(); } } @@ -1216,6 +1250,8 @@ void CacheBuilder::BuildFrame(Frame* root, Frame* frame, if (!href.isEmpty() && !WebCore::protocolIsJavaScript(href.string())) // Set the exported string for all non-javascript anchors. exported = href.string().threadsafeCopy(); + } else if (node->hasTagName(HTMLNames::selectTag)) { + type = SELECT_CACHEDNODETYPE; } if (type == TEXT_INPUT_CACHEDNODETYPE) { RenderTextControl* renderText = @@ -2673,7 +2709,8 @@ bool CacheBuilder::isFocusableText(NodeWalk* walk, bool more, Node* node, do { do { do { - node = node->traverseNextNode(); + if (node) + node = node->traverseNextNode(); if (node == NULL || node->hasTagName(HTMLNames::aTag) || node->hasTagName(HTMLNames::inputTag) || node->hasTagName(HTMLNames::textareaTag)) { diff --git a/WebKit/android/nav/CacheBuilder.h b/WebKit/android/nav/CacheBuilder.h index 4ded58d..8183954 100644 --- a/WebKit/android/nav/CacheBuilder.h +++ b/WebKit/android/nav/CacheBuilder.h @@ -94,6 +94,7 @@ public: static IntRect getAreaRect(const HTMLAreaElement* area); static void GetGlobalOffset(Frame* , int* x, int * y); static void GetGlobalOffset(Node* , int* x, int * y); + bool pictureSetDisabled() { return mPictureSetDisabled; } static bool validNode(Frame* startFrame, void* framePtr, void* nodePtr); private: enum AddressProgress { @@ -249,6 +250,7 @@ private: Node* tryFocus(Direction direction); Node* trySegment(Direction direction, int mainStart, int mainEnd); CachedNodeBits mAllowableTypes; + bool mPictureSetDisabled; #if DUMP_NAV_CACHE public: class Debug { diff --git a/WebKit/android/nav/CachedFrame.cpp b/WebKit/android/nav/CachedFrame.cpp index ce5600b..ff13508 100644 --- a/WebKit/android/nav/CachedFrame.cpp +++ b/WebKit/android/nav/CachedFrame.cpp @@ -1390,6 +1390,7 @@ void CachedFrame::Debug::print() const const CachedInput* input = b->textInput(node); if (input) input->mDebug.print(); + DUMP_NAV_LOGD("\n"); } DUMP_NAV_LOGD("// }; // end of nodes\n"); #if USE(ACCELERATED_COMPOSITING) diff --git a/WebKit/android/nav/CachedInput.cpp b/WebKit/android/nav/CachedInput.cpp index 924bbca..608c41b 100644 --- a/WebKit/android/nav/CachedInput.cpp +++ b/WebKit/android/nav/CachedInput.cpp @@ -28,6 +28,11 @@ namespace android { +void CachedInput::init() { + bzero(this, sizeof(CachedInput)); + mName = WebCore::String(); +} + #if DUMP_NAV_CACHE #define DEBUG_PRINT_BOOL(field) \ @@ -55,7 +60,7 @@ void CachedInput::Debug::print() const { CachedInput* b = base(); printWebCoreString("// char* mName=\"", b->mName); - DUMP_NAV_LOGD("// void* mForm=%p;", b->mForm); + DUMP_NAV_LOGD("// void* mForm=%p;\n", b->mForm); DUMP_NAV_LOGD("// int mMaxLength=%d;\n", b->mMaxLength); DUMP_NAV_LOGD("// int mTextSize=%d;\n", b->mTextSize); DUMP_NAV_LOGD("// int mInputType=%d;\n", b->mInputType); diff --git a/WebKit/android/nav/CachedInput.h b/WebKit/android/nav/CachedInput.h index 42cadf1..a3d6b10 100644 --- a/WebKit/android/nav/CachedInput.h +++ b/WebKit/android/nav/CachedInput.h @@ -39,10 +39,7 @@ public: // constructor } void* formPointer() const { return mForm; } - void init() { - bzero(this, sizeof(CachedInput)); - mName = WebCore::String(); - } + void init(); WebCore::HTMLInputElement::InputType inputType() const { return mInputType; } bool isRtlText() const { return mIsRtlText; } bool isTextField() const { return mIsTextField; } diff --git a/WebKit/android/nav/CachedNode.cpp b/WebKit/android/nav/CachedNode.cpp index 0c9d541..8fc5f5b 100644 --- a/WebKit/android/nav/CachedNode.cpp +++ b/WebKit/android/nav/CachedNode.cpp @@ -367,6 +367,8 @@ const char* CachedNode::Debug::type(android::CachedNodeType t) const case FRAME_CACHEDNODETYPE: return "FRAME"; break; case PLUGIN_CACHEDNODETYPE: return "PLUGIN"; break; case TEXT_INPUT_CACHEDNODETYPE: return "INPUT"; break; + case SELECT_CACHEDNODETYPE: return "SELECT"; break; + case CONTENT_EDITABLE_CACHEDNODETYPE: return "CONTENT_EDITABLE"; break; default: return "???"; } } @@ -419,7 +421,6 @@ void CachedNode::Debug::print() const DEBUG_PRINT_BOOL(mLast); DEBUG_PRINT_BOOL(mUseBounds); DEBUG_PRINT_BOOL(mUseHitBounds); - DUMP_NAV_LOGD("\n"); } #endif diff --git a/WebKit/android/nav/CachedNode.h b/WebKit/android/nav/CachedNode.h index f3cfd98..09f53c3 100644 --- a/WebKit/android/nav/CachedNode.h +++ b/WebKit/android/nav/CachedNode.h @@ -108,6 +108,7 @@ public: int index() const { return mIndex; } void init(WebCore::Node* node); bool isAnchor() const { return mType == ANCHOR_CACHEDNODETYPE; } + bool isContentEditable() const { return mType == CONTENT_EDITABLE_CACHEDNODETYPE; } bool isCursor() const { return mIsCursor; } bool isArea() const { return mType == AREA_CACHEDNODETYPE; } bool isFocus() const { return mIsFocus; } @@ -118,6 +119,7 @@ public: return clip.intersects(bounds(frame)); } bool isPlugin() const { return mType == PLUGIN_CACHEDNODETYPE; } + bool isSelect() const { return mType == SELECT_CACHEDNODETYPE; } bool isSyntheticLink() const { return mType >= ADDRESS_CACHEDNODETYPE && mType <= PHONE_CACHEDNODETYPE; } @@ -173,7 +175,7 @@ public: const CachedNode* traverseNextNode() const { return mLast ? NULL : &this[1]; } bool useBounds() const { return mUseBounds; } bool useHitBounds() const { return mUseHitBounds; } - bool wantsKeyEvents() const { return isTextInput() || isPlugin(); } + bool wantsKeyEvents() const { return isTextInput() || isPlugin() || isContentEditable(); } private: friend class CacheBuilder; WebCore::String mExport; diff --git a/WebKit/android/nav/CachedNodeType.h b/WebKit/android/nav/CachedNodeType.h index 21e2d40..8bc9328 100644 --- a/WebKit/android/nav/CachedNodeType.h +++ b/WebKit/android/nav/CachedNodeType.h @@ -37,7 +37,9 @@ enum CachedNodeType { AREA_CACHEDNODETYPE, FRAME_CACHEDNODETYPE, PLUGIN_CACHEDNODETYPE, - TEXT_INPUT_CACHEDNODETYPE + TEXT_INPUT_CACHEDNODETYPE, + SELECT_CACHEDNODETYPE, + CONTENT_EDITABLE_CACHEDNODETYPE }; enum CachedNodeBits { diff --git a/WebKit/android/nav/CachedPrefix.h b/WebKit/android/nav/CachedPrefix.h index b682288..73a5c2c 100644 --- a/WebKit/android/nav/CachedPrefix.h +++ b/WebKit/android/nav/CachedPrefix.h @@ -43,4 +43,11 @@ #define OFFSETOF(type, field) ((char*)&(((type*)1)->field) - (char*)1) // avoids gnu warning +#ifndef BZERO_DEFINED +#define BZERO_DEFINED +// http://www.opengroup.org/onlinepubs/000095399/functions/bzero.html +// For maximum portability, it is recommended to replace the function call to bzero() as follows: +#define bzero(b,len) (memset((b), '\0', (len)), (void) 0) +#endif + #endif diff --git a/WebKit/android/nav/FindCanvas.cpp b/WebKit/android/nav/FindCanvas.cpp index d8e908b..8eaaaef 100644 --- a/WebKit/android/nav/FindCanvas.cpp +++ b/WebKit/android/nav/FindCanvas.cpp @@ -98,7 +98,7 @@ GlyphSet::~GlyphSet() { // part of mLowerGlyphs } -GlyphSet::GlyphSet& GlyphSet::operator=(GlyphSet& src) { +GlyphSet& GlyphSet::operator=(GlyphSet& src) { mTypeface = src.mTypeface; mCount = src.mCount; if (mCount > MAX_STORAGE_COUNT) { @@ -675,4 +675,3 @@ void FindOnPage::setMatches(WTF::Vector<MatchInfo>* matches) } } - diff --git a/WebKit/android/nav/FindCanvas.h b/WebKit/android/nav/FindCanvas.h index b9dbeea..34929ec 100644 --- a/WebKit/android/nav/FindCanvas.h +++ b/WebKit/android/nav/FindCanvas.h @@ -220,6 +220,7 @@ public: virtual ~FindOnPage() { delete m_matches; } void clearCurrentLocation() { m_hasCurrentLocation = false; } IntRect currentMatchBounds() const; + int currentMatchIndex() const { return m_findIndex; } bool currentMatchIsInLayer() const; virtual void draw(SkCanvas* , LayerAndroid* ); void findNext(bool forward); diff --git a/WebKit/android/nav/SelectText.cpp b/WebKit/android/nav/SelectText.cpp index e471307..9df6ef5 100644 --- a/WebKit/android/nav/SelectText.cpp +++ b/WebKit/android/nav/SelectText.cpp @@ -23,9 +23,10 @@ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -#define LOG_TAG "webcoreglue" +#define LOG_TAG "webviewglue" #include "CachedPrefix.h" +#include "BidiResolver.h" #include "CachedRoot.h" #include "LayerAndroid.h" #include "SelectText.h" @@ -39,26 +40,156 @@ #include "SkRect.h" #include "SkRegion.h" #include "SkUtils.h" +#include "TextRun.h" #ifdef DEBUG_NAV_UI -#include "CString.h" +#include <wtf/text/CString.h> #endif +#define VERBOSE_LOGGING 0 +// #define EXTRA_NOISY_LOGGING 1 + +// TextRunIterator has been copied verbatim from GraphicsContext.cpp +namespace WebCore { + +class TextRunIterator { +public: + TextRunIterator() + : m_textRun(0) + , m_offset(0) + { + } + + TextRunIterator(const TextRun* textRun, unsigned offset) + : m_textRun(textRun) + , m_offset(offset) + { + } + + TextRunIterator(const TextRunIterator& other) + : m_textRun(other.m_textRun) + , m_offset(other.m_offset) + { + } + + unsigned offset() const { return m_offset; } + void increment() { m_offset++; } + bool atEnd() const { return !m_textRun || m_offset >= m_textRun->length(); } + UChar current() const { return (*m_textRun)[m_offset]; } + WTF::Unicode::Direction direction() const { return atEnd() ? WTF::Unicode::OtherNeutral : WTF::Unicode::direction(current()); } + + bool operator==(const TextRunIterator& other) + { + return m_offset == other.m_offset && m_textRun == other.m_textRun; + } + + bool operator!=(const TextRunIterator& other) { return !operator==(other); } + +private: + const TextRun* m_textRun; + int m_offset; +}; + +// ReverseBidi is a trimmed-down version of GraphicsContext::drawBidiText() +void ReverseBidi(UChar* chars, int len) { + using namespace WTF::Unicode; + WTF::Vector<UChar> result; + result.reserveCapacity(len); + TextRun run(chars, len); + BidiResolver<TextRunIterator, BidiCharacterRun> bidiResolver; + bidiResolver.setStatus(BidiStatus(LeftToRight, LeftToRight, LeftToRight, + BidiContext::create(0, LeftToRight, false))); + bidiResolver.setPosition(TextRunIterator(&run, 0)); + bidiResolver.createBidiRunsForLine(TextRunIterator(&run, len)); + if (!bidiResolver.runCount()) + return; + BidiCharacterRun* bidiRun = bidiResolver.firstRun(); + while (bidiRun) { + int bidiStart = bidiRun->start(); + int bidiStop = bidiRun->stop(); + int size = result.size(); + int bidiCount = bidiStop - bidiStart; + result.append(chars + bidiStart, bidiCount); + if (bidiRun->level() % 2) { + UChar* start = &result[size]; + UChar* end = start + bidiCount; + // reverse the order of any RTL substrings + while (start < end) { + UChar temp = *start; + *start++ = *--end; + *end = temp; + } + start = &result[size]; + end = start + bidiCount - 1; + // if the RTL substring had a surrogate pair, restore its order + while (start < end) { + UChar trail = *start++; + if (!U16_IS_SURROGATE(trail)) + continue; + start[-1] = *start; // lead + *start++ = trail; + } + } + bidiRun = bidiRun->next(); + } + bidiResolver.deleteRuns(); + memcpy(chars, &result[0], len * sizeof(UChar)); +} + +} + namespace android { +/* SpaceBounds and SpaceCanvas are used to measure the left and right side + * bearings of two consecutive glyphs to help determine if the glyphs were + * originally laid out with a space character between the glyphs. + */ +class SpaceBounds : public SkBounder { +public: + virtual bool onIRectGlyph(const SkIRect& , const SkBounder::GlyphRec& rec) + { + mFirstGlyph = mLastGlyph; + mLastGlyph = rec; + return false; + } + + SkBounder::GlyphRec mFirstGlyph; + SkBounder::GlyphRec mLastGlyph; +}; + +class SpaceCanvas : public SkCanvas { +public: + SpaceCanvas(const SkIRect& area) + { + setBounder(&mBounder); + SkBitmap bitmap; + bitmap.setConfig(SkBitmap::kARGB_8888_Config, area.width(), + area.height()); + setBitmapDevice(bitmap); + translate(SkIntToScalar(-area.fLeft), SkIntToScalar(-area.fTop)); + } + + SpaceBounds mBounder; +}; + +#define HYPHEN_MINUS 0x2D // ASCII hyphen +#define HYPHEN 0x2010 // unicode hyphen, first in range of dashes +#define HORZ_BAR 0x2015 // unicode horizontal bar, last in range of dashes +#define TOUCH_SLOP 10 // additional distance from character rect when hit + class CommonCheck : public SkBounder { public: - CommonCheck() : mMatrix(NULL), mPaint(NULL) {} - - virtual void setUp(const SkPaint& paint, const SkMatrix& matrix, SkScalar y, - const void* text) { - mMatrix = &matrix; - mPaint = &paint; - mText = static_cast<const uint16_t*>(text); - mY = y; - mBase = mBottom = mTop = INT_MAX; + CommonCheck(int width, int height) + : mHeight(height) + , mLastUni(0) + , mMatrix(0) + , mPaint(0) + , mWidth(width) + { + mLastGlyph.fGlyphID = static_cast<uint16_t>(-1); + reset(); } - + int base() { if (mBase == INT_MAX) { SkPoint result; @@ -78,7 +209,148 @@ public: } return mBottom; } - + +#if DEBUG_NAV_UI + // make current (possibily uncomputed) value visible for debugging + int bottomDebug() const + { + return mBottom; + } +#endif + + bool addNewLine(const SkBounder::GlyphRec& rec) + { + SkFixed lineSpacing = SkFixedAbs(mLastGlyph.fLSB.fY - rec.fLSB.fY); + SkFixed lineHeight = SkIntToFixed(bottom() - top()); + return lineSpacing >= lineHeight + (lineHeight >> 1); // 1.5 + } + + bool addSpace(const SkBounder::GlyphRec& rec) + { + bool newBaseLine = mLastGlyph.fLSB.fY != rec.fLSB.fY; + if (((mLastUni >= HYPHEN && mLastUni <= HORZ_BAR) + || mLastUni == HYPHEN_MINUS) && newBaseLine) + { + return false; + } + return isSpace(rec); + } + + void finishGlyph() + { + mLastGlyph = mLastCandidate; + mLastUni = mLastUniCandidate; + } + + SkUnichar getUniChar(const SkBounder::GlyphRec& rec) + { + SkUnichar unichar; + SkPaint utfPaint = *mPaint; + utfPaint.setTextEncoding(SkPaint::kUTF16_TextEncoding); + utfPaint.glyphsToUnichars(&rec.fGlyphID, 1, &unichar); + return unichar; + } + + bool isSpace(const SkBounder::GlyphRec& rec) + { + DBG_NAV_LOGD("mLastGlyph=((%g, %g),(%g, %g), %d)" + " rec=((%g, %g),(%g, %g), %d)" + " mMinSpaceWidth=%g mLastUni=0x%04x '%c'", + SkFixedToScalar(mLastGlyph.fLSB.fX), + SkFixedToScalar(mLastGlyph.fLSB.fY), + SkFixedToScalar(mLastGlyph.fRSB.fX), + SkFixedToScalar(mLastGlyph.fRSB.fY), mLastGlyph.fGlyphID, + SkFixedToScalar(rec.fLSB.fX), SkFixedToScalar(rec.fLSB.fY), + SkFixedToScalar(rec.fRSB.fX), SkFixedToScalar(rec.fRSB.fY), + rec.fGlyphID, + SkFixedToScalar(mMinSpaceWidth), + mLastUni, mLastUni && mLastUni < 0x7f ? mLastUni : '?'); + bool newBaseLine = mLastGlyph.fLSB.fY != rec.fLSB.fY; + if (newBaseLine) + return true; + SkFixed gapOne = mLastGlyph.fLSB.fX - rec.fRSB.fX; + SkFixed gapTwo = rec.fLSB.fX - mLastGlyph.fRSB.fX; + if (gapOne < 0 && gapTwo < 0) + return false; // overlaps + uint16_t test[2]; + test[0] = mLastGlyph.fGlyphID; + test[1] = rec.fGlyphID; + SkIRect area; + area.set(0, 0, mWidth, mHeight); + SpaceCanvas spaceChecker(area); + spaceChecker.drawText(test, sizeof(test), + SkFixedToScalar(mLastGlyph.fLSB.fX), + SkFixedToScalar(mLastGlyph.fLSB.fY), *mPaint); + const SkBounder::GlyphRec& g1 = spaceChecker.mBounder.mFirstGlyph; + const SkBounder::GlyphRec& g2 = spaceChecker.mBounder.mLastGlyph; + DBG_NAV_LOGD("g1=(%g, %g,%g, %g) g2=(%g, %g, %g, %g)", + SkFixedToScalar(g1.fLSB.fX), SkFixedToScalar(g1.fLSB.fY), + SkFixedToScalar(g1.fRSB.fX), SkFixedToScalar(g1.fRSB.fY), + SkFixedToScalar(g2.fLSB.fX), SkFixedToScalar(g2.fLSB.fY), + SkFixedToScalar(g2.fRSB.fX), SkFixedToScalar(g2.fRSB.fY)); + gapOne = SkFixedAbs(gapOne); + gapTwo = SkFixedAbs(gapTwo); + SkFixed gap = gapOne < gapTwo ? gapOne : gapTwo; + SkFixed overlap = g2.fLSB.fX - g1.fRSB.fX; + if (overlap < 0) + gap -= overlap; + DBG_NAV_LOGD("gap=%g overlap=%g gapOne=%g gapTwo=%g minSpaceWidth()=%g", + SkFixedToScalar(gap), SkFixedToScalar(overlap), + SkFixedToScalar(gapOne), SkFixedToScalar(gapTwo), + SkFixedToScalar(minSpaceWidth())); + // FIXME: the -1/2 below takes care of slop beween the computed gap + // and the actual space width -- it's a rounding error from + // moving from fixed to float and back and could be much smaller. + return gap >= minSpaceWidth() - SK_Fixed1 / 2; + } + + SkFixed minSpaceWidth() + { + if (mMinSpaceWidth == SK_FixedMax) { + SkPaint charPaint = *mPaint; + charPaint.setTextEncoding(SkPaint::kUTF8_TextEncoding); + SkScalar width = charPaint.measureText(" ", 1); + mMinSpaceWidth = SkScalarToFixed(width * mMatrix->getScaleX()); + DBG_NAV_LOGD("width=%g matrix sx/sy=(%g, %g) tx/ty=(%g, %g)" + " mMinSpaceWidth=%g", width, + mMatrix->getScaleX(), mMatrix->getScaleY(), + mMatrix->getTranslateX(), mMatrix->getTranslateY(), + SkFixedToScalar(mMinSpaceWidth)); + } + return mMinSpaceWidth; + } + + void recordGlyph(const SkBounder::GlyphRec& rec) + { + mLastCandidate = rec; + mLastUniCandidate = getUniChar(rec); + } + + void reset() + { + mMinSpaceWidth = SK_FixedMax; // mark as uninitialized + mBase = mBottom = mTop = INT_MAX; // mark as uninitialized + } + + void set(CommonCheck& check) + { + mLastGlyph = check.mLastGlyph; + mLastUni = check.mLastUni; + mMatrix = check.mMatrix; + mPaint = check.mPaint; + reset(); + } + + void setUp(const SkPaint& paint, const SkMatrix& matrix, SkScalar y, + const void* text) + { + mMatrix = &matrix; + mPaint = &paint; + mText = static_cast<const uint16_t*>(text); + mY = y; + reset(); + } + int top() { if (mTop == INT_MAX) { SkPoint result; @@ -89,127 +361,417 @@ public: } return mTop; } - -protected: + +#if DEBUG_NAV_UI + // make current (possibily uncomputed) value visible for debugging + int topDebug() const + { + return mTop; + } +#endif + +protected: + int mHeight; + SkBounder::GlyphRec mLastCandidate; + SkBounder::GlyphRec mLastGlyph; + SkUnichar mLastUni; + SkUnichar mLastUniCandidate; const SkMatrix* mMatrix; const SkPaint* mPaint; const uint16_t* mText; + int mWidth; SkScalar mY; +private: int mBase; int mBottom; + SkFixed mMinSpaceWidth; int mTop; + friend class EdgeCheck; }; class FirstCheck : public CommonCheck { public: - FirstCheck(int x, int y) - : mDistance(INT_MAX), mFocusX(x), mFocusY(y) { - mBestBounds.setEmpty(); + FirstCheck(int x, int y, const SkIRect& area) + : INHERITED(area.width(), area.height()) + , mFocusX(x - area.fLeft) + , mFocusY(y - area.fTop) + , mRecordGlyph(false) + { + reset(); } - const SkIRect& bestBounds() { - DBG_NAV_LOGD("mBestBounds:(%d, %d, %d, %d) mTop=%d mBottom=%d", + const SkIRect& adjustedBounds(const SkIRect& area, int* base) + { + *base = mBestBase + area.fTop; + mBestBounds.offset(area.fLeft, area.fTop); + DBG_NAV_LOGD("FirstCheck mBestBounds:(%d, %d, %d, %d) mTop=%d mBottom=%d", mBestBounds.fLeft, mBestBounds.fTop, mBestBounds.fRight, - mBestBounds.fBottom, mTop, mBottom); - return mBestBounds; - } - - void offsetBounds(int dx, int dy) { - mBestBounds.offset(dx, dy); + mBestBounds.fBottom, topDebug(), bottomDebug()); + return mBestBounds; } - - virtual bool onIRect(const SkIRect& rect) { - int dx = ((rect.fLeft + rect.fRight) >> 1) - mFocusX; - int dy = ((top() + bottom()) >> 1) - mFocusY; + + virtual bool onIRectGlyph(const SkIRect& rect, + const SkBounder::GlyphRec& rec) + { + /* compute distance from rectangle center. + * centerX = (rect.L + rect.R) / 2 + * multiply centerX and comparison x by 2 to retain better precision + */ + int dx = rect.fLeft + rect.fRight - (mFocusX << 1); + int dy = top() + bottom() - (mFocusY << 1); int distance = dx * dx + dy * dy; #ifdef EXTRA_NOISY_LOGGING if (distance < 500 || abs(distance - mDistance) < 500) - DBG_NAV_LOGD("distance=%d mDistance=%d", distance, mDistance); + DBG_NAV_LOGD("FirstCheck distance=%d mDistance=%d", distance, mDistance); #endif if (mDistance > distance) { - mDistance = distance; + mBestBase = base(); mBestBounds.set(rect.fLeft, top(), rect.fRight, bottom()); -#ifdef EXTRA_NOISY_LOGGING - DBG_NAV_LOGD("mBestBounds={%d,%d,r=%d,b=%d}", - mBestBounds.fLeft, mBestBounds.fTop, - mBestBounds.fRight, mBestBounds.fBottom); -#endif + if (distance < 100) { + DBG_NAV_LOGD("FirstCheck mBestBounds={%d,%d,r=%d,b=%d} distance=%d", + mBestBounds.fLeft, mBestBounds.fTop, + mBestBounds.fRight, mBestBounds.fBottom, distance >> 2); + } + mDistance = distance; + if (mRecordGlyph) + recordGlyph(rec); } return false; } + + void reset() + { + mBestBounds.setEmpty(); + mDistance = INT_MAX; + } + + void setRecordGlyph() + { + mRecordGlyph = true; + } + protected: + int mBestBase; SkIRect mBestBounds; int mDistance; int mFocusX; int mFocusY; + bool mRecordGlyph; +private: + typedef CommonCheck INHERITED; +}; + +class EdgeCheck : public FirstCheck { +public: + EdgeCheck(int x, int y, const SkIRect& area, CommonCheck& last, bool left) + : INHERITED(x, y, area) + , mLast(area.width(), area.height()) + , mLeft(left) + { + mLast.set(last); + mLastGlyph = last.mLastGlyph; + mLastUni = last.mLastUni; + } + + bool adjacent() + { + return !mLast.isSpace(mLastGlyph); + } + + const SkIRect& bestBounds(int* base) + { + *base = mBestBase; + return mBestBounds; + } + + virtual bool onIRectGlyph(const SkIRect& rect, + const SkBounder::GlyphRec& rec) + { + int dx = mLeft ? mFocusX - rect.fRight : rect.fLeft - mFocusX; + int dy = ((top() + bottom()) >> 1) - mFocusY; + if (mLeft ? mFocusX <= rect.fLeft : mFocusX >= rect.fRight) { + if (abs(dx) <= 10 && abs(dy) <= 10) { + DBG_NAV_LOGD("EdgeCheck fLeft=%d fRight=%d mFocusX=%d dx=%d dy=%d", + rect.fLeft, rect.fRight, mFocusX, dx, dy); + } + return false; + } + int distance = dx * dx + dy * dy; + if (mDistance > distance) { + if (rec.fLSB == mLastGlyph.fLSB && rec.fRSB == mLastGlyph.fRSB) { + DBG_NAV_LOGD("dup rec.fLSB.fX=%g rec.fRSB.fX=%g", + SkFixedToScalar(rec.fLSB.fX), SkFixedToScalar(rec.fRSB.fX)); + return false; + } + recordGlyph(rec); + mDistance = distance; + mBestBase = base(); + mBestBounds.set(rect.fLeft, top(), rect.fRight, bottom()); + if (distance <= 100) { + DBG_NAV_LOGD("EdgeCheck mBestBounds={%d,%d,r=%d,b=%d} distance=%d", + mBestBounds.fLeft, mBestBounds.fTop, + mBestBounds.fRight, mBestBounds.fBottom, distance); + } + } + return false; + } + + void shiftStart(SkIRect bounds) + { + DBG_NAV_LOGD("EdgeCheck mFocusX=%d mLeft=%s bounds.fLeft=%d bounds.fRight=%d", + mFocusX, mLeft ? "true" : "false", bounds.fLeft, bounds.fRight); + reset(); + mFocusX = mLeft ? bounds.fLeft : bounds.fRight; + mLast.set(*this); + } + +protected: + CommonCheck mLast; + bool mLeft; +private: + typedef FirstCheck INHERITED; +}; + +class FindFirst : public CommonCheck { +public: + FindFirst(int width, int height) + : INHERITED(width, height) + { + mBestBounds.set(width, height, width, height); + } + + const SkIRect& bestBounds(int* base) + { + *base = mBestBase; + return mBestBounds; + } + + virtual bool onIRect(const SkIRect& rect) + { + if (mBestBounds.isEmpty()) { + mBestBase = base(); + mBestBounds.set(rect.fLeft, top(), rect.fRight, bottom()); + } + return false; + } + +protected: + int mBestBase; + SkIRect mBestBounds; +private: + typedef CommonCheck INHERITED; +}; + +class FindLast : public FindFirst { +public: + FindLast(int width, int height) + : INHERITED(width, height) + { + mBestBounds.setEmpty(); + } + + virtual bool onIRect(const SkIRect& rect) + { + mBestBase = base(); + mBestBounds.set(rect.fLeft, top(), rect.fRight, bottom()); + return false; + } + +private: + typedef FindFirst INHERITED; }; +static bool baseLinesAgree(const SkIRect& rectA, int baseA, + const SkIRect& rectB, int baseB) +{ + return (rectA.fTop < baseB && rectA.fBottom >= baseB) + || (rectB.fTop < baseA && rectB.fBottom >= baseA); +} + class MultilineBuilder : public CommonCheck { public: - MultilineBuilder(const SkIRect& start, const SkIRect& end, int dx, int dy, - SkRegion* region) - : mStart(start), mEnd(end), mSelectRegion(region), mCapture(false) { + MultilineBuilder(const SkIRect& start, int startBase, const SkIRect& end, + int endBase, const SkIRect& area, SkRegion* region) + : INHERITED(area.width(),area.height()) + , mCapture(false) + , mEnd(end) + , mEndBase(endBase) + , mFlipped(false) + , mSelectRegion(region) + , mStart(start) + , mStartBase(startBase) + { + mEnd.offset(-area.fLeft, -area.fTop); + mEndBase -= area.fTop; mLast.setEmpty(); mLastBase = INT_MAX; - mStart.offset(-dx, -dy); - mEnd.offset(-dx, -dy); + mStart.offset(-area.fLeft, -area.fTop); + mStartBase -= area.fTop; } - virtual bool onIRect(const SkIRect& rect) { - bool captureLast = false; - if ((rect.fLeft == mStart.fLeft && rect.fRight == mStart.fRight && - top() == mStart.fTop && bottom() == mStart.fBottom) || - (rect.fLeft == mEnd.fLeft && rect.fRight == mEnd.fRight && - top() == mEnd.fTop && bottom() == mEnd.fBottom)) { - captureLast = mCapture; - mCapture ^= true; + void finish() { + if (!mFlipped || SkIRect::Intersects(mLast, mSelectRect)) { + if (VERBOSE_LOGGING) DBG_NAV_LOGD(" mLast=(%d,%d,r=%d,b=%d)", + mLast.fLeft, mLast.fTop, mLast.fRight, mLast.fBottom); + mSelectRegion->op(mLast, SkRegion::kUnion_Op); } - if (mCapture || captureLast) { - SkIRect full; - full.set(rect.fLeft, top(), rect.fRight, bottom()); - if ((mLast.fTop < base() && mLast.fBottom >= base()) - || (mLastBase <= full.fBottom && mLastBase > full.fTop)) { - if (full.fLeft > mLast.fRight) - full.fLeft = mLast.fRight; - else if (full.fRight < mLast.fLeft) - full.fRight = mLast.fLeft; + } + + // return true if capture end was not found after capture begin + bool flipped() { + DBG_NAV_LOGD("flipped=%s", mCapture ? "true" : "false"); + if (!mCapture) + return false; + mFlipped = true; + mSelectRect = mStart; + mSelectRect.join(mEnd); + mLast.setEmpty(); + mLastBase = INT_MAX; + mSelectRegion->setEmpty(); + return true; + } + + virtual bool onIRect(const SkIRect& rect) { + SkIRect full; + full.set(rect.fLeft, top(), rect.fRight, bottom()); + int fullBase = base(); + if (mFlipped) { + if (fullBase < mStart.fTop || fullBase > mEnd.fBottom + || (baseLinesAgree(mStart, mStartBase, full, fullBase) + && full.fLeft < mStart.fLeft) + || (baseLinesAgree(mEnd, mEndBase, full, fullBase) + && full.fRight > mEnd.fRight)) { + return false; + } + if (baseLinesAgree(mLast, mLastBase, full, fullBase) + && (full.fLeft - mLast.fRight < minSpaceWidth() * 3 + || (SkIRect::Intersects(full, mSelectRect) + && SkIRect::Intersects(mLast, mSelectRect)))) { + mLast.join(full); + return false; } - mSelectRegion->op(full, SkRegion::kUnion_Op); - DBG_NAV_LOGD("MultilineBuilder full=(%d,%d,r=%d,b=%d)", - full.fLeft, full.fTop, full.fRight, full.fBottom); + finish(); mLast = full; - mLastBase = base(); - if (mStart == mEnd) - mCapture = false; + mLastBase = fullBase; + return false; + } + if (full == mStart) + mCapture = true; + if (mCapture) { + if (baseLinesAgree(mLast, mLastBase, full, fullBase)) + mLast.join(full); + else { + finish(); + mLast = full; + mLastBase = fullBase; + } } + if (full == mEnd) + mCapture = false; return false; } -protected: - SkIRect mStart; + +protected: + bool mCapture; SkIRect mEnd; + int mEndBase; + bool mFlipped; SkIRect mLast; int mLastBase; + SkIRect mSelectRect; SkRegion* mSelectRegion; - bool mCapture; + SkIRect mStart; + int mStartBase; +private: + typedef CommonCheck INHERITED; }; -#define HYPHEN_MINUS 0x2D // ASCII hyphen -#define HYPHEN 0x2010 // unicode hyphen, first in range of dashes -#define HORZ_BAR 0x2015 // unicode horizontal bar, last in range of dashes +static inline bool compareBounds(const SkIRect* first, const SkIRect* second) +{ + return first->fTop < second->fTop; +} class TextExtractor : public CommonCheck { public: - TextExtractor(const SkRegion& region) : mSelectRegion(region), - mSkipFirstSpace(true) { // don't start with a space + TextExtractor(const SkIRect& start, int startBase, const SkIRect& end, + int endBase, const SkIRect& area, bool flipped) + : INHERITED(area.width(), area.height()) + , mEnd(end) + , mEndBase(endBase) + , mFlipped(flipped) + , mRecord(false) + , mSelectRect(start) + , mSkipFirstSpace(true) // don't start with a space + , mStart(start) + , mStartBase(startBase) + { + mEmpty.setEmpty(); + mEnd.offset(-area.fLeft, -area.fTop); + mEndBase -= area.fTop; + mLast.setEmpty(); + mLastBase = INT_MAX; + mSelectRect.join(end); + mSelectRect.offset(-area.fLeft, -area.fTop); + mStart.offset(-area.fLeft, -area.fTop); + mStartBase -= area.fTop; + } + + void addCharacter(const SkBounder::GlyphRec& rec) + { + if (!mSkipFirstSpace) { + if (addNewLine(rec)) { + DBG_NAV_LOG("write new line"); + *mSelectText.append() = '\n'; + *mSelectText.append() = '\n'; + } else if (addSpace(rec)) { + DBG_NAV_LOG("write space"); + *mSelectText.append() = ' '; + } + } else + mSkipFirstSpace = false; + recordGlyph(rec); + finishGlyph(); + if (VERBOSE_LOGGING) DBG_NAV_LOGD("glyphID=%d uni=%d '%c'", rec.fGlyphID, + mLastUni, mLastUni && mLastUni < 0x7f ? mLastUni : '?'); + if (mLastUni) { + uint16_t chars[2]; + size_t count = SkUTF16_FromUnichar(mLastUni, chars); + *mSelectText.append() = chars[0]; + if (count == 2) + *mSelectText.append() = chars[1]; + } } - virtual void setUp(const SkPaint& paint, const SkMatrix& matrix, SkScalar y, - const void* text) { - INHERITED::setUp(paint, matrix, y, text); - SkPaint charPaint = paint; - charPaint.setTextEncoding(SkPaint::kUTF8_TextEncoding); - mMinSpaceWidth = std::max(0, SkScalarToFixed( - charPaint.measureText(" ", 1)) - SK_Fixed1); + void finish() + { + Vector<SkIRect*> sortedBounds; + SkTDArray<uint16_t> temp; + int index; + DBG_NAV_LOGD("mSelectBounds.count=%d text=%d", mSelectBounds.count(), + mSelectText.count()); + for (index = 0; index < mSelectBounds.count(); index++) + sortedBounds.append(&mSelectBounds[index]); + std::sort(sortedBounds.begin(), sortedBounds.end(), compareBounds); + int lastEnd = -1; + for (index = 0; index < mSelectBounds.count(); index++) { + if (sortedBounds[index]->isEmpty()) + continue; + int order = sortedBounds[index] - &mSelectBounds[0]; + int start = order > 0 ? mSelectCount[order - 1] : 0; + int end = mSelectCount[order]; + DBG_NAV_LOGD("order=%d start=%d end=%d top=%d", order, start, end, + mSelectBounds[order].fTop); + int count = temp.count(); + if (count > 0 && temp[count - 1] != '\n' && start != lastEnd) { + // always separate paragraphs when original text is out of order + DBG_NAV_LOG("write new line"); + *temp.append() = '\n'; + *temp.append() = '\n'; + } + temp.append(end - start, &mSelectText[start]); + lastEnd = end; + } + mSelectText.swap(temp); } virtual bool onIRectGlyph(const SkIRect& rect, @@ -217,64 +779,85 @@ public: { SkIRect full; full.set(rect.fLeft, top(), rect.fRight, bottom()); - if (mSelectRegion.contains(full)) { - if (!mSkipFirstSpace && (mLastUni < HYPHEN || mLastUni > HORZ_BAR) - && mLastUni != HYPHEN_MINUS - && (mLastGlyph.fLSB.fY != rec.fLSB.fY // new baseline - || mLastGlyph.fLSB.fX > rec.fLSB.fX // glyphs are LTR - || mLastGlyph.fRSB.fX + mMinSpaceWidth < rec.fLSB.fX)) { - DBG_NAV_LOGD("TextExtractor append space" - " mLast=(%d,%d,r=%d,b=%d) mLastGlyph=((%g,%g),(%g,%g),%d)" - " full=(%d,%d,r=%d,b=%d) rec=((%g,%g),(%g,%g),%d)" - " mMinSpaceWidth=%g", - mLast.fLeft, mLast.fTop, mLast.fRight, mLast.fBottom, - SkFixedToScalar(mLastGlyph.fLSB.fX), - SkFixedToScalar(mLastGlyph.fLSB.fY), - SkFixedToScalar(mLastGlyph.fRSB.fX), - SkFixedToScalar(mLastGlyph.fRSB.fY), mLastGlyph.fGlyphID, - full.fLeft, full.fTop, full.fRight, full.fBottom, - SkFixedToScalar(rec.fLSB.fX), - SkFixedToScalar(rec.fLSB.fY), - SkFixedToScalar(rec.fRSB.fX), - SkFixedToScalar(rec.fRSB.fY), rec.fGlyphID, - SkFixedToScalar(mMinSpaceWidth)); - *mSelectText.append() = ' '; - } else - mSkipFirstSpace = false; - DBG_NAV_LOGD("TextExtractor [%02x] append full=(%d,%d,r=%d,b=%d)", - rec.fGlyphID, full.fLeft, full.fTop, full.fRight, full.fBottom); - SkPaint utfPaint = *mPaint; - utfPaint.setTextEncoding(SkPaint::kUTF16_TextEncoding); - utfPaint.glyphsToUnichars(&rec.fGlyphID, 1, &mLastUni); - if (mLastUni) { - uint16_t chars[2]; - size_t count = SkUTF16_FromUnichar(mLastUni, chars); - *mSelectText.append() = chars[0]; - if (count == 2) - *mSelectText.append() = chars[1]; + int fullBase = base(); + if (mFlipped) { + if (fullBase < mStart.fTop || fullBase > mEnd.fBottom + || (baseLinesAgree(mStart, mStartBase, full, fullBase) + && full.fLeft < mStart.fLeft) + || (baseLinesAgree(mEnd, mEndBase, full, fullBase) + && full.fRight > mEnd.fRight)) { + mSkipFirstSpace = true; + return false; + } + if (baseLinesAgree(mLast, mLastBase, full, fullBase) + && (full.fLeft - mLast.fRight < minSpaceWidth() * 3 + || (SkIRect::Intersects(full, mSelectRect) + && SkIRect::Intersects(mLast, mSelectRect)))) { + mLast.join(full); + addCharacter(rec); + return false; } + if (VERBOSE_LOGGING) DBG_NAV_LOGD("baseLinesAgree=%s" + " mLast=(%d,%d,r=%d,b=%d) mLastBase=%d" + " full=(%d,%d,r=%d,b=%d) fullBase=%d" + " mSelectRect=(%d,%d,r=%d,b=%d)", + baseLinesAgree(mLast, mLastBase, full, fullBase) ? "true" : "false", + mLast.fLeft, mLast.fTop, mLast.fRight, mLast.fBottom, mLastBase, + full.fLeft, full.fTop, full.fRight, full.fBottom, fullBase, + mSelectRect.fLeft, mSelectRect.fTop, mSelectRect.fRight, mSelectRect.fBottom); + *mSelectBounds.append() = SkIRect::Intersects(mLast, mSelectRect) + ? mLast : mEmpty; + *mSelectCount.append() = mSelectText.count(); + addCharacter(rec); mLast = full; - mLastGlyph = rec; - } else { - mSkipFirstSpace = true; - DBG_NAV_LOGD("TextExtractor [%02x] skip full=(%d,%d,r=%d,b=%d)", - rec.fGlyphID, full.fLeft, full.fTop, full.fRight, full.fBottom); + mLastBase = fullBase; + return false; } + if (full == mStart) + mRecord = true; + if (mRecord) + addCharacter(rec); + else + mSkipFirstSpace = true; + if (full == mEnd) + mRecord = false; return false; } WebCore::String text() { + if (mFlipped) + finish(); + // the text has been copied in visual order. Reverse as needed if + // result contains right-to-left characters. + const uint16_t* start = mSelectText.begin(); + const uint16_t* end = mSelectText.end(); + while (start < end) { + SkUnichar ch = SkUTF16_NextUnichar(&start); + WTF::Unicode::Direction charDirection = WTF::Unicode::direction(ch); + if (WTF::Unicode::RightToLeftArabic == charDirection + || WTF::Unicode::RightToLeft == charDirection) { + WebCore::ReverseBidi(mSelectText.begin(), mSelectText.count()); + break; + } + } return WebCore::String(mSelectText.begin(), mSelectText.count()); } protected: - const SkRegion& mSelectRegion; - SkTDArray<uint16_t> mSelectText; + SkIRect mEmpty; + SkIRect mEnd; + int mEndBase; + bool mFlipped; SkIRect mLast; - SkBounder::GlyphRec mLastGlyph; - SkUnichar mLastUni; - SkFixed mMinSpaceWidth; + int mLastBase; + bool mRecord; + SkTDArray<SkIRect> mSelectBounds; + SkTDArray<int> mSelectCount; + SkIRect mSelectRect; + SkTDArray<uint16_t> mSelectText; bool mSkipFirstSpace; + SkIRect mStart; + int mStartBase; private: typedef CommonCheck INHERITED; }; @@ -282,7 +865,7 @@ private: class TextCanvas : public SkCanvas { public: - TextCanvas(CommonCheck* bounder, const SkPicture& picture, const SkIRect& area) + TextCanvas(CommonCheck* bounder, const SkIRect& area) : mBounder(*bounder) { setBounder(bounder); SkBitmap bitmap; @@ -340,49 +923,201 @@ public: CommonCheck& mBounder; }; -void CopyPaste::buildSelection(const SkPicture& picture, const SkIRect& area, - const SkIRect& selStart, const SkIRect& selEnd, SkRegion* region) { +static bool buildSelection(const SkPicture& picture, const SkIRect& area, + const SkIRect& selStart, int startBase, + const SkIRect& selEnd, int endBase, SkRegion* region) +{ DBG_NAV_LOGD("area=(%d, %d, %d, %d) selStart=(%d, %d, %d, %d)" - " selEnd=(%d, %d, %d, %d)", + " selEnd=(%d, %d, %d, %d)", area.fLeft, area.fTop, area.fRight, area.fBottom, selStart.fLeft, selStart.fTop, selStart.fRight, selStart.fBottom, selEnd.fLeft, selEnd.fTop, selEnd.fRight, selEnd.fBottom); - MultilineBuilder builder(selStart, selEnd, area.fLeft, area.fTop, region); - TextCanvas checker(&builder, picture, area); + MultilineBuilder builder(selStart, startBase, selEnd, endBase, area, region); + TextCanvas checker(&builder, area); checker.drawPicture(const_cast<SkPicture&>(picture)); + bool flipped = builder.flipped(); + if (flipped) { + TextCanvas checker(&builder, area); + checker.drawPicture(const_cast<SkPicture&>(picture)); + } + builder.finish(); region->translate(area.fLeft, area.fTop); + return flipped; } -SkIRect CopyPaste::findClosest(const SkPicture& picture, const SkIRect& area, - int x, int y) { - FirstCheck _check(x - area.fLeft, y - area.fTop); - DBG_NAV_LOGD("area=(%d, %d, %d, %d) x=%d y=%d", area.fLeft, area.fTop, - area.fRight, area.fBottom, x, y); - TextCanvas checker(&_check, picture, area); +static SkIRect findClosest(FirstCheck& _check, const SkPicture& picture, + const SkIRect& area, int* base) +{ + DBG_NAV_LOGD("area=(%d, %d, %d, %d)", area.fLeft, area.fTop, + area.fRight, area.fBottom); + TextCanvas checker(&_check, area); checker.drawPicture(const_cast<SkPicture&>(picture)); - _check.offsetBounds(area.fLeft, area.fTop); - return _check.bestBounds(); + _check.finishGlyph(); + return _check.adjustedBounds(area, base); } -WebCore::String CopyPaste::text(const SkPicture& picture, const SkIRect& area, - const SkRegion& region) { - SkRegion copy = region; - copy.translate(-area.fLeft, -area.fTop); - const SkIRect& bounds = copy.getBounds(); - DBG_NAV_LOGD("area=(%d, %d, %d, %d) region=(%d, %d, %d, %d)", - area.fLeft, area.fTop, area.fRight, area.fBottom, - bounds.fLeft, bounds.fTop, bounds.fRight, bounds.fBottom); - TextExtractor extractor(copy); - TextCanvas checker(&extractor, picture, area); +static SkIRect findEdge(const SkPicture& picture, const SkIRect& area, + int x, int y, bool left, int* base) +{ + SkIRect result; + result.setEmpty(); + FirstCheck center(x, y, area); + center.setRecordGlyph(); + int closestBase; + SkIRect closest = findClosest(center, picture, area, &closestBase); + closest.inset(-TOUCH_SLOP, -TOUCH_SLOP); + if (!closest.contains(x, y)) { + DBG_NAV_LOGD("closest=(%d, %d, %d, %d) area=(%d, %d, %d, %d) x/y=%d,%d", + closest.fLeft, closest.fTop, closest.fRight, closest.fBottom, + area.fLeft, area.fTop, area.fRight, area.fBottom, x, y); + return result; + } + EdgeCheck edge(x, y, area, center, left); + do { // detect left or right until there's a gap + DBG_NAV_LOGD("edge=%p picture=%p area=%d,%d,%d,%d", + &edge, &picture, area.fLeft, area.fTop, area.fRight, area.fBottom); + TextCanvas checker(&edge, area); + checker.drawPicture(const_cast<SkPicture&>(picture)); + edge.finishGlyph(); + if (!edge.adjacent()) { + DBG_NAV_LOG("adjacent break"); + break; + } + int nextBase; + const SkIRect& next = edge.bestBounds(&nextBase); + if (next.isEmpty()) { + DBG_NAV_LOG("empty"); + break; + } + if (result == next) { + DBG_NAV_LOG("result == next"); + break; + } + *base = nextBase; + result = next; + edge.shiftStart(result); + } while (true); + if (!result.isEmpty()) { + *base += area.fTop; + result.offset(area.fLeft, area.fTop); + } + return result; +} + +static SkIRect findFirst(const SkPicture& picture, int* base) +{ + FindFirst finder(picture.width(), picture.height()); + SkIRect area; + area.set(0, 0, picture.width(), picture.height()); + TextCanvas checker(&finder, area); + checker.drawPicture(const_cast<SkPicture&>(picture)); + return finder.bestBounds(base); +} + +static SkIRect findLast(const SkPicture& picture, int* base) +{ + FindLast finder(picture.width(), picture.height()); + SkIRect area; + area.set(0, 0, picture.width(), picture.height()); + TextCanvas checker(&finder, area); + checker.drawPicture(const_cast<SkPicture&>(picture)); + return finder.bestBounds(base); +} + +static SkIRect findLeft(const SkPicture& picture, const SkIRect& area, + int x, int y, int* base) +{ + return findEdge(picture, area, x, y, true, base); +} + +static SkIRect findRight(const SkPicture& picture, const SkIRect& area, + int x, int y, int* base) +{ + return findEdge(picture, area, x, y, false, base); +} + +static WebCore::String text(const SkPicture& picture, const SkIRect& area, + const SkIRect& start, int startBase, const SkIRect& end, + int endBase, bool flipped) +{ + TextExtractor extractor(start, startBase, end, endBase, area, flipped); + TextCanvas checker(&extractor, area); checker.drawPicture(const_cast<SkPicture&>(picture)); return extractor.text(); } +#define CONTROL_OFFSET 3 +#define CONTROL_NOTCH 9 +#define CONTROL_HEIGHT 18 +#define CONTROL_WIDTH 12 +#define STROKE_WIDTH 0.4f +#define SLOP 20 + +SelectText::SelectText() +{ + reset(); + SkScalar innerW = CONTROL_WIDTH - STROKE_WIDTH; + SkScalar innerH = CONTROL_HEIGHT - STROKE_WIDTH; + SkPaint paint; + paint.setAntiAlias(true); + paint.setStrokeWidth(STROKE_WIDTH); + + SkPath startPath; + startPath.moveTo(-CONTROL_WIDTH, CONTROL_NOTCH); + startPath.lineTo(-CONTROL_WIDTH, CONTROL_HEIGHT); + startPath.lineTo(0, CONTROL_HEIGHT); + startPath.lineTo(0, CONTROL_OFFSET); + startPath.close(); + + SkCanvas* canvas = m_startControl.beginRecording(CONTROL_WIDTH, CONTROL_HEIGHT); + paint.setStyle(SkPaint::kFill_Style); + paint.setColor(0xD077A14B); + canvas->drawPath(startPath, paint); + paint.setStyle(SkPaint::kStroke_Style); + paint.setColor(0x40000000); + canvas->drawLine(-innerW, CONTROL_NOTCH, -innerW, innerH, paint); + canvas->drawLine(-innerW + STROKE_WIDTH, innerH, -STROKE_WIDTH, innerH, paint); + paint.setColor(0x40ffffff); + canvas->drawLine(0, CONTROL_OFFSET + STROKE_WIDTH, + -CONTROL_WIDTH, CONTROL_NOTCH + STROKE_WIDTH, paint); + canvas->drawLine(-STROKE_WIDTH, CONTROL_NOTCH + STROKE_WIDTH, + -STROKE_WIDTH, innerH, paint); + paint.setColor(0xffaaaaaa); + canvas->drawPath(startPath, paint); + m_startControl.endRecording(); + + SkPath endPath; + endPath.moveTo(0, CONTROL_OFFSET); + endPath.lineTo(0, CONTROL_HEIGHT); + endPath.lineTo(CONTROL_WIDTH, CONTROL_HEIGHT); + endPath.lineTo(CONTROL_WIDTH, CONTROL_NOTCH); + endPath.close(); + + canvas = m_endControl.beginRecording(CONTROL_WIDTH, CONTROL_HEIGHT); + paint.setStyle(SkPaint::kFill_Style); + paint.setColor(0xD077A14B); + canvas->drawPath(endPath, paint); + paint.setStyle(SkPaint::kStroke_Style); + paint.setColor(0x40000000); + canvas->drawLine(STROKE_WIDTH, CONTROL_OFFSET + STROKE_WIDTH, + STROKE_WIDTH, innerH, paint); + canvas->drawLine(STROKE_WIDTH + STROKE_WIDTH, innerH, innerW, innerH, paint); + paint.setColor(0x40ffffff); + canvas->drawLine(0, CONTROL_OFFSET + STROKE_WIDTH, + CONTROL_WIDTH, CONTROL_NOTCH + STROKE_WIDTH, paint); + canvas->drawLine(STROKE_WIDTH, CONTROL_NOTCH + STROKE_WIDTH, + STROKE_WIDTH, innerH, paint); + paint.setColor(0xffaaaaaa); + canvas->drawPath(endPath, paint); + m_endControl.endRecording(); +} + void SelectText::draw(SkCanvas* canvas, LayerAndroid* layer) { - if (layer->picture() != m_picture) - return; - if (m_drawRegion) + // FIXME: layer may not own the original selected picture + m_picture = layer->picture(); + DBG_NAV_LOGD("m_extendSelection=%d m_drawPointer=%d", m_extendSelection, m_drawPointer); + if (m_extendSelection) drawSelectionRegion(canvas); if (m_drawPointer) drawSelectionPointer(canvas); @@ -406,7 +1141,7 @@ void SelectText::drawSelectionPointer(SkCanvas* canvas) paint.setStrokeWidth(SK_Scalar1 * 2); int sc = canvas->save(); canvas->scale(m_inverseScale, m_inverseScale); - canvas->translate(SkIntToScalar(m_selectX), SkIntToScalar(m_selectY)); + canvas->translate(m_selectX, m_selectY); canvas->drawPath(path, paint); if (!m_extendSelection) { paint.setStyle(SkPaint::kFill_Style); @@ -424,19 +1159,92 @@ void SelectText::drawSelectionRegion(SkCanvas* canvas) return; SkIRect ivisBounds; visBounds.round(&ivisBounds); - CopyPaste::buildSelection(*m_picture, ivisBounds, m_selStart, m_selEnd, - &m_selRegion); + ivisBounds.join(m_selStart); + ivisBounds.join(m_selEnd); + DBG_NAV_LOGD("m_selStart=(%d, %d, %d, %d) m_selEnd=(%d, %d, %d, %d)", + m_selStart.fLeft, m_selStart.fTop, m_selStart.fRight, m_selStart.fBottom, + m_selEnd.fLeft, m_selEnd.fTop, m_selEnd.fRight, m_selEnd.fBottom); + m_flipped = buildSelection(*m_picture, ivisBounds, m_selStart, m_startBase, + m_selEnd, m_endBase, &m_selRegion); SkPath path; m_selRegion.getBoundaryPath(&path); + path.setFillType(SkPath::kEvenOdd_FillType); + SkPaint paint; paint.setAntiAlias(true); - paint.setColor(SkColorSetARGB(0x40, 255, 51, 204)); + paint.setColor(SkColorSetARGB(0x80, 151, 200, 73)); canvas->drawPath(path, paint); + // experiment to draw touchable controls that resize the selection + canvas->save(); + canvas->translate(m_selStart.fLeft, m_selStart.fBottom); + canvas->drawPicture(m_startControl); + canvas->restore(); + canvas->save(); + canvas->translate(m_selEnd.fRight, m_selEnd.fBottom); + canvas->drawPicture(m_endControl); + canvas->restore(); +} + +void SelectText::extendSelection(const SkPicture* picture, int x, int y) +{ + SkIRect clipRect = m_visibleRect; + int base; + if (m_startSelection) { + if (!clipRect.contains(x, y) + || !clipRect.contains(m_original.fX, m_original.fY)) { + clipRect.set(m_original.fX, m_original.fY, x, y); + clipRect.sort(); + clipRect.inset(-m_visibleRect.width(), -m_visibleRect.height()); + } + DBG_NAV_LOGD("selStart clip=(%d,%d,%d,%d)", clipRect.fLeft, + clipRect.fTop, clipRect.fRight, clipRect.fBottom); + m_picture = picture; + FirstCheck center(m_original.fX, m_original.fY, clipRect); + m_selStart = m_selEnd = findClosest(center, *picture, clipRect, &base); + m_startBase = m_endBase = base; + m_startSelection = false; + m_extendSelection = true; + m_original.fX = m_original.fY = 0; + } else if (picture != m_picture) + return; + x -= m_original.fX; + y -= m_original.fY; + if (!clipRect.contains(x, y) || !clipRect.contains(m_selStart)) { + clipRect.set(m_selStart.fLeft, m_selStart.fTop, x, y); + clipRect.sort(); + clipRect.inset(-m_visibleRect.width(), -m_visibleRect.height()); + } + DBG_NAV_LOGD("extend clip=(%d,%d,%d,%d)", clipRect.fLeft, + clipRect.fTop, clipRect.fRight, clipRect.fBottom); + FirstCheck extension(x, y, clipRect); + SkIRect found = findClosest(extension, *picture, clipRect, &base); + DBG_NAV_LOGD("pic=%p x=%d y=%d m_startSelection=%s %s=(%d, %d, %d, %d)" + " m_extendSelection=%s", + picture, x, y, m_startSelection ? "true" : "false", + m_hitTopLeft ? "m_selStart" : "m_selEnd", + found.fLeft, found.fTop, found.fRight, found.fBottom, + m_extendSelection ? "true" : "false"); + if (m_hitTopLeft) { + m_startBase = base; + m_selStart = found; + } else { + m_endBase = base; + m_selEnd = found; + } + swapAsNeeded(); } const String SelectText::getSelection() { - String result = CopyPaste::text(*m_picture, m_visibleRect, m_selRegion); + SkIRect clipRect; + clipRect.set(0, 0, m_picture->width(), m_picture->height()); + String result = text(*m_picture, clipRect, m_selStart, m_startBase, + m_selEnd, m_endBase, m_flipped); + DBG_NAV_LOGD("clip=(%d,%d,%d,%d)" + " m_selStart=(%d, %d, %d, %d) m_selEnd=(%d, %d, %d, %d)", + clipRect.fLeft, clipRect.fTop, clipRect.fRight, clipRect.fBottom, + m_selStart.fLeft, m_selStart.fTop, m_selStart.fRight, m_selStart.fBottom, + m_selEnd.fLeft, m_selEnd.fTop, m_selEnd.fRight, m_selEnd.fBottom); DBG_NAV_LOGD("text=%s", result.latin1().data()); // uses CString return result; } @@ -447,35 +1255,173 @@ void SelectText::getSelectionArrow(SkPath* path) 0, 14, 3, 11, 5, 15, 9, 15, 7, 11, 11, 11 }; for (unsigned index = 0; index < sizeof(arrow)/sizeof(arrow[0]); index += 2) - path->lineTo(SkIntToScalar(arrow[index]), SkIntToScalar(arrow[index + 1])); + path->lineTo(arrow[index], arrow[index + 1]); path->close(); } void SelectText::getSelectionCaret(SkPath* path) { - SkScalar height = SkIntToScalar(m_selStart.fBottom - m_selStart.fTop); + SkScalar height = m_selStart.fBottom - m_selStart.fTop; SkScalar dist = height / 4; path->moveTo(0, -height / 2); path->rLineTo(0, height); path->rLineTo(-dist, dist); - path->rMoveTo(0, -SK_Scalar1/2); + path->rMoveTo(0, -0.5f); path->rLineTo(dist * 2, 0); - path->rMoveTo(0, SK_Scalar1/2); + path->rMoveTo(0, 0.5f); path->rLineTo(-dist, -dist); } -void SelectText::moveSelection(const SkPicture* picture, int x, int y, - bool extendSelection) +bool SelectText::hitCorner(int cx, int cy, int x, int y) const +{ + SkIRect test; + test.set(cx, cy, cx, cy); + test.inset(-SLOP, -SLOP); + return test.contains(x, y); +} + +bool SelectText::hitSelection(int x, int y) const +{ + int left = m_selStart.fLeft - CONTROL_WIDTH / 2; + int top = m_selStart.fBottom + CONTROL_HEIGHT / 2; + if (hitCorner(left, top, x, y)) + return true; + int right = m_selEnd.fRight + CONTROL_WIDTH / 2; + int bottom = m_selEnd.fBottom + CONTROL_HEIGHT / 2; + if (hitCorner(right, bottom, x, y)) + return true; + return m_selRegion.contains(x, y); +} + +void SelectText::moveSelection(const SkPicture* picture, int x, int y) { - if (!extendSelection) + SkIRect clipRect = m_visibleRect; + clipRect.join(m_selStart); + clipRect.join(m_selEnd); + if (!m_extendSelection) m_picture = picture; - m_selEnd = CopyPaste::findClosest(*picture, m_visibleRect, x, y); - if (!extendSelection) - m_selStart = m_selEnd; + FirstCheck center(x, y, clipRect); + int base; + SkIRect found = findClosest(center, *picture, clipRect, &base); + if (m_hitTopLeft || !m_extendSelection) { + m_startBase = base; + m_selStart = found; + } + if (!m_hitTopLeft || !m_extendSelection) { + m_endBase = base; + m_selEnd = found; + } + swapAsNeeded(); DBG_NAV_LOGD("x=%d y=%d extendSelection=%s m_selStart=(%d, %d, %d, %d)" - " m_selEnd=(%d, %d, %d, %d)", x, y, extendSelection ? "true" : "false", + " m_selEnd=(%d, %d, %d, %d)", x, y, m_extendSelection ? "true" : "false", + m_selStart.fLeft, m_selStart.fTop, m_selStart.fRight, m_selStart.fBottom, + m_selEnd.fLeft, m_selEnd.fTop, m_selEnd.fRight, m_selEnd.fBottom); +} + +void SelectText::reset() +{ + DBG_NAV_LOG("m_extendSelection=false"); + m_selStart.setEmpty(); + m_selEnd.setEmpty(); + m_extendSelection = false; + m_startSelection = false; +} + +void SelectText::selectAll(const SkPicture* picture) +{ + m_selStart = findFirst(*picture, &m_startBase); + m_selEnd = findLast(*picture, &m_endBase); + m_extendSelection = true; +} + +int SelectText::selectionX() const +{ + return m_hitTopLeft ? m_selStart.fLeft : m_selEnd.fRight; +} + +int SelectText::selectionY() const +{ + const SkIRect& rect = m_hitTopLeft ? m_selStart : m_selEnd; + return (rect.fTop + rect.fBottom) >> 1; +} + +bool SelectText::startSelection(int x, int y) +{ + m_original.fX = x; + m_original.fY = y; + if (m_selStart.isEmpty()) { + DBG_NAV_LOGD("empty start x=%d y=%d", x, y); + m_startSelection = true; + return true; + } + int left = m_selStart.fLeft - CONTROL_WIDTH / 2; + int top = m_selStart.fBottom + CONTROL_HEIGHT / 2; + m_hitTopLeft = hitCorner(left, top, x, y); + int right = m_selEnd.fRight + CONTROL_WIDTH / 2; + int bottom = m_selEnd.fBottom + CONTROL_HEIGHT / 2; + bool hitBottomRight = hitCorner(right, bottom, x, y); + DBG_NAV_LOGD("left=%d top=%d right=%d bottom=%d x=%d y=%d", left, top, + right, bottom, x, y); + if (m_hitTopLeft && (!hitBottomRight || y - top < bottom - y)) { + DBG_NAV_LOG("hit top left"); + m_original.fX -= left; + m_original.fY -= (m_selStart.fTop + m_selStart.fBottom) >> 1; + } else if (hitBottomRight) { + DBG_NAV_LOG("hit bottom right"); + m_original.fX -= right; + m_original.fY -= (m_selEnd.fTop + m_selEnd.fBottom) >> 1; + } + return m_hitTopLeft || hitBottomRight; +} + +/* selects the word at (x, y) +* a word is normally delimited by spaces +* a string of digits (even with inside spaces) is a word (for phone numbers) +* FIXME: digit find isn't implemented yet +* returns true if a word was selected +*/ +bool SelectText::wordSelection(const SkPicture* picture) +{ + int x = m_selStart.fLeft; + int y = (m_selStart.fTop + m_selStart.fBottom) >> 1; + SkIRect clipRect = m_visibleRect; + clipRect.fLeft -= m_visibleRect.width() >> 1; + int base; + SkIRect left = findLeft(*picture, clipRect, x, y, &base); + if (!left.isEmpty()) { + m_startBase = base; + m_selStart = left; + } + x = m_selEnd.fRight; + y = (m_selEnd.fTop + m_selEnd.fBottom) >> 1; + clipRect = m_visibleRect; + clipRect.fRight += m_visibleRect.width() >> 1; + SkIRect right = findRight(*picture, clipRect, x, y, &base); + if (!right.isEmpty()) { + m_endBase = base; + m_selEnd = right; + } + DBG_NAV_LOGD("m_selStart=(%d, %d, %d, %d) m_selEnd=(%d, %d, %d, %d)", m_selStart.fLeft, m_selStart.fTop, m_selStart.fRight, m_selStart.fBottom, m_selEnd.fLeft, m_selEnd.fTop, m_selEnd.fRight, m_selEnd.fBottom); + if (!left.isEmpty() || !right.isEmpty()) { + m_extendSelection = true; + return true; + } + return false; +} + +void SelectText::swapAsNeeded() +{ + if (m_selStart.fTop >= m_selEnd.fBottom + || (m_selStart.fBottom > m_selEnd.fTop + && m_selStart.fRight > m_selEnd.fLeft)) + { + SkTSwap(m_startBase, m_endBase); + SkTSwap(m_selStart, m_selEnd); + m_hitTopLeft ^= true; + DBG_NAV_LOGD("m_hitTopLeft=%s", m_hitTopLeft ? "true" : "false"); + } } } diff --git a/WebKit/android/nav/SelectText.h b/WebKit/android/nav/SelectText.h index 8174046..404e9e7 100644 --- a/WebKit/android/nav/SelectText.h +++ b/WebKit/android/nav/SelectText.h @@ -29,6 +29,7 @@ #include "DrawExtra.h" #include "IntRect.h" #include "PlatformString.h" +#include "SkPath.h" class SkPicture; struct SkIRect; @@ -38,47 +39,57 @@ namespace android { class CachedRoot; -class CopyPaste { -public: - static void buildSelection(const SkPicture& , const SkIRect& area, - const SkIRect& selStart, const SkIRect& selEnd, SkRegion* region); - static SkIRect findClosest(const SkPicture& , const SkIRect& area, - int x, int y); - static String text(const SkPicture& , const SkIRect& area, - const SkRegion& ); -}; - class SelectText : public DrawExtra { public: - SelectText() { - m_selStart.setEmpty(); - m_selEnd.setEmpty(); - } + SelectText(); virtual void draw(SkCanvas* , LayerAndroid* ); + void extendSelection(const SkPicture* , int x, int y); const String getSelection(); - void moveSelection(const SkPicture* , int x, int y, bool extendSelection); + bool hitSelection(int x, int y) const; + void moveSelection(const SkPicture* , int x, int y); + void reset(); + void selectAll(const SkPicture* ); + int selectionX() const; + int selectionY() const; void setDrawPointer(bool drawPointer) { m_drawPointer = drawPointer; } - void setDrawRegion(bool drawRegion) { m_drawRegion = drawRegion; } + void setExtendSelection(bool extend) { m_extendSelection = extend; } void setVisibleRect(const IntRect& rect) { m_visibleRect = rect; } + bool startSelection(int x, int y); + bool wordSelection(const SkPicture* picture); +public: + float m_inverseScale; // inverse scale, x, y used for drawing select path + int m_selectX; + int m_selectY; private: - friend class WebView; void drawSelectionPointer(SkCanvas* ); void drawSelectionRegion(SkCanvas* ); static void getSelectionArrow(SkPath* ); void getSelectionCaret(SkPath* ); + bool hitCorner(int cx, int cy, int x, int y) const; + void swapAsNeeded(); + SkIPoint m_original; // computed start of extend selection SkIRect m_selStart; SkIRect m_selEnd; - SkIRect m_visibleRect; - SkRegion m_selRegion; + int m_startBase; + int m_endBase; + SkIRect m_visibleRect; // constrains picture computations to visible area + SkRegion m_selRegion; // computed from sel start, end + SkPicture m_startControl; + SkPicture m_endControl; const SkPicture* m_picture; - float m_inverseScale; - int m_selectX; - int m_selectY; - bool m_drawRegion; bool m_drawPointer; - bool m_extendSelection; + bool m_extendSelection; // false when trackball is moving pointer + bool m_flipped; + bool m_hitTopLeft; + bool m_startSelection; }; } +namespace WebCore { + +void ReverseBidi(UChar* chars, int len); + +} + #endif diff --git a/WebKit/android/nav/WebView.cpp b/WebKit/android/nav/WebView.cpp index 2ed6148..5154b42 100644 --- a/WebKit/android/nav/WebView.cpp +++ b/WebKit/android/nav/WebView.cpp @@ -30,10 +30,10 @@ #include "AndroidAnimation.h" #include "AndroidLog.h" #include "AtomicString.h" +#include "BaseLayerAndroid.h" #include "CachedFrame.h" #include "CachedNode.h" #include "CachedRoot.h" -#include "CString.h" #include "DrawExtra.h" #include "FindCanvas.h" #include "Frame.h" @@ -68,6 +68,7 @@ #include <JNIHelp.h> #include <jni.h> #include <ui/KeycodeLabels.h> +#include <wtf/text/CString.h> namespace android { @@ -177,7 +178,7 @@ WebView(JNIEnv* env, jobject javaWebView, int viewImpl) : m_lastDx = 0; m_lastDxTime = 0; m_ringAnimationEnd = 0; - m_rootLayer = 0; + m_baseLayer = 0; } ~WebView() @@ -190,7 +191,7 @@ WebView(JNIEnv* env, jobject javaWebView, int viewImpl) : } delete m_frameCacheUI; delete m_navPictureUI; - delete m_rootLayer; + delete m_baseLayer; } WebViewCore* getWebViewCore() const { @@ -303,10 +304,11 @@ void scrollRectOnScreen(const IntRect& rect) SkRect visible; calcOurContentVisibleRect(&visible); #if USE(ACCELERATED_COMPOSITING) - if (m_rootLayer) { - m_rootLayer->updateFixedLayersPositions(visible); - m_rootLayer->updatePositions(); - visible = m_rootLayer->subtractLayers(visible); + LayerAndroid* root = compositeRoot(); + if (root) { + root->updateFixedLayersPositions(visible); + root->updatePositions(); + visible = root->subtractLayers(visible); } #endif int dx = 0; @@ -394,14 +396,30 @@ void drawCursorPostamble() } } -void drawExtras(SkCanvas* canvas, int extras) +PictureSet* draw(SkCanvas* canvas, SkColor bgColor, int extras, bool split) { + PictureSet* ret = 0; + if (!m_baseLayer) { + canvas->drawColor(bgColor); + return ret; + } + + // draw the content of the base layer first + PictureSet* content = m_baseLayer->content(); + int sc = canvas->save(SkCanvas::kClip_SaveFlag); + canvas->clipRect(SkRect::MakeLTRB(0, 0, content->width(), + content->height()), SkRegion::kDifference_Op); + canvas->drawColor(bgColor); + canvas->restoreToCount(sc); + if (content->draw(canvas)) + ret = split ? new PictureSet(*content) : 0; + CachedRoot* root = getFrameCache(AllowNewer); if (!root) { DBG_NAV_LOG("!root"); if (extras == DrawExtrasCursorRing) resetCursorRing(); - return; + return ret; } LayerAndroid mainPicture(m_navPictureUI); DrawExtra* extra = 0; @@ -425,22 +443,24 @@ void drawExtras(SkCanvas* canvas, int extras) if (extra) extra->draw(canvas, &mainPicture); #if USE(ACCELERATED_COMPOSITING) - if (!m_rootLayer) - return; - m_rootLayer->setExtra(extra); + LayerAndroid* compositeLayer = compositeRoot(); + if (!compositeLayer) + return ret; + compositeLayer->setExtra(extra); SkRect visible; calcOurContentVisibleRect(&visible); // call this to be sure we've adjusted for any scrolling or animations // before we actually draw - m_rootLayer->updateFixedLayersPositions(visible); - m_rootLayer->updatePositions(); - // We have to set the canvas' matrix on the root layer + compositeLayer->updateFixedLayersPositions(visible); + compositeLayer->updatePositions(); + // We have to set the canvas' matrix on the base layer // (to have fixed layers work as intended) SkAutoCanvasRestore restore(canvas, true); - m_rootLayer->setMatrix(canvas->getTotalMatrix()); + m_baseLayer->setMatrix(canvas->getTotalMatrix()); canvas->resetMatrix(); - m_rootLayer->draw(canvas); + m_baseLayer->draw(canvas); #endif + return ret; } @@ -565,7 +585,7 @@ CachedRoot* getFrameCache(FrameCachePermission allowNewer) m_viewImpl->m_navPictureKit = 0; m_viewImpl->gFrameCacheMutex.unlock(); if (m_frameCacheUI) - m_frameCacheUI->setRootLayer(m_rootLayer); + m_frameCacheUI->setRootLayer(compositeRoot()); #if USE(ACCELERATED_COMPOSITING) if (layerId >= 0) { SkRect visible; @@ -892,7 +912,8 @@ bool motionUp(int x, int y, int slop) viewInvalidate(); if (!result->isTextInput()) { clearTextEntry(); - setFollowedLink(true); + if (!result->isSelect() && !result->isContentEditable()) + setFollowedLink(true); if (syntheticLink) overrideUrlLoading(result->getExport()); } @@ -946,7 +967,7 @@ String getSelection() return m_selectText.getSelection(); } -void moveSelection(int x, int y, bool extendSelection) +void moveSelection(int x, int y) { const CachedRoot* root = getFrameCache(DontAllowNewer); if (!root) @@ -957,26 +978,81 @@ void moveSelection(int x, int y, bool extendSelection) IntRect visibleRect; getVisibleRect(&visibleRect); m_selectText.setVisibleRect(visibleRect); - m_selectText.moveSelection(picture, x, y, extendSelection); + m_selectText.moveSelection(picture, x, y); +} + +void selectAll() +{ + const CachedRoot* root = getFrameCache(DontAllowNewer); + if (!root) + return; + SkPicture* picture = root->pictureAt(0, 0); + m_selectText.selectAll(picture); +} + +int selectionX() +{ + return m_selectText.selectionX(); } -void setSelectionPointer(bool set, float scale, int x, int y, - bool extendSelection) +int selectionY() +{ + return m_selectText.selectionY(); +} + +void resetSelection() +{ + m_selectText.reset(); +} + +bool startSelection(int x, int y) +{ + return m_selectText.startSelection(x, y); +} + +bool wordSelection(int x, int y) +{ + startSelection(x, y); + if (!extendSelection(x, y)) + return false; + m_selectText.setDrawPointer(false); + SkPicture* picture = getFrameCache(DontAllowNewer)->pictureAt(x, y); + return m_selectText.wordSelection(picture); +} + +bool extendSelection(int x, int y) +{ + const CachedRoot* root = getFrameCache(DontAllowNewer); + if (!root) + return false; + SkPicture* picture = root->pictureAt(x, y); + IntRect visibleRect; + getVisibleRect(&visibleRect); + m_selectText.setVisibleRect(visibleRect); + m_selectText.extendSelection(picture, x, y); + return true; +} + +bool hitSelection(int x, int y) +{ + return m_selectText.hitSelection(x, y); +} + +void setExtendSelection() +{ + m_selectText.setExtendSelection(true); +} + +void setSelectionPointer(bool set, float scale, int x, int y) { m_selectText.setDrawPointer(set); if (!set) return; - m_selectText.m_extendSelection = extendSelection; m_selectText.m_inverseScale = scale; m_selectText.m_selectX = x; m_selectText.m_selectY = y; } -void setSelectionRegion(bool set) -{ - m_selectText.setDrawRegion(set); -} - void sendMoveFocus(WebCore::Frame* framePtr, WebCore::Node* nodePtr) { DBG_NAV_LOGD("framePtr=%p nodePtr=%p", framePtr, nodePtr); @@ -1035,6 +1111,11 @@ void setMatches(WTF::Vector<MatchInfo>* matches) viewInvalidate(); } +int currentMatchIndex() +{ + return m_findOnPage.currentMatchIndex(); +} + bool scrollBy(int dx, int dy) { LOG_ASSERT(m_javaGlue.m_obj, "A java object was not associated with this native WebView!"); @@ -1109,20 +1190,49 @@ int moveGeneration() return m_viewImpl->m_moveGeneration; } -LayerAndroid* rootLayer() const +LayerAndroid* compositeRoot() const { - return m_rootLayer; + LOG_ASSERT(!m_baseLayer || m_baseLayer->countChildren() == 1, + "base layer can't have more than one child %s", __FUNCTION__); + if (m_baseLayer && m_baseLayer->countChildren() == 1) + return static_cast<LayerAndroid*>(m_baseLayer->getChild(0)); + else + return 0; } -void setRootLayer(LayerAndroid* layer) +void setBaseLayer(BaseLayerAndroid* layer) { - delete m_rootLayer; - m_rootLayer = layer; + delete m_baseLayer; + m_baseLayer = layer; CachedRoot* root = getFrameCache(DontAllowNewer); if (!root) return; root->resetLayers(); - root->setRootLayer(m_rootLayer); + root->setRootLayer(compositeRoot()); +} + +void replaceBaseContent(PictureSet* set) +{ + if (!m_baseLayer) + return; + m_baseLayer->setContent(*set); + delete set; +} + +void copyBaseContentToPicture(SkPicture* picture) +{ + if (!m_baseLayer) + return; + PictureSet* content = m_baseLayer->content(); + content->draw(picture->beginRecording(content->width(), content->height(), + SkPicture::kUsePathBoundsForClip_RecordingFlag)); + picture->endRecording(); +} + +bool hasContent() { + if (!m_baseLayer) + return false; + return !m_baseLayer->content()->isEmpty(); } private: // local state for WebView @@ -1139,7 +1249,7 @@ private: // local state for WebView SelectText m_selectText; FindOnPage m_findOnPage; CursorRing m_ring; - LayerAndroid* m_rootLayer; + BaseLayerAndroid* m_baseLayer; }; // end of WebView class /* @@ -1272,6 +1382,19 @@ static const CachedInput* getInputCandidate(JNIEnv *env, jobject obj) return cursor ? frame->textInput(cursor) : 0; } +static jboolean nativePageShouldHandleShiftAndArrows(JNIEnv *env, jobject obj) +{ + const CachedNode* focus = getFocusNode(env, obj); + if (!focus) return false; + // Plugins handle shift and arrows whether or not they have focus. + if (focus->isPlugin()) return true; + const CachedNode* cursor = getCursorNode(env, obj); + // ContentEditable nodes should only receive shift and arrows if they have + // both the cursor and the focus. + return cursor && cursor->nodePointer() == focus->nodePointer() + && cursor->isContentEditable(); +} + static jboolean nativeCursorMatchesFocus(JNIEnv *env, jobject obj) { const CachedNode* cursor = getCursorNode(env, obj); @@ -1357,28 +1480,43 @@ static void nativeDebugDump(JNIEnv *env, jobject obj) #endif } -static void nativeDrawExtras(JNIEnv *env, jobject obj, jobject canv, jint extras) -{ +static jint nativeDraw(JNIEnv *env, jobject obj, jobject canv, jint color, + jint extras, jboolean split) { SkCanvas* canvas = GraphicsJNI::getNativeCanvas(env, canv); - GET_NATIVE_VIEW(env, obj)->drawExtras(canvas, extras); + return reinterpret_cast<jint>(GET_NATIVE_VIEW(env, obj)->draw(canvas, color, extras, split)); } static bool nativeEvaluateLayersAnimations(JNIEnv *env, jobject obj) { #if USE(ACCELERATED_COMPOSITING) - const LayerAndroid* root = GET_NATIVE_VIEW(env, obj)->rootLayer(); + const LayerAndroid* root = GET_NATIVE_VIEW(env, obj)->compositeRoot(); if (root) return root->evaluateAnimations(); #endif return false; } -static void nativeSetRootLayer(JNIEnv *env, jobject obj, jint layer) +static void nativeSetBaseLayer(JNIEnv *env, jobject obj, jint layer) { -#if USE(ACCELERATED_COMPOSITING) - LayerAndroid* layerImpl = reinterpret_cast<LayerAndroid*>(layer); - GET_NATIVE_VIEW(env, obj)->setRootLayer(layerImpl); -#endif + BaseLayerAndroid* layerImpl = reinterpret_cast<BaseLayerAndroid*>(layer); + GET_NATIVE_VIEW(env, obj)->setBaseLayer(layerImpl); +} + +static void nativeReplaceBaseContent(JNIEnv *env, jobject obj, jint content) +{ + PictureSet* set = reinterpret_cast<PictureSet*>(content); + GET_NATIVE_VIEW(env, obj)->replaceBaseContent(set); +} + +static void nativeCopyBaseContentToPicture(JNIEnv *env, jobject obj, jobject pict) +{ + SkPicture* picture = GraphicsJNI::getNativePicture(env, pict); + GET_NATIVE_VIEW(env, obj)->copyBaseContentToPicture(picture); +} + +static bool nativeHasContent(JNIEnv *env, jobject obj) +{ + return GET_NATIVE_VIEW(env, obj)->hasContent(); } static jobject nativeImageURI(JNIEnv *env, jobject obj, jint x, jint y) @@ -1568,7 +1706,7 @@ static jobject nativeSubtractLayers(JNIEnv* env, jobject obj, jobject jrect) { SkIRect irect = jrect_to_webrect(env, jrect); #if USE(ACCELERATED_COMPOSITING) - LayerAndroid* root = GET_NATIVE_VIEW(env, obj)->rootLayer(); + LayerAndroid* root = GET_NATIVE_VIEW(env, obj)->compositeRoot(); if (root) { SkRect rect; rect.set(irect); @@ -1644,9 +1782,12 @@ static void nativeSetFindIsEmpty(JNIEnv *env, jobject obj) static void nativeSetFollowedLink(JNIEnv *env, jobject obj, bool followed) { - WebView* view = GET_NATIVE_VIEW(env, obj); - LOG_ASSERT(view, "view not set in %s", __FUNCTION__); - view->setFollowedLink(followed); + const CachedNode* cursor = getCursorNode(env, obj); + if (cursor && !cursor->isSelect() && ! cursor->isContentEditable()) { + WebView* view = GET_NATIVE_VIEW(env, obj); + LOG_ASSERT(view, "view not set in %s", __FUNCTION__); + view->setFollowedLink(followed); + } } static void nativeSetHeightCanMeasure(JNIEnv *env, jobject obj, bool measure) @@ -1735,6 +1876,13 @@ static void nativeFindNext(JNIEnv *env, jobject obj, bool forward) view->findNext(forward); } +static int nativeFindIndex(JNIEnv *env, jobject obj) +{ + WebView* view = GET_NATIVE_VIEW(env, obj); + LOG_ASSERT(view, "view not set in nativeFindIndex"); + return view->currentMatchIndex(); +} + static void nativeUpdateCachedTextfield(JNIEnv *env, jobject obj, jstring updatedText, jint generation) { WebView* view = GET_NATIVE_VIEW(env, obj); @@ -1805,11 +1953,39 @@ static int nativeMoveGeneration(JNIEnv *env, jobject obj) return view->moveGeneration(); } -static void nativeMoveSelection(JNIEnv *env, jobject obj, int x, int y, bool ex) +static void nativeMoveSelection(JNIEnv *env, jobject obj, int x, int y) { - WebView* view = GET_NATIVE_VIEW(env, obj); - LOG_ASSERT(view, "view not set in %s", __FUNCTION__); - view->moveSelection(x, y, ex); + GET_NATIVE_VIEW(env, obj)->moveSelection(x, y); +} + +static void nativeResetSelection(JNIEnv *env, jobject obj) +{ + return GET_NATIVE_VIEW(env, obj)->resetSelection(); +} + +static void nativeSelectAll(JNIEnv* env, jobject obj) +{ + GET_NATIVE_VIEW(env, obj)->selectAll(); +} + +static void nativeSetExtendSelection(JNIEnv *env, jobject obj) +{ + GET_NATIVE_VIEW(env, obj)->setExtendSelection(); +} + +static jboolean nativeStartSelection(JNIEnv *env, jobject obj, int x, int y) +{ + return GET_NATIVE_VIEW(env, obj)->startSelection(x, y); +} + +static jboolean nativeWordSelection(JNIEnv *env, jobject obj, int x, int y) +{ + return GET_NATIVE_VIEW(env, obj)->wordSelection(x, y); +} + +static void nativeExtendSelection(JNIEnv *env, jobject obj, int x, int y) +{ + GET_NATIVE_VIEW(env, obj)->extendSelection(x, y); } static jobject nativeGetSelection(JNIEnv *env, jobject obj) @@ -1820,15 +1996,25 @@ static jobject nativeGetSelection(JNIEnv *env, jobject obj) return env->NewString((jchar*)selection.characters(), selection.length()); } -static void nativeSetSelectionPointer(JNIEnv *env, jobject obj, jboolean set, - jfloat scale, jint x, jint y, bool ex) +static jboolean nativeHitSelection(JNIEnv *env, jobject obj, int x, int y) { - GET_NATIVE_VIEW(env, obj)->setSelectionPointer(set, scale, x, y, ex); + return GET_NATIVE_VIEW(env, obj)->hitSelection(x, y); } -static void nativeSetSelectionRegion(JNIEnv *env, jobject obj, jboolean set) +static jint nativeSelectionX(JNIEnv *env, jobject obj) { - GET_NATIVE_VIEW(env, obj)->setSelectionRegion(set); + return GET_NATIVE_VIEW(env, obj)->selectionX(); +} + +static jint nativeSelectionY(JNIEnv *env, jobject obj) +{ + return GET_NATIVE_VIEW(env, obj)->selectionY(); +} + +static void nativeSetSelectionPointer(JNIEnv *env, jobject obj, jboolean set, + jfloat scale, jint x, jint y) +{ + GET_NATIVE_VIEW(env, obj)->setSelectionPointer(set, scale, x, y); } #ifdef ANDROID_DUMP_DISPLAY_TREE @@ -1859,26 +2045,13 @@ static void nativeDumpDisplayTree(JNIEnv* env, jobject jwebview, jstring jurl) SkDumpCanvas canvas(&dumper); // this will playback the picture into the canvas, which will // spew its contents to the dumper - view->getWebViewCore()->drawContent(&canvas, 0); -#if USE(ACCELERATED_COMPOSITING) - if (true) { - LayerAndroid* rootLayer = view->rootLayer(); - if (rootLayer) { - // We have to set the canvas' matrix on the root layer - // (to have fixed layers work as intended) - SkAutoCanvasRestore restore(&canvas, true); - rootLayer->setMatrix(canvas.getTotalMatrix()); - canvas.resetMatrix(); - rootLayer->draw(&canvas); - } - } -#endif + view->draw(&canvas, 0, 0, false); // we're done with the file now fwrite("\n", 1, 1, file); fclose(file); } #if USE(ACCELERATED_COMPOSITING) - const LayerAndroid* rootLayer = view->rootLayer(); + const LayerAndroid* rootLayer = view->compositeRoot(); if (rootLayer) { FILE* file = fopen(LAYERS_TREE_LOG_FILE,"w"); if (file) { @@ -1907,6 +2080,8 @@ static JNINativeMethod gJavaWebViewMethods[] = { (void*) nativeCreate }, { "nativeCursorFramePointer", "()I", (void*) nativeCursorFramePointer }, + { "nativePageShouldHandleShiftAndArrows", "()Z", + (void*) nativePageShouldHandleShiftAndArrows }, { "nativeCursorMatchesFocus", "()Z", (void*) nativeCursorMatchesFocus }, { "nativeCursorNodeBounds", "()Landroid/graphics/Rect;", @@ -1929,16 +2104,20 @@ static JNINativeMethod gJavaWebViewMethods[] = { (void*) nativeDebugDump }, { "nativeDestroy", "()V", (void*) nativeDestroy }, - { "nativeDrawExtras", "(Landroid/graphics/Canvas;I)V", - (void*) nativeDrawExtras }, + { "nativeDraw", "(Landroid/graphics/Canvas;IIZ)I", + (void*) nativeDraw }, { "nativeDumpDisplayTree", "(Ljava/lang/String;)V", (void*) nativeDumpDisplayTree }, { "nativeEvaluateLayersAnimations", "()Z", (void*) nativeEvaluateLayersAnimations }, + { "nativeExtendSelection", "(II)V", + (void*) nativeExtendSelection }, { "nativeFindAll", "(Ljava/lang/String;Ljava/lang/String;)I", (void*) nativeFindAll }, { "nativeFindNext", "(Z)V", (void*) nativeFindNext }, + { "nativeFindIndex", "()I", + (void*) nativeFindIndex}, { "nativeFocusCandidateFramePointer", "()I", (void*) nativeFocusCandidateFramePointer }, { "nativeFocusCandidateHasNextTextfield", "()Z", @@ -1979,6 +2158,8 @@ static JNINativeMethod gJavaWebViewMethods[] = { (void*) nativeHasFocusNode }, { "nativeHideCursor", "()V", (void*) nativeHideCursor }, + { "nativeHitSelection", "(II)Z", + (void*) nativeHitSelection }, { "nativeImageURI", "(II)Ljava/lang/String;", (void*) nativeImageURI }, { "nativeInstrumentReport", "()V", @@ -1991,14 +2172,24 @@ static JNINativeMethod gJavaWebViewMethods[] = { (void*) nativeMoveCursorToNextTextInput }, { "nativeMoveGeneration", "()I", (void*) nativeMoveGeneration }, - { "nativeMoveSelection", "(IIZ)V", + { "nativeMoveSelection", "(II)V", (void*) nativeMoveSelection }, { "nativePointInNavCache", "(III)Z", (void*) nativePointInNavCache }, { "nativeRecordButtons", "(ZZZ)V", (void*) nativeRecordButtons }, + { "nativeResetSelection", "()V", + (void*) nativeResetSelection }, + { "nativeSelectAll", "()V", + (void*) nativeSelectAll }, { "nativeSelectBestAt", "(Landroid/graphics/Rect;)V", (void*) nativeSelectBestAt }, + { "nativeSelectionX", "()I", + (void*) nativeSelectionX }, + { "nativeSelectionY", "()I", + (void*) nativeSelectionY }, + { "nativeSetExtendSelection", "()V", + (void*) nativeSetExtendSelection }, { "nativeSetFindIsEmpty", "()V", (void*) nativeSetFindIsEmpty }, { "nativeSetFindIsUp", "(Z)V", @@ -2007,18 +2198,26 @@ static JNINativeMethod gJavaWebViewMethods[] = { (void*) nativeSetFollowedLink }, { "nativeSetHeightCanMeasure", "(Z)V", (void*) nativeSetHeightCanMeasure }, - { "nativeSetRootLayer", "(I)V", - (void*) nativeSetRootLayer }, - { "nativeSetSelectionPointer", "(ZFIIZ)V", + { "nativeSetBaseLayer", "(I)V", + (void*) nativeSetBaseLayer }, + { "nativeReplaceBaseContent", "(I)V", + (void*) nativeReplaceBaseContent }, + { "nativeCopyBaseContentToPicture", "(Landroid/graphics/Picture;)V", + (void*) nativeCopyBaseContentToPicture }, + { "nativeHasContent", "()Z", + (void*) nativeHasContent }, + { "nativeSetSelectionPointer", "(ZFII)V", (void*) nativeSetSelectionPointer }, - { "nativeSetSelectionRegion", "(Z)V", - (void*) nativeSetSelectionRegion }, + { "nativeStartSelection", "(II)Z", + (void*) nativeStartSelection }, { "nativeSubtractLayers", "(Landroid/graphics/Rect;)Landroid/graphics/Rect;", (void*) nativeSubtractLayers }, { "nativeTextGeneration", "()I", (void*) nativeTextGeneration }, { "nativeUpdateCachedTextfield", "(Ljava/lang/String;I)V", (void*) nativeUpdateCachedTextfield }, + { "nativeWordSelection", "(II)Z", + (void*) nativeWordSelection }, { "nativeGetBlockLeftEdge", "(IIF)I", (void*) nativeGetBlockLeftEdge }, }; |
