summaryrefslogblamecommitdiff
path: root/src/gui/widgets/browserbox.cpp
blob: 9eee94487a4e8bfe77d528b8742d183955820aa1 (plain) (tree)
1
2
3
4
5
6
7
8
9
  
                   
                                                            
                                                
                                                
  
                                         
  
                                                                        



                                                                        
                                                                   




                                                                     
                                                                         

   

                                   
                    
                             

                                    

                               

                            

                              
                               
                           
                                    
 
                    
 
                    
 
                                   
 
              
                    






                               
 
                                             






                                                  
 

                                                                     

 
 
                                  
               
 

                           
 
 
                                    
 
                                               
 
                                               
 

                                        
     
                              
 
                                                       
                                   
                                         
         

                                                   
 

                                                                       
 


                                                                   
 



                                                       
                                                        



                                             

                                                
 
                                   
                             
             
                                     

                                  
         
 
                           
     


                                              
                          

     







                                                        
 



                                                       
     





                                                  
         
                                        
                                        
 
                                        
                                             
         

     
                                         




                            

                         
                        

 
                                                     
 


                      
                                                  
 
                       
                                                     

                                            

 
                                                   
 
                                                  

                                                                      

 




                                                    
                                              
 
                                                                  



                                   
 

                                       
 
                     
     
                                   
 

                                               
                                        
         
                                                                       
                                          

         
                                       
         
                                                                       



                                                     


         
                                     
     





                                          
 
                                  
 






                                                                           
                                                                                  
                    
                                                                                  
             
 
                         



                                                                           



                                                                          
             
 

                                           
                                                                  
         
     

 
   
                                                                        

                               
 
                                     
 

                                    
 
                                  
                          

                         
 





                                                                                 



                                                         

                                 
 


                           
 
                                
                                    
     
                                                                
         






                                                     

         
                                
 














                                                                        
         
                                            
                            

                            
 
                                                 
         
                                                                                 
                                                                                      
             
                                                      
                           
 
                           
                                                                       
 








                                                 







                                             



                                           
                               
                 











                                                               
                 
 



                                                             
 



                                                                               
 
                                

                 
         
 

                                       
 



                                                     

                                                         
 
                                                       
                                                     


                                                
                            
                                            

                                
 




                                                                       
             

                                                   
 

                                                             
                 



                                                                             
                 
 



                                                                       
 
                                                               
                                                         
             
                                               
                                                    
 


                                                                               






                                                                        





                                                    
 
                           
         
 






                                                 
 





                                                       
     
 

                                    

 
                                                
 


                                     
     







                                          
     
 




                                                               
                                   



                   
/*
 *  The Mana Client
 *  Copyright (C) 2004-2009  The Mana World Development Team
 *  Copyright (C) 2009-2012  The Mana Developers
 *  Copyright (C) 2009  Aethyra Development Team
 *
 *  This file is part of The Mana Client.
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#include "gui/widgets/browserbox.h"

#include "gui/gui.h"
#include "gui/truetypefont.h"
#include "gui/widgets/linkhandler.h"

#include "resources/itemdb.h"
#include "resources/iteminfo.h"
#include "resources/theme.h"

#include "utils/stringutils.h"

#include <guichan/graphics.hpp>
#include <guichan/font.hpp>
#include <guichan/cliprectangle.hpp>

#include <algorithm>

struct LayoutContext
{
    LayoutContext(gcn::Font *font);

    int y = 0;
    gcn::Font *font;
    const int fontHeight;
    const int minusWidth;
    const int tildeWidth;
    int lineHeight;
    gcn::Color selColor;
    const gcn::Color textColor;
};

LayoutContext::LayoutContext(gcn::Font *font)
    : font(font)
    , fontHeight(font->getHeight())
    , minusWidth(font->getWidth("-"))
    , tildeWidth(font->getWidth("~"))
    , lineHeight(fontHeight)
    , selColor(Theme::getThemeColor(Theme::TEXT))
    , textColor(Theme::getThemeColor(Theme::TEXT))
{
    if (auto *trueTypeFont = dynamic_cast<const TrueTypeFont*>(font))
        lineHeight = trueTypeFont->getLineHeight();
}


BrowserBox::BrowserBox(Mode mode):
    mMode(mode)
{
    setFocusable(true);
    addMouseListener(this);
}

BrowserBox::~BrowserBox() = default;

void BrowserBox::addRow(const std::string &row)
{
    TextRow &newRow = mTextRows.emplace_back();

    // Use links and user defined colors
    if (mUseLinksAndUserColors)
    {
        std::string tmp = row;

        // Check for links in format "@@link|Caption@@"
        auto idx1 = tmp.find("@@");
        while (idx1 != std::string::npos)
        {
            const auto idx2 = tmp.find("|", idx1);
            const auto idx3 = tmp.find("@@", idx2);

            if (idx2 == std::string::npos || idx3 == std::string::npos)
                break;

            BrowserLink &link = newRow.links.emplace_back();
            link.link = tmp.substr(idx1 + 2, idx2 - (idx1 + 2));
            link.caption = tmp.substr(idx2 + 1, idx3 - (idx2 + 1));

            if (link.caption.empty())
            {
                const int id = atoi(link.link.c_str());
                if (id)
                    link.caption = itemDb->get(id).name;
                else
                    link.caption = link.link;
            }

            newRow.text += tmp.substr(0, idx1);
            newRow.text += "##<" + link.caption;

            tmp.erase(0, idx3 + 2);
            if (!tmp.empty())
            {
                newRow.text += "##>";
            }
            idx1 = tmp.find("@@");
        }

        newRow.text += tmp;
    }
    // Don't use links and user defined colors
    else
    {
        newRow.text = row;
    }

    // Layout the newly added row
    LayoutContext context(getFont());
    context.y = getHeight();
    layoutTextRow(newRow, context);

    // Auto size mode
    if (mMode == AUTO_SIZE && newRow.width > getWidth())
        setWidth(newRow.width);

    // Discard older rows when a row limit has been set
    // (this might invalidate the newRow reference)
    int removedHeight = 0;
    while (mMaxRows > 0 && mTextRows.size() > mMaxRows)
    {
        removedHeight += mTextRows.front().height;
        mTextRows.pop_front();
    }
    if (removedHeight > 0)
    {
        for (auto &row : mTextRows)
        {
            for (auto &part : row.parts)
                part.y -= removedHeight;

            for (auto &link : row.links)
                link.rect.y -= removedHeight;
        }
    }

    setHeight(context.y - removedHeight);
}

void BrowserBox::clearRows()
{
    mTextRows.clear();
    setSize(0, 0);
    mHoveredLink.reset();
    maybeRelayoutText();
}

void BrowserBox::mousePressed(gcn::MouseEvent &event)
{
    if (!mLinkHandler)
        return;

    updateHoveredLink(event.getX(), event.getY());

    if (mHoveredLink) {
        mLinkHandler->handleLink(mHoveredLink->link);
        gui->setCursorType(Cursor::POINTER);
    }
}

void BrowserBox::mouseMoved(gcn::MouseEvent &event)
{
    updateHoveredLink(event.getX(), event.getY());
    gui->setCursorType(mHoveredLink ? Cursor::HAND : Cursor::POINTER);
    event.consume();        // Suppress mouse cursor change by parent
}

void BrowserBox::mouseExited(gcn::MouseEvent &event)
{
    mHoveredLink.reset();
}

void BrowserBox::draw(gcn::Graphics *graphics)
{
    const gcn::ClipRectangle &cr = graphics->getCurrentClipArea();
    int yStart = cr.y - cr.yOffset;
    int yEnd = yStart + cr.height;
    if (yStart < 0)
        yStart = 0;

    if (getWidth() != mLastLayoutWidth)
        maybeRelayoutText();

    if (mHoveredLink)
    {
        auto &link = *mHoveredLink;

        const gcn::Rectangle &rect = link.rect;

        if (mHighlightMode & BACKGROUND)
        {
            graphics->setColor(Theme::getThemeColor(Theme::HIGHLIGHT));
            graphics->fillRectangle(rect);
        }

        if (mHighlightMode & UNDERLINE)
        {
            graphics->setColor(Theme::getThemeColor(Theme::HYPERLINK));
            graphics->drawLine(rect.x,
                               rect.y + rect.height,
                               rect.x + rect.width,
                               rect.y + rect.height);
        }
    }

    for (const auto &row : mTextRows)
    {
        for (const auto &part : row.parts)
        {
            if (part.y + 50 < yStart)
                continue;
            if (part.y > yEnd)
                return;

            auto font = part.font;

            // Handle text shadows
            if (mShadows)
            {
                graphics->setColor(Theme::getThemeColor(Theme::SHADOW,
                                                        part.color.a / 2));

                if (mOutline)
                    font->drawString(graphics, part.text, part.x + 2, part.y + 2);
                else
                    font->drawString(graphics, part.text, part.x + 1, part.y + 1);
            }

            if (mOutline)
            {
                // Text outline
                graphics->setColor(Theme::getThemeColor(Theme::OUTLINE,
                                                        part.color.a / 4));
                font->drawString(graphics, part.text, part.x + 1, part.y);
                font->drawString(graphics, part.text, part.x - 1, part.y);
                font->drawString(graphics, part.text, part.x, part.y + 1);
                font->drawString(graphics, part.text, part.x, part.y - 1);
            }

            // the main text
            graphics->setColor(part.color);
            font->drawString(graphics, part.text, part.x, part.y);
        }
    }
}

/**
 * Relayouts all text rows and returns the new height of the BrowserBox.
 */
void BrowserBox::relayoutText()
{
    LayoutContext context(getFont());

    for (auto &row : mTextRows)
        layoutTextRow(row, context);

    mLastLayoutWidth = getWidth();
    mLayoutTimer.set(100);
    setHeight(context.y);
}

/**
 * Layers out the given \a row of text starting at the given \a context position.
 * @return the context position for the next row.
 */
void BrowserBox::layoutTextRow(TextRow &row, LayoutContext &context)
{
    // each line starts with normal font in default color
    context.font = getFont();
    context.selColor = context.textColor;

    const int startY = context.y;
    row.parts.clear();

    unsigned linkIndex = 0;
    bool wrapped = false;
    int x = 0;

    // Check for separator lines
    if (startsWith(row.text, "---"))
    {
        for (x = 0; x < getWidth(); x += context.minusWidth - 1)
        {
            row.parts.push_back(LinePart {
                                    x,
                                    context.y,
                                    context.selColor,
                                    "-",
                                    context.font
                                });
        }

        context.y += row.height;

        row.width = getWidth();
        row.height = context.y - startY;
        return;
    }

    gcn::Color prevColor = context.selColor;

    // TODO: Check if we must take texture size limits into account here
    // TODO: Check if some of the O(n) calls can be removed
    for (std::string::size_type start = 0, end = std::string::npos;
            start != std::string::npos;
            start = end, end = std::string::npos)
    {
        // Wrapped line continuation shall be indented
        if (wrapped)
        {
            context.y += context.lineHeight;
            x = mWrapIndent;
            wrapped = false;
        }

        if (mUseLinksAndUserColors || start == 0)
        {
            // Check for color or font change in format "##x", x = [<,>,B,p,0..9]
            while (row.text.size() > start + 2 && row.text.find("##", start) == start)
            {
                const char c = row.text.at(start + 2);
                start += 3;

                bool valid;
                const gcn::Color &col = Theme::getThemeColor(c, valid);

                if (c == '>')
                {
                    context.selColor = prevColor;
                }
                else if (c == '<')
                {
                    prevColor = context.selColor;
                    context.selColor = col;
                }
                else if (c == 'B')
                {
                    context.font = boldFont;
                }
                else if (c == 'b')
                {
                    context.font = getFont();
                }
                else if (valid)
                {
                    context.selColor = col;
                }
                else switch (c)
                {
                    case '1': context.selColor = RED; break;
                    case '2': context.selColor = GREEN; break;
                    case '3': context.selColor = BLUE; break;
                    case '4': context.selColor = ORANGE; break;
                    case '5': context.selColor = YELLOW; break;
                    case '6': context.selColor = PINK; break;
                    case '7': context.selColor = PURPLE; break;
                    case '8': context.selColor = GRAY; break;
                    case '9': context.selColor = BROWN; break;
                    case '0':
                    default:
                        context.selColor = context.textColor;
                }

                // Update the position of the links
                if (c == '<' && linkIndex < row.links.size())
                {
                    auto &link = row.links[linkIndex];

                    link.rect.x = x;
                    link.rect.y = context.y;
                    link.rect.width = context.font->getWidth(link.caption) + 1;
                    link.rect.height = context.fontHeight - 1;

                    linkIndex++;
                }
            }
        }

        if (start >= row.text.length())
            break;

        // "Tokenize" the string at control sequences
        if (mUseLinksAndUserColors)
            end = row.text.find("##", start + 1);

        std::string::size_type len =
            end == std::string::npos ? end : end - start;

        std::string part = row.text.substr(start, len);
        int partWidth = context.font->getWidth(part);

        // Auto wrap mode
        if (mMode == AUTO_WRAP && getWidth() > 0
            && partWidth > 0
            && (x + partWidth) > getWidth())
        {
            bool forced = false;

            /* FIXME: This code layout makes it easy to crash remote
               clients by talking garbage. Forged long utf-8 characters
               will cause either a buffer underflow in substr or an
               infinite loop in the main loop. */
            do
            {
                if (!forced)
                    end = row.text.rfind(' ', end);

                // Check if we have to (stupidly) force-wrap
                if (end == std::string::npos || end <= start)
                {
                    forced = true;
                    end = row.text.size();
                    x += context.tildeWidth; // Account for the wrap-notifier
                    continue;
                }

                // Skip to the start of the current character
                while ((row.text[end] & 192) == 128)
                    end--;
                end--; // And then to the last byte of the previous one

                part = row.text.substr(start, end - start + 1);
                partWidth = context.font->getWidth(part);
            }
            while (end > start && partWidth > 0
                   && (x + partWidth) > getWidth());

            if (forced)
            {
                x -= context.tildeWidth; // Remove the wrap-notifier accounting
                row.parts.push_back(LinePart {
                                        getWidth() - context.tildeWidth,
                                        context.y,
                                        context.selColor,
                                        "~",
                                        getFont()
                                    });
                end++; // Skip to the next character
            }
            else
            {
                end += 2; // Skip to after the space
            }

            wrapped = true;
        }

        row.parts.push_back(LinePart {
                                x,
                                context.y,
                                context.selColor,
                                std::move(part),
                                context.font
                            });

        row.width = std::max(row.width, x + partWidth);

        if (mMode == AUTO_WRAP && partWidth == 0)
            break;

        x += partWidth;
    }

    context.y += context.lineHeight;
    row.height = context.y - startY;
}

void BrowserBox::updateHoveredLink(int x, int y)
{
    mHoveredLink.reset();

    for (const auto &row : mTextRows)
    {
        for (const auto &link : row.links)
        {
            if (link.contains(x, y))
            {
                mHoveredLink = link;
                return;
            }
        }
    }
}

void BrowserBox::maybeRelayoutText()
{
    // Reduce relayouting frequency when there is a lot of text
    if (mTextRows.size() > 100)
        if (!mLayoutTimer.passed())
            return;

    relayoutText();
}