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;
}