sdlterm/src/sdlterm.cc
author Radek Brich <radek.brich@devl.cz>
Wed, 30 Jan 2013 20:21:08 +0100
changeset 73 85a282b5e4fc
parent 72 6e0656600754
permissions -rw-r--r--
Add mousehover event (only SDL).

#include "sdlterm.h"

#include <algorithm>


void ColorMap::index_to_rgb(int index, SDL_Color &color) const
{
    color.r = _map[index][0];
    color.g = _map[index][1];
    color.b = _map[index][2];
}


SDL_Surface *GlyphCache::lookup_glyph(Uint64 id)
{
    auto iter = _glyph_map.find(id);
    if (iter == _glyph_map.end())
    {
        return NULL;
    }
    return iter->second;
}


void GlyphCache::put_glyph(Uint64 id, SDL_Surface *srf)
{
    _glyph_map[id] = srf;
}


void GlyphCache::flush()
{
    for (auto iter = _glyph_map.begin(); iter != _glyph_map.end(); iter++)
    {
        SDL_FreeSurface(iter->second);
    }
    _glyph_map.clear();
}


GlyphRenderer::GlyphRenderer()
 : _font_regular(NULL), _font_bold(NULL), _cell_width(0), _cell_height(0), _cache(), _colormap()
{
    if (TTF_Init() == -1)
    {
        throw SDLTermError(std::string("TTF_Init: ") + TTF_GetError());
    }
}


GlyphRenderer::~GlyphRenderer()
{
    _cache.flush();
    close_font();
    TTF_Quit();
}


void GlyphRenderer::open_font(const char *fname_regular, const char *fname_bold, int ptsize)
{
    close_font();

    // open regular font
    _font_regular = TTF_OpenFont(fname_regular, ptsize);
    if (!_font_regular)
    {
        throw SDLTermError(std::string("TTF_OpenFont: ") + TTF_GetError());
    }

    // open bold font
    _font_bold = TTF_OpenFont(fname_bold, ptsize);
    if (!_font_bold)
    {
        throw SDLTermError(std::string("TTF_OpenFont: ") + TTF_GetError());
    }

    // update metrics for regular font
    int advance;
    if (TTF_GlyphMetrics(_font_regular, 'M', NULL, NULL, NULL, NULL, &advance) == -1)
    {
        throw SDLTermError(std::string("TTF_GlyphMetrics: ") + TTF_GetError());
    }
    _cell_width = advance;
    _cell_height = TTF_FontHeight(_font_regular);

    // read metrics for bold font
    if (TTF_GlyphMetrics(_font_bold, 'M', NULL, NULL, NULL, NULL, &advance) == -1)
    {
        throw SDLTermError(std::string("TTF_GlyphMetrics: ") + TTF_GetError());
    }
    if (advance > _cell_width)
    {
        _cell_width = advance;
    }
    int height = TTF_FontHeight(_font_bold);
    if (height > _cell_height)
    {
        _cell_height = height;
    }
}


void GlyphRenderer::close_font()
{
    if (_font_regular)
    {
        TTF_CloseFont(_font_regular);
        _font_regular = NULL;
    }
    if (_font_bold)
    {
        TTF_CloseFont(_font_bold);
        _font_bold = NULL;
    }
}


SDL_Surface *GlyphRenderer::render_cell(Uint32 ch, Uint32 attr, bool blink_state)
{
    SDL_Surface *cell_surface;
    TTF_Font *font;
    SDL_Color fgcolor, bgcolor;

    // blink affects cache lookup, must be processed first
    if ((attr & Style::BLINK) && !blink_state)
    {
        ch = ' ';
    }

    // try cache
    Uint64 id = (Uint64)ch | (Uint64)attr << 32;
    cell_surface = _cache.lookup_glyph(id);
    if (cell_surface)
    {
        return cell_surface;
    }

    // load attributes
    _colormap.index_to_rgb((attr & 0x000000FF), fgcolor);
    _colormap.index_to_rgb((attr & 0x0000FF00) >> 8, bgcolor);
    font = (attr & Style::BOLD) ? _font_bold : _font_regular;
    if (attr & Style::STANDOUT)
    {
        std::swap(fgcolor, bgcolor);
    }

    // create surface for whole cell and fill it with bg color
    cell_surface = SDL_CreateRGBSurface(SDL_SWSURFACE,
            _cell_width, _cell_height, 32, 0, 0, 0, 0);
    SDL_Rect dst_rect;
    dst_rect.x = 0;
    dst_rect.y = 0;
    dst_rect.w = _cell_width;
    dst_rect.h = _cell_height;
    Uint32 bgcolor_mapped = SDL_MapRGB(cell_surface->format, bgcolor.r, bgcolor.g, bgcolor.b);
    SDL_FillRect(cell_surface, &dst_rect, bgcolor_mapped);

    // render glyph, blit it onto cell surface
    if (ch)
    {
        // when glyph is not provided by BOLD font but is provided by REGULAR font, use that (better than nothing)
        if ((attr & Style::BOLD) && !TTF_GlyphIsProvided(font, ch) && TTF_GlyphIsProvided(_font_regular, ch))
        {
            // use bold style of regular font instead of bold font
            TTF_SetFontStyle(_font_regular, TTF_STYLE_BOLD);
            _render_glyph(cell_surface, _font_regular, ch, fgcolor, bgcolor);
            TTF_SetFontStyle(_font_regular, TTF_STYLE_NORMAL);
        }
        else
        {
            // normal case
            _render_glyph(cell_surface, font, ch, fgcolor, bgcolor);
        }
        if (attr & Style::UNDERLINE)
        {
            // draw underline
            SDL_LockSurface(cell_surface);
            int y = 1 + TTF_FontAscent(font);
            Uint32 fgcolor_mapped = SDL_MapRGB(cell_surface->format, fgcolor.r, fgcolor.g, fgcolor.b);
            Uint32 *p = (Uint32 *)((Uint8 *)cell_surface->pixels + y * cell_surface->pitch);
            for (int x = 0; x < _cell_width; x++)
                *p++ = fgcolor_mapped;
            SDL_UnlockSurface(cell_surface);
        }
    }

    // convert to display format
    SDL_Surface *tmp_surface = cell_surface;
    cell_surface = SDL_DisplayFormat(tmp_surface);
    SDL_FreeSurface(tmp_surface);

    // put to cache
    _cache.put_glyph(id, cell_surface);

    return cell_surface;
}


void GlyphRenderer::_render_glyph(SDL_Surface *cell_surface, TTF_Font *font, Uint32 ch,
        SDL_Color fgcolor, SDL_Color bgcolor)
{
    int minx, maxy;
    SDL_Rect dst_rect;
    SDL_Surface *glyph_surface = TTF_RenderGlyph_Shaded(font, ch, fgcolor, bgcolor);
    TTF_GlyphMetrics(font, ch, &minx, NULL, NULL, &maxy, NULL);
    dst_rect.x = minx;
    dst_rect.y = TTF_FontAscent(font) - maxy;
    SDL_BlitSurface(glyph_surface, NULL, cell_surface, &dst_rect);
    SDL_FreeSurface(glyph_surface);
}


void TerminalScreen::select_font(const char *fname_regular, const char *fname_bold, int ptsize)
{
    _render.open_font(fname_regular, fname_bold, ptsize);
    _reset_cells();
}

void TerminalScreen::resize(int pxwidth, int pxheight)
{
    _screen_surface = SDL_SetVideoMode(pxwidth, pxheight, 0, SDL_SWSURFACE|SDL_ANYFORMAT|SDL_RESIZABLE);

    if (_screen_surface == NULL)
    {
        throw SDLTermError(std::string("SDL_SetVideoMode: ") + SDL_GetError());
    }

    _pixel_width = pxwidth;
    _pixel_height = pxheight;

    _reset_cells();
}


void TerminalScreen::erase()
{
    std::fill(_cells_front.begin(), _cells_front.end(), TerminalCell());
}


void TerminalScreen::putch(int x, int y, Uint32 ch, Uint32 attr)
{
    TerminalCell &cell = _cells_front[y * _width + x];
    cell.ch = ch;
    cell.attr = attr;
}


void TerminalScreen::toggle_cursor(int x, int y)
{
    TerminalCell &cell = _cells_front[y * _width + x];
    cell.attr ^= Style::STANDOUT;
}


void TerminalScreen::commit()
{
    auto front_iter = _cells_front.begin();
    auto back_iter = _cells_back.begin();
    SDL_Surface *cell_surface;
    SDL_Rect dst_rect;
    for (int y = 0; y < _height; y++)
    {
        for (int x = 0; x < _width; x++)
        {
            if (*front_iter != *back_iter)
            {
                dst_rect.x = x * _cell_width;
                dst_rect.y = y * _cell_height;
                cell_surface = _render.render_cell(front_iter->ch, front_iter->attr, _blink_state);
                SDL_BlitSurface(cell_surface, NULL, _screen_surface, &dst_rect);
                *back_iter = *front_iter;
            }
            front_iter++;
            back_iter++;
        }
    }

    SDL_UpdateRect(_screen_surface, 0, 0, 0, 0);
}


void TerminalScreen::redraw()
{
    // clear back buffer, current screen is considered blank
    std::fill(_cells_back.begin(), _cells_back.end(), TerminalCell());
}


void TerminalScreen::_reset_cells()
{
    _cell_width = _render.get_cell_width();
    _cell_height = _render.get_cell_height();
    if (!_cell_width || !_cell_height)
        return;

    _width = _pixel_width / _cell_width;
    _height = _pixel_height / _cell_height;
    if (!_width || !_height)
        return;

    int num_cells = _width * _height;
    _cells_front.resize(num_cells, TerminalCell());
    _cells_back.resize(num_cells);
    redraw();
}


void TerminalScreen::_draw_blink()
{
    // Use back buffer which contains commited changes.
    // This is called from timer while application may draw into front_buffer.
    auto back_iter = _cells_back.begin();
    SDL_Surface *cell_surface;
    SDL_Rect dst_rect;
    for (int y = 0; y < _height; y++)
    {
        for (int x = 0; x < _width; x++)
        {
            // draw only blinking characters
            if (back_iter->attr & Style::BLINK)
            {
                dst_rect.x = x * _cell_width;
                dst_rect.y = y * _cell_height;
                cell_surface = _render.render_cell(back_iter->ch, back_iter->attr, _blink_state);
                SDL_BlitSurface(cell_surface, NULL, _screen_surface, &dst_rect);
            }
            back_iter++;
        }
    }

    SDL_UpdateRect(_screen_surface, 0, 0, 0, 0);
}


Terminal::Terminal()
 : _screen(), _attr(7), _cursor_x(0), _cursor_y(0), _cursor_visible(false),
   _mousemove_last_x(-1), _mousemove_last_y(-1)
{
    if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER) == -1)
    {
        throw SDLTermError(std::string("SDL_Init: ") + SDL_GetError());
    }
    SDL_EnableUNICODE(1);
    SDL_EnableKeyRepeat(250, SDL_DEFAULT_REPEAT_INTERVAL);
    SDL_WM_SetCaption("terminal", NULL);
    SDL_AddTimer(500, _blink_toggle_callback, NULL);
}


Terminal::~Terminal()
{
    SDL_Quit();
}


void Terminal::commit()
{
    if (_cursor_visible)
    {
        _screen.toggle_cursor(_cursor_x, _cursor_y);
        _screen.commit();
        _screen.toggle_cursor(_cursor_x, _cursor_y);
    }
    else
    {
        _screen.commit();
    }
}


bool Terminal::wait_event(Event &event, Uint32 timeout)
{
    SDL_Event sdl_event;
    bool translated;

    // any events pending?
    while (SDL_PollEvent(&sdl_event))
    {
        translated = _handle_event(sdl_event, event);
        if (translated)
            return true;
    }

    // use timer to simulate SDL_WaitEventTimeout, which is not available in SDL 1.2
    SDL_TimerID timer_id = NULL;
    if (timeout)
    {
        timer_id = SDL_AddTimer(timeout, _wait_event_callback, NULL);
    }

    // loop until we have something to return
    bool event_ready = false;
    while (!event_ready && SDL_WaitEvent(&sdl_event))
    {
        do
        {
            translated = _handle_event(sdl_event, event);
            if (translated)
            {
                event_ready = true;
                break;
            }
            // timeout?
            if (sdl_event.type == SDL_USEREVENT && sdl_event.user.code == 1)
            {
                SDL_RemoveTimer(timer_id);
                return false;
            }
        }
        while (!event_ready && SDL_PollEvent(&sdl_event));
    }

    // remove timer when other event came before timeout
    if (timeout)
    {
        SDL_RemoveTimer(timer_id);
    }

    // ok or error?
    if (event_ready)
    {
        return true;
    }
    else
    {
        throw SDLTermError(std::string("SDL_WaitEvent: ") + SDL_GetError());
    }
}


// return true when SDL_Event was translated to our Event
bool Terminal::_handle_event(const SDL_Event &sdl_event, Event &event)
{
    switch (sdl_event.type)
    {
        case SDL_USEREVENT:
            // toggle blink
            if (sdl_event.user.code == 2)
            {
                _screen.toggle_blink();
            }
            return false;

        case SDL_QUIT:
            event.type = Event::QUIT;
            return true;

        case SDL_VIDEORESIZE:
            event.type = Event::RESIZE;
            _screen.resize(sdl_event.resize.w, sdl_event.resize.h);
            return true;

        case SDL_VIDEOEXPOSE:
            _screen.redraw();
            return false;

        case SDL_KEYDOWN:
        {
            //switch(event.key.keysym.sym)
            event.type = Event::KEYPRESS;
            const char *keyname = _translate_keyname(sdl_event.key.keysym.sym);
            // return only keyname or unicode, never both
            if (keyname)
            {
                strncpy(event.key.keyname, keyname, 10);
                event.key.unicode = 0;
            }
            else
            {
                event.key.keyname[0] = 0;
                event.key.unicode = sdl_event.key.keysym.unicode;
                if (!event.key.unicode)
                {
                    // unknown key
                    return false;
                }
            }
            event.key.mod = _translate_mod(sdl_event.key.keysym.mod);
            return true;
        }

        case SDL_MOUSEBUTTONDOWN:
        case SDL_MOUSEBUTTONUP:
            event.type = (sdl_event.type == SDL_MOUSEBUTTONDOWN) ? Event::MOUSEDOWN : Event::MOUSEUP;
            event.mouse.x = sdl_event.button.x / _screen.get_cell_width();
            event.mouse.y = sdl_event.button.y / _screen.get_cell_height();
            event.mouse.button = sdl_event.button.button;
            if (sdl_event.button.button == SDL_BUTTON_WHEELUP || sdl_event.button.button == SDL_BUTTON_WHEELDOWN)
            {
                if (sdl_event.type == SDL_MOUSEBUTTONUP)
                {
                    // do not report button-up events for mouse wheel
                    return false;
                }
                event.type = Event::MOUSEWHEEL;
            }
            _mousemove_last_x = event.mouse.x;
            _mousemove_last_y = event.mouse.y;
            return true;

        case SDL_MOUSEMOTION:
            event.mouse.x = sdl_event.motion.x / _screen.get_cell_width();
            event.mouse.y = sdl_event.motion.y / _screen.get_cell_height();
            if (_mousemove_last_x == event.mouse.x &&
                _mousemove_last_y == event.mouse.y)
            {
                // mouse position did not change
                return false;
            }
            if (sdl_event.motion.state == 0 || _mousemove_last_x == -1)
            {
                // no button is pressed or last pos not initialized
                event.type = Event::MOUSEHOVER;
            }
            else
            {
                // some button pressed
                event.type = Event::MOUSEMOVE;
                event.mouse.relx = event.mouse.x - _mousemove_last_x;
                event.mouse.rely = event.mouse.y - _mousemove_last_y;
            }
            _mousemove_last_x = event.mouse.x;
            _mousemove_last_y = event.mouse.y;
            return true;

        default:
            // unknown event
            return false;
    }
}


const char *Terminal::_translate_keyname(SDLKey sym)
{
    switch (sym)
    {
        case SDLK_BACKSPACE:    return "backspace";
        case SDLK_TAB:          return "tab";
        case SDLK_RETURN:       return "enter";
        case SDLK_KP_ENTER:     return "enter";
        case SDLK_ESCAPE:       return "escape";
        case SDLK_DELETE:       return "delete";
        case SDLK_INSERT:       return "insert";
        case SDLK_UP:           return "up";
        case SDLK_DOWN:         return "down";
        case SDLK_LEFT:         return "left";
        case SDLK_RIGHT:        return "right";
        case SDLK_HOME:         return "home";
        case SDLK_END:          return "end";
        case SDLK_PAGEUP:       return "pageup";
        case SDLK_PAGEDOWN:     return "pagedown";
        case SDLK_F1:           return "f1";
        case SDLK_F2:           return "f2";
        case SDLK_F3:           return "f3";
        case SDLK_F4:           return "f4";
        case SDLK_F5:           return "f5";
        case SDLK_F6:           return "f6";
        case SDLK_F7:           return "f7";
        case SDLK_F8:           return "f8";
        case SDLK_F9:           return "f9";
        case SDLK_F10:          return "f10";
        case SDLK_F11:          return "f11";
        case SDLK_F12:          return "f12";
        case SDLK_PRINT:        return "print";
        case SDLK_SCROLLOCK:    return "scrllock";
        case SDLK_PAUSE:        return "pause";
        default: return NULL;
    }
}


int Terminal::_translate_mod(SDLMod mod)
{
    int res = 0;
    if (mod & KMOD_SHIFT)   res |= KeyMod::SHIFT;
    if (mod & KMOD_ALT)     res |= KeyMod::ALT;
    if (mod & KMOD_CTRL)    res |= KeyMod::CTRL;
    if (mod & KMOD_META)    res |= KeyMod::META;
    return res;
}


Uint32 Terminal::_wait_event_callback(Uint32 interval, void *param)
{
    SDL_Event event;
    event.type = SDL_USEREVENT;
    event.user.code = 1;
    SDL_PushEvent(&event);
    return 0;
}


Uint32 Terminal::_blink_toggle_callback(Uint32 interval, void *param)
{
    SDL_Event event;
    event.type = SDL_USEREVENT;
    event.user.code = 2;
    SDL_PushEvent(&event);
    return interval;
}