root/src/apps/haikudepot/textview/TextDocumentLayout.cpp
/*
 * Copyright 2013-2015, Stephan Aßmus <superstippi@gmx.de>.
 * All rights reserved. Distributed under the terms of the MIT License.
 */

#include "TextDocumentLayout.h"

#include <new>
#include <stdio.h>

#include <View.h>


class LayoutTextListener : public TextListener {
public:
        LayoutTextListener(TextDocumentLayout* layout)
                :
                fLayout(layout)
        {
        }

        virtual void TextChanging(TextChangingEvent& event)
        {
        }

        virtual void TextChanged(const TextChangedEvent& event)
        {
//              printf("TextChanged(%" B_PRIi32 ", %" B_PRIi32 ")\n",
//                      event.FirstChangedParagraph(),
//                      event.ChangedParagraphCount());
                // TODO: The event does not contain useful data. Make the event
                // system work so only the affected paragraphs are updated.
                // I think we need "first affected", "last affected" (both relative
                // to the original paragraph count), and than how many new paragraphs
                // are between these. From the difference in old number of paragraphs
                // inbetween and the new count, we know how many new paragraphs are
                // missing, and the rest in the range needs to be updated.
//              fLayout->InvalidateParagraphs(event.FirstChangedParagraph(),
//                      event.ChangedParagraphCount());
                fLayout->Invalidate();
        }

private:
        TextDocumentLayout*     fLayout;
};



TextDocumentLayout::TextDocumentLayout()
        :
        fWidth(0.0f),
        fLayoutValid(false),

        fDocument(),
        fTextListener(new(std::nothrow) LayoutTextListener(this), true),
        fParagraphLayouts()
{
}


TextDocumentLayout::TextDocumentLayout(const TextDocumentRef& document)
        :
        fWidth(0.0f),
        fLayoutValid(false),

        fDocument(),
        fTextListener(new(std::nothrow) LayoutTextListener(this), true),
        fParagraphLayouts()
{
        SetTextDocument(document);
}


TextDocumentLayout::TextDocumentLayout(const TextDocumentLayout& other)
        :
        fWidth(other.fWidth),
        fLayoutValid(other.fLayoutValid),

        fDocument(other.fDocument),
        fTextListener(new(std::nothrow) LayoutTextListener(this), true),
        fParagraphLayouts(other.fParagraphLayouts)
{
        if (fDocument.IsSet())
                fDocument->AddListener(fTextListener);
}


TextDocumentLayout::~TextDocumentLayout()
{
        SetTextDocument(NULL);
}


void
TextDocumentLayout::SetTextDocument(const TextDocumentRef& document)
{
        if (fDocument == document)
                return;

        if (fDocument.IsSet())
                fDocument->RemoveListener(fTextListener);

        fDocument = document;
        _Init();
        fLayoutValid = false;

        if (fDocument.IsSet())
                fDocument->AddListener(fTextListener);
}


void
TextDocumentLayout::Invalidate()
{
        if (fDocument.IsSet())
                InvalidateParagraphs(0, fDocument->CountParagraphs());
}


void
TextDocumentLayout::InvalidateParagraphs(int32 start, int32 count)
{
        if (start < 0 || count == 0 || !fDocument.IsSet())
                return;

        fLayoutValid = false;

        while (count > 0) {
                const int32 paragraphCount = fDocument->CountParagraphs();
                if (start >= paragraphCount)
                        break;
                const Paragraph& paragraph = fDocument->ParagraphAtIndex(start);
                if (start >= static_cast<int32>(fParagraphLayouts.size())) {
                        ParagraphLayoutRef layout(new(std::nothrow) ParagraphLayout(
                                paragraph), true);
                        if (!layout.IsSet()) {
                                fprintf(stderr, "TextDocumentLayout::InvalidateParagraphs() - "
                                        "out of memory\n");
                                return;
                        }
                        try {
                                fParagraphLayouts.push_back(ParagraphLayoutInfo(0.0f, layout));
                        }
                        catch (std::bad_alloc& ba) {
                                fprintf(stderr, "bad_alloc when invalidating paragraphs\n");
                                return;
                        }
                } else {
                        const ParagraphLayoutInfo& info = fParagraphLayouts[start];
                        info.layout->SetParagraph(paragraph);
                }

                start++;
                count--;
        }

        // Remove any extra paragraph layouts
        while (fDocument->CountParagraphs()
                        < static_cast<int32>(fParagraphLayouts.size()))
                fParagraphLayouts.erase(fParagraphLayouts.end() - 1);
}


void
TextDocumentLayout::SetWidth(float width)
{
        if (fWidth != width) {
                fWidth = width;
                fLayoutValid = false;
        }
}


float
TextDocumentLayout::Height()
{
        _ValidateLayout();

        float height = 0.0f;

        if (fParagraphLayouts.size() > 0) {
                const ParagraphLayoutInfo& lastLayout
                        = fParagraphLayouts[fParagraphLayouts.size() - 1];
                height = lastLayout.y + lastLayout.layout->Height();
        }

        return height;
}


void
TextDocumentLayout::Draw(BView* view, const BPoint& offset,
        const BRect& updateRect)
{
        _ValidateLayout();

        int layoutCount = fParagraphLayouts.size();
        for (int i = 0; i < layoutCount; i++) {
                const ParagraphLayoutInfo& layout = fParagraphLayouts[i];
                BPoint location(offset.x, offset.y + layout.y);
                if (location.y > updateRect.bottom)
                        break;
                if (location.y + layout.layout->Height() > updateRect.top)
                        layout.layout->Draw(view, location);
        }
}


int32
TextDocumentLayout::LineIndexForOffset(int32 textOffset)
{
        int32 index = _ParagraphLayoutIndexForOffset(textOffset);
        if (index >= 0) {
                int32 lineIndex = 0;
                for (int32 i = 0; i < index; i++) {
                        lineIndex += fParagraphLayouts[i].layout->CountLines();
                }

                const ParagraphLayoutInfo& info = fParagraphLayouts[index];
                return lineIndex + info.layout->LineIndexForOffset(textOffset);
        }

        return 0;
}


int32
TextDocumentLayout::FirstOffsetOnLine(int32 lineIndex)
{
        int32 paragraphOffset;
        int32 index = _ParagraphLayoutIndexForLineIndex(lineIndex, paragraphOffset);
        if (index >= 0) {
                const ParagraphLayoutInfo& info = fParagraphLayouts[index];
                return info.layout->FirstOffsetOnLine(lineIndex) + paragraphOffset;
        }

        return 0;
}


int32
TextDocumentLayout::LastOffsetOnLine(int32 lineIndex)
{
        int32 paragraphOffset;
        int32 index = _ParagraphLayoutIndexForLineIndex(lineIndex, paragraphOffset);
        if (index >= 0) {
                const ParagraphLayoutInfo& info = fParagraphLayouts[index];
                return info.layout->LastOffsetOnLine(lineIndex) + paragraphOffset;
        }

        return 0;
}


int32
TextDocumentLayout::CountLines()
{
        _ValidateLayout();

        int32 lineCount = 0;

        int32 count = fParagraphLayouts.size();
        for (int32 i = 0; i < count; i++) {
                const ParagraphLayoutInfo& info = fParagraphLayouts[i];
                lineCount += info.layout->CountLines();
        }

        return lineCount;
}


void
TextDocumentLayout::GetLineBounds(int32 lineIndex, float& x1, float& y1,
        float& x2, float& y2)
{
        int32 paragraphOffset;
        int32 index = _ParagraphLayoutIndexForLineIndex(lineIndex, paragraphOffset);
        if (index >= 0) {
                const ParagraphLayoutInfo& info = fParagraphLayouts[index];
                info.layout->GetLineBounds(lineIndex, x1, y1, x2, y2);
                y1 += info.y;
                y2 += info.y;
                return;
        }

        x1 = 0.0f;
        y1 = 0.0f;
        x2 = 0.0f;
        y2 = 0.0f;
}


void
TextDocumentLayout::GetTextBounds(int32 textOffset, float& x1, float& y1,
        float& x2, float& y2)
{
        int32 index = _ParagraphLayoutIndexForOffset(textOffset);
        if (index >= 0) {
                const ParagraphLayoutInfo& info = fParagraphLayouts[index];
                info.layout->GetTextBounds(textOffset, x1, y1, x2, y2);
                y1 += info.y;
                y2 += info.y;
                return;
        }

        x1 = 0.0f;
        y1 = 0.0f;
        x2 = 0.0f;
        y2 = 0.0f;
}


int32
TextDocumentLayout::TextOffsetAt(float x, float y, bool& rightOfCenter)
{
        _ValidateLayout();

        int32 textOffset = 0;
        rightOfCenter = false;

        int32 paragraphs = fParagraphLayouts.size();
        for (int32 i = 0; i < paragraphs; i++) {
                const ParagraphLayoutInfo& info = fParagraphLayouts[i];
                if (y > info.y + info.layout->Height()) {
                        textOffset += info.layout->CountGlyphs();
                        continue;
                }

                textOffset += info.layout->TextOffsetAt(x, y - info.y, rightOfCenter);
                break;
        }

        return textOffset;
}

// #pragma mark - private


void
TextDocumentLayout::_ValidateLayout()
{
        if (!fLayoutValid) {
                _Layout();
                fLayoutValid = true;
        }
}


void
TextDocumentLayout::_Init()
{
        fParagraphLayouts.clear();

        if (!fDocument.IsSet())
                return;

        int paragraphCount = fDocument->CountParagraphs();
        for (int i = 0; i < paragraphCount; i++) {
                const Paragraph& paragraph = fDocument->ParagraphAtIndex(i);
                ParagraphLayoutRef layout(new(std::nothrow) ParagraphLayout(paragraph),
                        true);
                if (!layout.IsSet()) {
                        fprintf(stderr, "TextDocumentLayout::_Layout() - out of memory\n");
                        return;
                }
                try {
                        fParagraphLayouts.push_back(ParagraphLayoutInfo(0.0f, layout));
                }
                catch (std::bad_alloc& ba) {
                        fprintf(stderr, "bad_alloc when inititalizing the text document "
                                "layout\n");
                        return;
                }
        }
}


void
TextDocumentLayout::_Layout()
{
        float y = 0.0f;

        int layoutCount = fParagraphLayouts.size();
        for (int i = 0; i < layoutCount; i++) {
                ParagraphLayoutInfo info = fParagraphLayouts[i];
                const ParagraphStyle& style = info.layout->Style();

                if (i > 0)
                        y += style.SpacingTop();

                fParagraphLayouts[i] = ParagraphLayoutInfo(y, info.layout);

                info.layout->SetWidth(fWidth);
                y += info.layout->Height() + style.SpacingBottom();
        }
}


int32
TextDocumentLayout::_ParagraphLayoutIndexForOffset(int32& textOffset)
{
        _ValidateLayout();

        int32 paragraphs = fParagraphLayouts.size();
        for (int32 i = 0; i < paragraphs - 1; i++) {
                const ParagraphLayoutInfo& info = fParagraphLayouts[i];

                int32 length = info.layout->CountGlyphs();
                if (textOffset >= length) {
                        textOffset -= length;
                        continue;
                }

                return i;
        }

        if (paragraphs > 0) {
                const ParagraphLayoutInfo& info
                        = fParagraphLayouts[fParagraphLayouts.size() - 1];

                // Return last paragraph if the textOffset is still within or
                // exactly behind the last valid offset in that paragraph.
                int32 length = info.layout->CountGlyphs();
                if (textOffset <= length)
                        return paragraphs - 1;
        }

        return -1;
}

int32
TextDocumentLayout::_ParagraphLayoutIndexForLineIndex(int32& lineIndex,
        int32& paragraphOffset)
{
        _ValidateLayout();

        paragraphOffset = 0;
        int32 paragraphs = fParagraphLayouts.size();
        for (int32 i = 0; i < paragraphs; i++) {
                const ParagraphLayoutInfo& info = fParagraphLayouts[i];

                int32 lineCount = info.layout->CountLines();
                if (lineIndex >= lineCount) {
                        lineIndex -= lineCount;
                        paragraphOffset += info.layout->CountGlyphs();
                        continue;
                }

                return i;
        }

        return -1;
}