d197ba42c3
Commitd355ef4030
removed a seemingly unnecessary modification of the global variable grd_curcanv->cv_font, after eliminating all local reads of it. However, a non-local read, buried in listbox_mouse, depended on grd_curcanv->cv_font being set to a MEDIUM font. After that commit, grd_curcanv->cv_font retained its prior value, which is not a MEDIUM font. This caused listbox_mouse to compute an incorrect height of the lines in the listbox, which manifested as the game choosing the wrong line when the mouse is clicked in the listbox. Fix the problem by explicitly using MEDIUM3_FONT, since that was usually the value left in grd_curcanv->cv_font prior to that commit. In some cases, a different MEDIUM font would be left there, but all the MEDIUM fonts have the same height, so they are interchangeable for this purpose. Reported-by: Q3BFG10K <https://github.com/dxx-rebirth/dxx-rebirth/issues/498> Fixes:d355ef4030
("Pass font to various drawing functions")
1209 lines
32 KiB
C++
1209 lines
32 KiB
C++
/*
|
|
* Portions of this file are copyright Rebirth contributors and licensed as
|
|
* described in COPYING.txt.
|
|
* Portions of this file are copyright Parallax Software and licensed
|
|
* according to the Parallax license below.
|
|
* See COPYING.txt for license details.
|
|
|
|
THE COMPUTER CODE CONTAINED HEREIN IS THE SOLE PROPERTY OF PARALLAX
|
|
SOFTWARE CORPORATION ("PARALLAX"). PARALLAX, IN DISTRIBUTING THE CODE TO
|
|
END-USERS, AND SUBJECT TO ALL OF THE TERMS AND CONDITIONS HEREIN, GRANTS A
|
|
ROYALTY-FREE, PERPETUAL LICENSE TO SUCH END-USERS FOR USE BY SUCH END-USERS
|
|
IN USING, DISPLAYING, AND CREATING DERIVATIVE WORKS THEREOF, SO LONG AS
|
|
SUCH USE, DISPLAY OR CREATION IS FOR NON-COMMERCIAL, ROYALTY OR REVENUE
|
|
FREE PURPOSES. IN NO EVENT SHALL THE END-USER USE THE COMPUTER CODE
|
|
CONTAINED HEREIN FOR REVENUE-BEARING PURPOSES. THE END-USER UNDERSTANDS
|
|
AND AGREES TO THE TERMS HEREIN AND ACCEPTS THE SAME BY USE OF THIS FILE.
|
|
COPYRIGHT 1993-1999 PARALLAX SOFTWARE CORPORATION. ALL RIGHTS RESERVED.
|
|
*/
|
|
|
|
/*
|
|
*
|
|
* Graphical routines for drawing fonts.
|
|
*
|
|
*/
|
|
|
|
#include <algorithm>
|
|
#include <memory>
|
|
#include <stdexcept>
|
|
#include <stdarg.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#ifndef macintosh
|
|
#include <fcntl.h>
|
|
#endif
|
|
|
|
#include "u_mem.h"
|
|
#include "gr.h"
|
|
#include "grdef.h"
|
|
#include "dxxerror.h"
|
|
#include "common/2d/bitmap.h"
|
|
#include "makesig.h"
|
|
#include "physfsx.h"
|
|
#include "gamefont.h"
|
|
#include "byteutil.h"
|
|
#include "console.h"
|
|
#include "config.h"
|
|
#if DXX_USE_OGL
|
|
#include "ogl_init.h"
|
|
#endif
|
|
|
|
#include "compiler-array.h"
|
|
#include "compiler-range_for.h"
|
|
#include "compiler-make_unique.h"
|
|
#include "partial_range.h"
|
|
|
|
static font_x_scale_float FONTSCALE_X()
|
|
{
|
|
return FNTScaleX.operator float();
|
|
}
|
|
|
|
static auto FONTSCALE_Y(const int &y)
|
|
{
|
|
return font_scaled_float<'y'>(FNTScaleY * y);
|
|
}
|
|
|
|
#define MAX_OPEN_FONTS 50
|
|
|
|
namespace {
|
|
|
|
constexpr std::integral_constant<uint8_t, 255> kerndata_terminator{};
|
|
|
|
}
|
|
|
|
namespace dcx {
|
|
|
|
//list of open fonts, for use (for now) for palette remapping
|
|
static array<grs_font *, MAX_OPEN_FONTS> open_font;
|
|
|
|
#define BITS_TO_BYTES(x) (((x)+7)>>3)
|
|
|
|
static int gr_internal_string_clipped(grs_canvas &, const grs_font &cv_font, int x, int y, const char *s);
|
|
static int gr_internal_string_clipped_m(grs_canvas &, const grs_font &cv_font, int x, int y, const char *s);
|
|
|
|
static const uint8_t *find_kern_entry(const grs_font &font, const uint8_t first, const uint8_t second)
|
|
{
|
|
auto p = font.ft_kerndata;
|
|
|
|
while (*p != kerndata_terminator)
|
|
if (p[0]==first && p[1]==second)
|
|
return p;
|
|
else p+=3;
|
|
return NULL;
|
|
}
|
|
|
|
//takes the character AFTER being offset into font
|
|
|
|
namespace {
|
|
|
|
class font_character_extent
|
|
{
|
|
const unsigned r;
|
|
public:
|
|
font_character_extent(const grs_font &cv_font) :
|
|
r(cv_font.ft_maxchar - cv_font.ft_minchar)
|
|
{
|
|
}
|
|
bool operator()(const unsigned c) const
|
|
{
|
|
return c <= r;
|
|
}
|
|
};
|
|
|
|
template <typename T>
|
|
struct get_char_width_result
|
|
{
|
|
T width, spacing;
|
|
};
|
|
|
|
/* Floating form never uses width. This specialization allows the
|
|
* compiler to recognize width as dead, shortening
|
|
* get_char_width<float>.
|
|
*/
|
|
template <>
|
|
struct get_char_width_result<float>
|
|
{
|
|
float spacing;
|
|
get_char_width_result(float, float s) :
|
|
spacing(s)
|
|
{
|
|
}
|
|
};
|
|
|
|
}
|
|
|
|
//takes the character BEFORE being offset into current font
|
|
template <typename T>
|
|
static get_char_width_result<T> get_char_width(const grs_font &cv_font, const uint8_t c, const uint8_t c2)
|
|
{
|
|
const unsigned letter = c - cv_font.ft_minchar;
|
|
const auto ft_flags = cv_font.ft_flags;
|
|
const auto proportional = ft_flags & FT_PROPORTIONAL;
|
|
|
|
const auto &&fontscale_x = FONTSCALE_X();
|
|
const auto &&INFONT = font_character_extent(cv_font);
|
|
if (!INFONT(letter)) { //not in font, draw as space
|
|
return {0, static_cast<T>(proportional ? fontscale_x(cv_font.ft_w) / 2 : cv_font.ft_w)};
|
|
}
|
|
const T width = proportional ? fontscale_x(cv_font.ft_widths[letter]).operator float() : cv_font.ft_w;
|
|
if (ft_flags & FT_KERNED)
|
|
{
|
|
if (!(c2==0 || c2=='\n')) {
|
|
const unsigned letter2 = c2 - cv_font.ft_minchar;
|
|
|
|
if (INFONT(letter2)) {
|
|
const auto p = find_kern_entry(cv_font, letter, letter2);
|
|
if (p)
|
|
return {width, static_cast<T>(fontscale_x(p[2]))};
|
|
}
|
|
}
|
|
}
|
|
return {width, width};
|
|
}
|
|
|
|
static int get_centered_x(const grs_canvas &canvas, const grs_font &cv_font, const char *s)
|
|
{
|
|
float w;
|
|
for (w = 0.; const char c = *s; ++s)
|
|
{
|
|
if (c == '\n')
|
|
break;
|
|
if (c <= 0x06)
|
|
{
|
|
if (c <= 0x03)
|
|
s++;
|
|
continue;//skip color codes.
|
|
}
|
|
w += get_char_width<float>(cv_font, c, s[1]).spacing;
|
|
}
|
|
|
|
return (canvas.cv_bitmap.bm_w - w) / 2;
|
|
}
|
|
|
|
//hack to allow color codes to be embedded in strings -MPM
|
|
//note we subtract one from color, since 255 is "transparent" so it'll never be used, and 0 would otherwise end the string.
|
|
//function must already have orig_color var set (or they could be passed as args...)
|
|
//perhaps some sort of recursive orig_color type thing would be better, but that would be way too much trouble for little gain
|
|
constexpr std::integral_constant<int, 1> gr_message_color_level{};
|
|
#define CHECK_EMBEDDED_COLORS() if ((*text_ptr >= 0x01) && (*text_ptr <= 0x02)) { \
|
|
text_ptr++; \
|
|
if (*text_ptr){ \
|
|
if (gr_message_color_level >= *(text_ptr-1)) \
|
|
canvas.cv_font_fg_color = static_cast<uint8_t>(*text_ptr); \
|
|
text_ptr++; \
|
|
} \
|
|
} \
|
|
else if (*text_ptr == 0x03) \
|
|
{ \
|
|
underline = 1; \
|
|
text_ptr++; \
|
|
} \
|
|
else if ((*text_ptr >= 0x04) && (*text_ptr <= 0x06)){ \
|
|
if (gr_message_color_level >= *text_ptr - 3) \
|
|
canvas.cv_font_fg_color= static_cast<uint8_t>(orig_color); \
|
|
text_ptr++; \
|
|
}
|
|
|
|
template <bool masked_draws_background>
|
|
static int gr_internal_string0_template(grs_canvas &canvas, const grs_font &cv_font, const int x, int y, const char *const s)
|
|
{
|
|
const auto &&INFONT = font_character_extent(cv_font);
|
|
const auto ft_flags = cv_font.ft_flags;
|
|
const auto proportional = ft_flags & FT_PROPORTIONAL;
|
|
const auto cv_font_bg_color = canvas.cv_font_bg_color;
|
|
int skip_lines = 0;
|
|
|
|
unsigned int VideoOffset1;
|
|
|
|
//to allow easy reseting to default string color with colored strings -MPM
|
|
const auto orig_color = canvas.cv_font_fg_color;
|
|
VideoOffset1 = y * canvas.cv_bitmap.bm_rowsize + x;
|
|
auto next_row = s;
|
|
while (next_row != NULL )
|
|
{
|
|
const auto text_ptr1 = next_row;
|
|
next_row = NULL;
|
|
|
|
if (x==0x8000) { //centered
|
|
int xx = get_centered_x(canvas, cv_font, text_ptr1);
|
|
VideoOffset1 = y * canvas.cv_bitmap.bm_rowsize + xx;
|
|
}
|
|
|
|
for (int r=0; r < cv_font.ft_h; ++r)
|
|
{
|
|
auto text_ptr = text_ptr1;
|
|
|
|
unsigned VideoOffset = VideoOffset1;
|
|
|
|
for (; const uint8_t c0 = *text_ptr; ++text_ptr)
|
|
{
|
|
if (c0 == '\n')
|
|
{
|
|
next_row = &text_ptr[1];
|
|
break;
|
|
}
|
|
|
|
if (c0 == CC_COLOR)
|
|
{
|
|
canvas.cv_font_fg_color = static_cast<uint8_t>(*++text_ptr);
|
|
continue;
|
|
}
|
|
|
|
if (c0 == CC_LSPACING)
|
|
{
|
|
skip_lines = *++text_ptr - '0';
|
|
continue;
|
|
}
|
|
|
|
auto underline = unlikely(c0 == CC_UNDERLINE)
|
|
? ++text_ptr, r == cv_font.ft_baseline + 2 || r == cv_font.ft_baseline + 3
|
|
: 0;
|
|
|
|
const uint8_t c = *text_ptr;
|
|
const auto &result = get_char_width<int>(cv_font, c, text_ptr[1]);
|
|
const auto &width = result.width;
|
|
const auto &spacing = result.spacing;
|
|
|
|
const unsigned letter = c - cv_font.ft_minchar;
|
|
|
|
if (masked_draws_background)
|
|
{
|
|
if (!INFONT(letter)) { //not in font, draw as space
|
|
VideoOffset += spacing;
|
|
text_ptr++;
|
|
continue;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (!INFONT(letter) || c <= 0x06) //not in font, draw as space
|
|
{
|
|
CHECK_EMBEDDED_COLORS() else{
|
|
VideoOffset += spacing;
|
|
text_ptr++;
|
|
}
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (width)
|
|
{
|
|
auto data = &canvas.cv_bitmap.get_bitmap_data()[VideoOffset];
|
|
const auto cv_font_fg_color = canvas.cv_font_fg_color;
|
|
if (underline)
|
|
{
|
|
std::fill_n(data, width, cv_font_fg_color);
|
|
}
|
|
else
|
|
{
|
|
auto fp = proportional ? cv_font.ft_chars[letter] : &cv_font.ft_data[letter * BITS_TO_BYTES(width) * cv_font.ft_h];
|
|
fp += BITS_TO_BYTES(width)*r;
|
|
|
|
/* Setting bits=0 is a dead store, but is necessary to
|
|
* prevent -Og -Wuninitialized from issuing a bogus
|
|
* warning. -Og does not see that bits_remaining=0
|
|
* guarantees that bits will be initialized from *fp before
|
|
* it is read.
|
|
*/
|
|
uint8_t bits_remaining = 0, bits = 0;
|
|
for (uint_fast32_t i = width; i--; ++data, --bits_remaining)
|
|
{
|
|
if (!bits_remaining)
|
|
{
|
|
bits = *fp++;
|
|
bits_remaining = 8;
|
|
}
|
|
|
|
const auto bit_enabled = (bits & 0x80);
|
|
bits <<= 1;
|
|
if (!masked_draws_background)
|
|
{
|
|
if (!bit_enabled)
|
|
continue;
|
|
}
|
|
*data = bit_enabled ? cv_font_fg_color : cv_font_bg_color;
|
|
}
|
|
}
|
|
}
|
|
VideoOffset += spacing;
|
|
}
|
|
VideoOffset1 += canvas.cv_bitmap.bm_rowsize;
|
|
y++;
|
|
}
|
|
y += skip_lines;
|
|
VideoOffset1 += canvas.cv_bitmap.bm_rowsize * skip_lines;
|
|
skip_lines = 0;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int gr_internal_string0(grs_canvas &canvas, const grs_font &cv_font, const int x, const int y, const char *const s)
|
|
{
|
|
return gr_internal_string0_template<true>(canvas, cv_font, x, y, s);
|
|
}
|
|
|
|
static int gr_internal_string0m(grs_canvas &canvas, const grs_font &cv_font, const int x, const int y, const char *const s)
|
|
{
|
|
return gr_internal_string0_template<false>(canvas, cv_font, x, y, s);
|
|
}
|
|
|
|
#if !DXX_USE_OGL
|
|
static int gr_internal_color_string(grs_canvas &canvas, const grs_font &cv_font, const int x, const int y, const char *const s)
|
|
{
|
|
//a bitmap for the character
|
|
grs_bitmap char_bm = {};
|
|
char_bm.set_type(bm_mode::linear);
|
|
char_bm.set_flags(BM_FLAG_TRANSPARENT);
|
|
const char *text_ptr, *next_row, *text_ptr1;
|
|
int letter;
|
|
int xx,yy;
|
|
|
|
const auto &&INFONT = font_character_extent(cv_font);
|
|
char_bm.bm_h = cv_font.ft_h; //set height for chars of this font
|
|
|
|
next_row = s;
|
|
|
|
yy = y;
|
|
|
|
const auto &&fspacy1 = FSPACY(1);
|
|
while (next_row != NULL)
|
|
{
|
|
text_ptr1 = next_row;
|
|
next_row = NULL;
|
|
|
|
text_ptr = text_ptr1;
|
|
|
|
xx = x;
|
|
|
|
if (xx==0x8000) //centered
|
|
xx = get_centered_x(canvas, cv_font, text_ptr);
|
|
|
|
while (*text_ptr)
|
|
{
|
|
if (*text_ptr == '\n' )
|
|
{
|
|
next_row = &text_ptr[1];
|
|
yy += cv_font.ft_h + fspacy1;
|
|
break;
|
|
}
|
|
|
|
letter = static_cast<uint8_t>(*text_ptr) - cv_font.ft_minchar;
|
|
|
|
const auto &result = get_char_width<int>(cv_font, text_ptr[0], text_ptr[1]);
|
|
const auto &width = result.width;
|
|
const auto &spacing = result.spacing;
|
|
|
|
if (!INFONT(letter)) { //not in font, draw as space
|
|
xx += spacing;
|
|
text_ptr++;
|
|
continue;
|
|
}
|
|
|
|
const auto fp = (cv_font.ft_flags & FT_PROPORTIONAL)
|
|
? cv_font.ft_chars[letter]
|
|
: &cv_font.ft_data[letter * BITS_TO_BYTES(width) * cv_font.ft_h];
|
|
|
|
gr_init_bitmap(char_bm, bm_mode::linear, 0, 0, width, cv_font.ft_h, width, fp);
|
|
gr_bitmapm(canvas, xx, yy, char_bm);
|
|
|
|
xx += spacing;
|
|
|
|
text_ptr++;
|
|
}
|
|
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
#else //OGL
|
|
|
|
static int get_font_total_width(const grs_font &font)
|
|
{
|
|
if (font.ft_flags & FT_PROPORTIONAL)
|
|
{
|
|
int w=0;
|
|
range_for (const auto v, unchecked_partial_range(font.ft_widths, static_cast<unsigned>(font.ft_maxchar - font.ft_minchar) + 1))
|
|
{
|
|
if (v < 0)
|
|
throw std::underflow_error("negative width");
|
|
w += v;
|
|
}
|
|
return w;
|
|
}else{
|
|
return font.ft_w*(font.ft_maxchar-font.ft_minchar+1);
|
|
}
|
|
}
|
|
|
|
static void ogl_font_choose_size(grs_font * font,int gap,int *rw,int *rh){
|
|
int nchars = font->ft_maxchar-font->ft_minchar+1;
|
|
int r,x,y,nc=0,smallest=999999,smallr=-1,tries;
|
|
int smallprop=10000;
|
|
int w;
|
|
for (int h=32;h<=256;h*=2){
|
|
// h=pow2ize(font->ft_h*rows+gap*(rows-1));
|
|
if (font->ft_h>h)continue;
|
|
r=(h/(font->ft_h+gap));
|
|
w=pow2ize((get_font_total_width(*font)+(nchars-r)*gap)/r);
|
|
tries=0;
|
|
do {
|
|
if (tries)
|
|
w=pow2ize(w+1);
|
|
if(tries>3){
|
|
break;
|
|
}
|
|
nc=0;
|
|
y=0;
|
|
while(y+font->ft_h<=h){
|
|
x=0;
|
|
while (x<w){
|
|
if (nc==nchars)
|
|
break;
|
|
if (font->ft_flags & FT_PROPORTIONAL){
|
|
if (x+font->ft_widths[nc]+gap>w)break;
|
|
x+=font->ft_widths[nc++]+gap;
|
|
}else{
|
|
if (x+font->ft_w+gap>w)break;
|
|
x+=font->ft_w+gap;
|
|
nc++;
|
|
}
|
|
}
|
|
if (nc==nchars)
|
|
break;
|
|
y+=font->ft_h+gap;
|
|
}
|
|
|
|
tries++;
|
|
}while(nc!=nchars);
|
|
if (nc!=nchars)
|
|
continue;
|
|
|
|
if (w*h==smallest){//this gives squarer sizes priority (ie, 128x128 would be better than 512*32)
|
|
if (w>=h){
|
|
if (w/h<smallprop){
|
|
smallprop=w/h;
|
|
smallest++;//hack
|
|
}
|
|
}else{
|
|
if (h/w<smallprop){
|
|
smallprop=h/w;
|
|
smallest++;//hack
|
|
}
|
|
}
|
|
}
|
|
if (w*h<smallest){
|
|
smallr=1;
|
|
smallest=w*h;
|
|
*rw=w;
|
|
*rh=h;
|
|
}
|
|
}
|
|
if (smallr<=0)
|
|
Error("Could not fit font?\n");
|
|
}
|
|
|
|
static void ogl_init_font(grs_font * font)
|
|
{
|
|
int oglflags = OGL_FLAG_ALPHA;
|
|
int nchars = font->ft_maxchar-font->ft_minchar+1;
|
|
int w,h,tw,th,curx=0,cury=0;
|
|
int gap=1; // x/y offset between the chars so we can filter
|
|
|
|
th = tw = 0xffff;
|
|
|
|
ogl_font_choose_size(font,gap,&tw,&th);
|
|
{
|
|
RAIIdmem<uint8_t[]> data;
|
|
const unsigned length = tw * th;
|
|
MALLOC(data, uint8_t, length);
|
|
std::fill_n(data.get(), length, TRANSPARENCY_COLOR); // map the whole data with transparency so we won't have borders if using gap
|
|
gr_init_main_bitmap(font->ft_parent_bitmap, bm_mode::linear, 0, 0, tw, th, tw, std::move(data));
|
|
}
|
|
gr_set_transparent(font->ft_parent_bitmap, 1);
|
|
|
|
if (!(font->ft_flags & FT_COLOR))
|
|
oglflags |= OGL_FLAG_NOCOLOR;
|
|
ogl_init_texture(*(font->ft_parent_bitmap.gltexture = ogl_get_free_texture()), tw, th, oglflags); // have to init the gltexture here so the subbitmaps will find it.
|
|
|
|
font->ft_bitmaps = make_unique<grs_bitmap[]>(nchars);
|
|
h=font->ft_h;
|
|
|
|
for(int i=0;i<nchars;i++)
|
|
{
|
|
if (font->ft_flags & FT_PROPORTIONAL)
|
|
w=font->ft_widths[i];
|
|
else
|
|
w=font->ft_w;
|
|
|
|
if (w<1 || w>256)
|
|
continue;
|
|
|
|
if (curx+w+gap>tw)
|
|
{
|
|
cury+=h+gap;
|
|
curx=0;
|
|
}
|
|
|
|
if (cury+h>th)
|
|
Error("font doesn't really fit (%i/%i)?\n",i,nchars);
|
|
|
|
if (font->ft_flags & FT_COLOR)
|
|
{
|
|
const auto fp = (font->ft_flags & FT_PROPORTIONAL)
|
|
? font->ft_chars[i]
|
|
: font->ft_data + i * w*h;
|
|
for (int y=0;y<h;y++)
|
|
{
|
|
for (int x=0;x<w;x++)
|
|
{
|
|
font->ft_parent_bitmap.get_bitmap_data()[curx+x+(cury+y)*tw] = fp[x+y*w];
|
|
// Let's call this a HACK:
|
|
// If we filter the fonts, the sliders will be messed up as the border pixels will have an
|
|
// alpha value while filtering. So the slider bitmaps will not look "connected".
|
|
// To prevent this, duplicate the first/last pixel-row with a 1-pixel offset.
|
|
if (gap && i >= 99 && i <= 102)
|
|
{
|
|
// See which bitmaps need left/right shifts:
|
|
// 99 = SLIDER_LEFT - shift RIGHT
|
|
// 100 = SLIDER_RIGHT - shift LEFT
|
|
// 101 = SLIDER_MIDDLE - shift LEFT+RIGHT
|
|
// 102 = SLIDER_MARKER - shift RIGHT
|
|
|
|
// shift left border
|
|
if (x==0 && i != 99 && i != 102)
|
|
font->ft_parent_bitmap.get_bitmap_data()[(curx+x+(cury+y)*tw)-1] = fp[x+y*w];
|
|
|
|
// shift right border
|
|
if (x==w-1 && i != 100)
|
|
font->ft_parent_bitmap.get_bitmap_data()[(curx+x+(cury+y)*tw)+1] = fp[x+y*w];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
int BitMask,bits=0,white=gr_find_closest_color(63,63,63);
|
|
auto fp = (font->ft_flags & FT_PROPORTIONAL)
|
|
? font->ft_chars[i]
|
|
: font->ft_data + i * BITS_TO_BYTES(w)*h;
|
|
for (int y=0;y<h;y++){
|
|
BitMask=0;
|
|
for (int x=0; x< w; x++ )
|
|
{
|
|
if (BitMask==0) {
|
|
bits = *fp++;
|
|
BitMask = 0x80;
|
|
}
|
|
|
|
if (bits & BitMask)
|
|
font->ft_parent_bitmap.get_bitmap_data()[curx+x+(cury+y)*tw] = white;
|
|
else
|
|
font->ft_parent_bitmap.get_bitmap_data()[curx+x+(cury+y)*tw] = 255;
|
|
BitMask >>= 1;
|
|
}
|
|
}
|
|
}
|
|
gr_init_sub_bitmap(font->ft_bitmaps[i],font->ft_parent_bitmap,curx,cury,w,h);
|
|
curx+=w+gap;
|
|
}
|
|
ogl_loadbmtexture_f(font->ft_parent_bitmap, CGameCfg.TexFilt, 0, 0);
|
|
}
|
|
|
|
static int ogl_internal_string(grs_canvas &canvas, const grs_font &cv_font, const int x, const int y, const char *const s)
|
|
{
|
|
const char * text_ptr, * next_row, * text_ptr1;
|
|
int letter;
|
|
int xx,yy;
|
|
int orig_color = canvas.cv_font_fg_color;//to allow easy reseting to default string color with colored strings -MPM
|
|
int underline;
|
|
|
|
next_row = s;
|
|
|
|
yy = y;
|
|
|
|
if (grd_curscreen->sc_canvas.cv_bitmap.get_type() != bm_mode::ogl)
|
|
Error("carp.\n");
|
|
const auto &&fspacy1 = FSPACY(1);
|
|
const auto &&INFONT = font_character_extent(cv_font);
|
|
const auto &&fontscale_x = FONTSCALE_X();
|
|
const auto &&FONTSCALE_Y_ft_h = FONTSCALE_Y(cv_font.ft_h);
|
|
ogl_colors colors;
|
|
while (next_row != NULL)
|
|
{
|
|
text_ptr1 = next_row;
|
|
next_row = NULL;
|
|
|
|
text_ptr = text_ptr1;
|
|
|
|
xx = x;
|
|
|
|
if (xx==0x8000) //centered
|
|
xx = get_centered_x(canvas, cv_font, text_ptr);
|
|
|
|
while (*text_ptr)
|
|
{
|
|
|
|
if (*text_ptr == '\n' )
|
|
{
|
|
next_row = &text_ptr[1];
|
|
yy += FONTSCALE_Y_ft_h + fspacy1;
|
|
break;
|
|
}
|
|
|
|
letter = static_cast<uint8_t>(*text_ptr) - cv_font.ft_minchar;
|
|
|
|
const auto &result = get_char_width<int>(cv_font, text_ptr[0], text_ptr[1]);
|
|
const auto &spacing = result.spacing;
|
|
|
|
underline = 0;
|
|
if (!INFONT(letter) || static_cast<uint8_t>(*text_ptr) <= 0x06) //not in font, draw as space
|
|
{
|
|
CHECK_EMBEDDED_COLORS() else{
|
|
xx += spacing;
|
|
text_ptr++;
|
|
}
|
|
|
|
if (underline)
|
|
{
|
|
const uint8_t color = canvas.cv_font_fg_color;
|
|
gr_rect(canvas, xx, yy + cv_font.ft_baseline + 2, xx + cv_font.ft_w, yy + cv_font.ft_baseline + 3, color);
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
const auto ft_w = (cv_font.ft_flags & FT_PROPORTIONAL)
|
|
? cv_font.ft_widths[letter]
|
|
: cv_font.ft_w;
|
|
|
|
ogl_ubitmapm_cs(canvas, xx, yy, fontscale_x(ft_w), FONTSCALE_Y_ft_h, cv_font.ft_bitmaps[letter], (cv_font.ft_flags & FT_COLOR) ? colors.white : (canvas.cv_bitmap.get_type() == bm_mode::ogl) ? colors.init(canvas.cv_font_fg_color) : throw std::runtime_error("non-color string to non-ogl dest"), F1_0);
|
|
|
|
xx += spacing;
|
|
|
|
text_ptr++;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
#define gr_internal_color_string ogl_internal_string
|
|
#endif //OGL
|
|
|
|
void gr_string(grs_canvas &canvas, const grs_font &cv_font, const int x, const int y, const char *const s)
|
|
{
|
|
int w, h;
|
|
gr_get_string_size(cv_font, s, &w, &h, nullptr);
|
|
gr_string(canvas, cv_font, x, y, s, w, h);
|
|
}
|
|
|
|
static void gr_ustring_mono(grs_canvas &canvas, const grs_font &cv_font, const int x, const int y, const char *const s)
|
|
{
|
|
switch (canvas.cv_bitmap.get_type())
|
|
{
|
|
case bm_mode::linear:
|
|
if (canvas.cv_font_bg_color == -1)
|
|
gr_internal_string0m(canvas, cv_font, x, y, s);
|
|
else
|
|
gr_internal_string0(canvas, cv_font, x, y, s);
|
|
}
|
|
}
|
|
|
|
void gr_string(grs_canvas &canvas, const grs_font &cv_font, const int x, const int y, const char *const s, const int w, const int h)
|
|
{
|
|
if (y + h < 0)
|
|
return;
|
|
const auto bm_h = canvas.cv_bitmap.bm_h;
|
|
if (y > bm_h)
|
|
return;
|
|
const auto bm_w = canvas.cv_bitmap.bm_w;
|
|
int xw = w;
|
|
if ( x == 0x8000 ) {
|
|
// for x, since this will be centered, only look at
|
|
// width.
|
|
} else {
|
|
if (x > bm_w)
|
|
return;
|
|
xw += x;
|
|
if (xw < 0)
|
|
return;
|
|
}
|
|
if (
|
|
#if DXX_USE_OGL
|
|
canvas.cv_bitmap.get_type() == bm_mode::ogl ||
|
|
#endif
|
|
cv_font.ft_flags & FT_COLOR)
|
|
{
|
|
gr_internal_color_string(canvas, cv_font, x, y, s);
|
|
return;
|
|
}
|
|
// Partially clipped...
|
|
if (!(y < 0 ||
|
|
x < 0 ||
|
|
xw > bm_w ||
|
|
y + h > bm_h))
|
|
{
|
|
gr_ustring_mono(canvas, cv_font, x, y, s);
|
|
return;
|
|
}
|
|
|
|
if (canvas.cv_font_bg_color == -1)
|
|
{
|
|
gr_internal_string_clipped_m(canvas, cv_font, x, y, s);
|
|
return;
|
|
}
|
|
gr_internal_string_clipped(canvas, cv_font, x, y, s);
|
|
}
|
|
|
|
void gr_ustring(grs_canvas &canvas, const grs_font &cv_font, const int x, const int y, const char *const s)
|
|
{
|
|
#if DXX_USE_OGL
|
|
if (canvas.cv_bitmap.get_type() == bm_mode::ogl)
|
|
{
|
|
ogl_internal_string(canvas, cv_font, x, y, s);
|
|
return;
|
|
}
|
|
#endif
|
|
|
|
if (canvas.cv_font->ft_flags & FT_COLOR) {
|
|
gr_internal_color_string(canvas, cv_font, x, y, s);
|
|
}
|
|
else
|
|
gr_ustring_mono(canvas, cv_font, x, y, s);
|
|
}
|
|
|
|
int gr_get_string_height(const grs_font &cv_font, const unsigned lines)
|
|
{
|
|
const auto fontscale_y = FONTSCALE_Y(cv_font.ft_h);
|
|
return static_cast<int>(fontscale_y + (lines * (fontscale_y + FSPACY(1))));
|
|
}
|
|
|
|
void gr_get_string_size(const grs_font &cv_font, const char *s, int *const string_width, int *const string_height, int *const average_width)
|
|
{
|
|
gr_get_string_size(cv_font, s, string_width, string_height, average_width, UINT_MAX);
|
|
}
|
|
|
|
void gr_get_string_size(const grs_font &cv_font, const char *s, int *const string_width, int *const string_height, int *const average_width, const unsigned max_chars_per_line)
|
|
{
|
|
float longest_width=0.0,string_width_f=0.0;
|
|
unsigned lines = 0;
|
|
if (average_width)
|
|
*average_width = cv_font.ft_w;
|
|
if (!string_width && !string_height)
|
|
return;
|
|
if (s)
|
|
{
|
|
unsigned remaining_chars_this_line = max_chars_per_line;
|
|
while (*s)
|
|
{
|
|
if (*s == '\n')
|
|
{
|
|
if (longest_width < string_width_f)
|
|
longest_width = string_width_f;
|
|
string_width_f = 0;
|
|
const auto os = s;
|
|
while (*++s == '\n')
|
|
{
|
|
}
|
|
lines += s - os;
|
|
if (!*s)
|
|
break;
|
|
remaining_chars_this_line = max_chars_per_line;
|
|
}
|
|
|
|
const auto &result = get_char_width<float>(cv_font, s[0], s[1]);
|
|
const auto &spacing = result.spacing;
|
|
string_width_f += spacing;
|
|
s++;
|
|
if (!--remaining_chars_this_line)
|
|
break;
|
|
}
|
|
}
|
|
if (string_width)
|
|
*string_width = std::max(longest_width, string_width_f);
|
|
if (string_height)
|
|
*string_height = gr_get_string_height(cv_font, lines);
|
|
}
|
|
|
|
std::pair<const char *, unsigned> gr_get_string_wrap(const grs_font &cv_font, const char *s, unsigned limit)
|
|
{
|
|
assert(s);
|
|
float string_width_f=0.0;
|
|
const float limit_f = limit;
|
|
for (uint8_t c; (c = *s); ++s)
|
|
{
|
|
const auto &&spacing = get_char_width<float>(cv_font, c, s[1]).spacing;
|
|
const float next_string_width = string_width_f + spacing;
|
|
if (next_string_width >= limit_f)
|
|
break;
|
|
string_width_f = next_string_width;
|
|
}
|
|
return {s, static_cast<unsigned>(string_width_f)};
|
|
}
|
|
|
|
template <void (&F)(grs_canvas &, const grs_font &, int, int, const char *)>
|
|
void (gr_printt)(grs_canvas &canvas, const grs_font &cv_font, const int x, const int y, const char *const format, ...)
|
|
{
|
|
char buffer[1000];
|
|
va_list args;
|
|
|
|
va_start(args, format );
|
|
vsnprintf(buffer, sizeof(buffer), format, args);
|
|
va_end(args);
|
|
F(canvas, cv_font, x, y, buffer);
|
|
}
|
|
|
|
template void gr_printt<gr_string>(grs_canvas &, const grs_font &, int, int, const char *, ...);
|
|
template void gr_printt<gr_ustring>(grs_canvas &, const grs_font &, int, int, const char *, ...);
|
|
|
|
void gr_close_font(std::unique_ptr<grs_font> font)
|
|
{
|
|
if (font)
|
|
{
|
|
//find font in list
|
|
if (!(font->ft_flags & FT_COLOR))
|
|
return;
|
|
auto e = end(open_font);
|
|
auto i = std::find(begin(open_font), e, font.get());
|
|
if (i == e)
|
|
throw std::logic_error("closing non-open font");
|
|
*i = nullptr;
|
|
}
|
|
}
|
|
|
|
//remap a font, re-reading its data & palette
|
|
static void gr_remap_font(grs_font *font, const char *fontname);
|
|
|
|
//remap (by re-reading) all the color fonts
|
|
void gr_remap_color_fonts()
|
|
{
|
|
range_for (auto font, open_font)
|
|
{
|
|
if (font)
|
|
gr_remap_font(font, &font->ft_filename[0]);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* reads a grs_font structure from a PHYSFS_File
|
|
*/
|
|
static void grs_font_read(grs_font *gf, PHYSFS_File *fp)
|
|
{
|
|
gf->ft_w = PHYSFSX_readShort(fp);
|
|
gf->ft_h = PHYSFSX_readShort(fp);
|
|
gf->ft_flags = PHYSFSX_readShort(fp);
|
|
gf->ft_baseline = PHYSFSX_readShort(fp);
|
|
gf->ft_minchar = PHYSFSX_readByte(fp);
|
|
gf->ft_maxchar = PHYSFSX_readByte(fp);
|
|
PHYSFSX_readShort(fp);
|
|
gf->ft_data = reinterpret_cast<uint8_t *>(static_cast<uintptr_t>(PHYSFSX_readInt(fp)) - GRS_FONT_SIZE);
|
|
PHYSFSX_readInt(fp);
|
|
gf->ft_widths = reinterpret_cast<int16_t *>(static_cast<uintptr_t>(PHYSFSX_readInt(fp)) - GRS_FONT_SIZE);
|
|
gf->ft_kerndata = reinterpret_cast<uint8_t *>(static_cast<uintptr_t>(PHYSFSX_readInt(fp)) - GRS_FONT_SIZE);
|
|
}
|
|
|
|
static std::unique_ptr<grs_font> gr_internal_init_font(const char *fontname)
|
|
{
|
|
const uint8_t *ptr;
|
|
uint8_t *ft_data;
|
|
struct {
|
|
array<char, 4> magic;
|
|
unsigned datasize; //size up to (but not including) palette
|
|
} file_header;
|
|
|
|
auto fontfile = PHYSFSX_openReadBuffered(fontname);
|
|
|
|
if (!fontfile) {
|
|
con_printf(CON_VERBOSE, "Can't open font file %s", fontname);
|
|
return {};
|
|
}
|
|
|
|
static_assert(sizeof(file_header) == 8, "file header size error");
|
|
if (PHYSFS_read(fontfile, &file_header, sizeof(file_header), 1) != 1 ||
|
|
memcmp(file_header.magic.data(), "PSFN", 4) ||
|
|
(file_header.datasize = INTEL_INT(file_header.datasize)) < GRS_FONT_SIZE)
|
|
{
|
|
con_printf(CON_NORMAL, "Invalid header in font file %s", fontname);
|
|
return {};
|
|
}
|
|
|
|
file_header.datasize -= GRS_FONT_SIZE; // subtract the size of the header.
|
|
const auto &datasize = file_header.datasize;
|
|
|
|
auto font = make_unique<grs_font>();
|
|
grs_font_read(font.get(), fontfile);
|
|
|
|
const unsigned nchars = font->ft_maxchar - font->ft_minchar + 1;
|
|
std::size_t ft_chars_storage = (font->ft_flags & FT_PROPORTIONAL)
|
|
? sizeof(uint8_t *) * nchars
|
|
: 0;
|
|
|
|
auto ft_allocdata = make_unique<uint8_t[]>(datasize + ft_chars_storage);
|
|
const auto font_data = &ft_allocdata[ft_chars_storage];
|
|
if (PHYSFS_read(fontfile, font_data, 1, datasize) != datasize)
|
|
{
|
|
con_printf(CON_URGENT, "Insufficient data in font file %s", fontname);
|
|
return {};
|
|
}
|
|
|
|
if (font->ft_flags & FT_PROPORTIONAL) {
|
|
const auto offset_widths = reinterpret_cast<uintptr_t>(font->ft_widths);
|
|
auto w = reinterpret_cast<short *>(&font_data[offset_widths]);
|
|
if (offset_widths >= datasize || offset_widths + (nchars * sizeof(*w)) >= datasize)
|
|
{
|
|
con_printf(CON_URGENT, "Missing widths in font file %s", fontname);
|
|
return {};
|
|
}
|
|
font->ft_widths = w;
|
|
const auto offset_data = reinterpret_cast<uintptr_t>(font->ft_data);
|
|
if (offset_data >= datasize)
|
|
{
|
|
con_printf(CON_URGENT, "Missing data in font file %s", fontname);
|
|
return {};
|
|
}
|
|
font->ft_data = ptr = ft_data = &font_data[offset_data];
|
|
const auto ft_chars = reinterpret_cast<const uint8_t **>(ft_allocdata.get());
|
|
font->ft_chars = reinterpret_cast<const uint8_t *const *>(ft_chars);
|
|
|
|
const unsigned is_color = font->ft_flags & FT_COLOR;
|
|
const unsigned ft_h = font->ft_h;
|
|
std::generate_n(ft_chars, nchars, [is_color, ft_h, &w, &ptr]{
|
|
const unsigned s = INTEL_SHORT(*w);
|
|
if (words_bigendian)
|
|
*w = static_cast<uint16_t>(s);
|
|
++w;
|
|
const auto r = ptr;
|
|
ptr += ft_h * (is_color ? s : BITS_TO_BYTES(s));
|
|
return r;
|
|
});
|
|
} else {
|
|
|
|
font->ft_data = ft_data = font_data;
|
|
font->ft_widths = NULL;
|
|
|
|
ptr = font->ft_data + (nchars * font->ft_w * font->ft_h);
|
|
}
|
|
|
|
if (font->ft_flags & FT_KERNED)
|
|
{
|
|
const auto offset_kerndata = reinterpret_cast<uintptr_t>(font->ft_kerndata);
|
|
if (datasize <= offset_kerndata)
|
|
{
|
|
con_printf(CON_URGENT, "Missing kerndata in font file %s", fontname);
|
|
return {};
|
|
}
|
|
const auto begin_kerndata = reinterpret_cast<const unsigned char *>(&font_data[offset_kerndata]);
|
|
const auto end_font_data = &font_data[datasize - ((datasize - offset_kerndata + 2) % 3)];
|
|
for (auto cur_kerndata = begin_kerndata;; cur_kerndata += 3)
|
|
{
|
|
if (cur_kerndata == end_font_data)
|
|
{
|
|
con_printf(CON_URGENT, "Unterminated kerndata in font file %s", fontname);
|
|
return {};
|
|
}
|
|
if (*cur_kerndata == kerndata_terminator)
|
|
break;
|
|
}
|
|
font->ft_kerndata = begin_kerndata;
|
|
}
|
|
else
|
|
font->ft_kerndata = nullptr;
|
|
|
|
if (font->ft_flags & FT_COLOR) { //remap palette
|
|
palette_array_t palette;
|
|
array<uint8_t, 256> colormap;
|
|
/* `freq` exists so that decode_data can write to it, but it is
|
|
* otherwise unused. `decode_data` is not guaranteed to write
|
|
* to every element, so `freq` must not be read without first
|
|
* adding an initialization step.
|
|
*/
|
|
array<bool, 256> freq;
|
|
|
|
PHYSFS_read(fontfile,&palette[0],sizeof(palette[0]),palette.size()); //read the palette
|
|
|
|
build_colormap_good(palette, colormap);
|
|
|
|
colormap[TRANSPARENCY_COLOR] = TRANSPARENCY_COLOR; // changed from colormap[255] = 255 to this for macintosh
|
|
|
|
decode_data(ft_data, ptr - font->ft_data, colormap, freq );
|
|
}
|
|
fontfile.reset();
|
|
//set curcanv vars
|
|
|
|
auto &ft_filename = font->ft_filename;
|
|
font->ft_allocdata = move(ft_allocdata);
|
|
strncpy(&ft_filename[0], fontname, ft_filename.size());
|
|
return font;
|
|
}
|
|
|
|
grs_font_ptr gr_init_font(grs_canvas &canvas, const char *fontname)
|
|
{
|
|
auto font = gr_internal_init_font(fontname);
|
|
if (!font)
|
|
return {};
|
|
|
|
canvas.cv_font = font.get();
|
|
canvas.cv_font_fg_color = 0;
|
|
canvas.cv_font_bg_color = 0;
|
|
if (font->ft_flags & FT_COLOR)
|
|
{
|
|
auto e = end(open_font);
|
|
auto i = std::find(begin(open_font), e, static_cast<grs_font *>(nullptr));
|
|
if (i == e)
|
|
throw std::logic_error("too many open fonts");
|
|
*i = font.get();
|
|
}
|
|
#if DXX_USE_OGL
|
|
ogl_init_font(font.get());
|
|
#endif
|
|
return grs_font_ptr(font.release());
|
|
}
|
|
|
|
//remap a font by re-reading its data & palette
|
|
void gr_remap_font(grs_font *font, const char *fontname)
|
|
{
|
|
auto n = gr_internal_init_font(fontname);
|
|
if (!n)
|
|
return;
|
|
if (!(n->ft_flags & FT_COLOR))
|
|
return;
|
|
*font = std::move(*n.get());
|
|
#if DXX_USE_OGL
|
|
ogl_init_font(font);
|
|
#endif
|
|
}
|
|
|
|
void gr_set_curfont(grs_canvas &canvas, const grs_font *n)
|
|
{
|
|
canvas.cv_font = n;
|
|
}
|
|
|
|
void gr_set_fontcolor(grs_canvas &canvas, const int fg_color, const int bg_color)
|
|
{
|
|
canvas.cv_font_fg_color = fg_color;
|
|
canvas.cv_font_bg_color = bg_color;
|
|
}
|
|
|
|
template <bool masked_draws_background>
|
|
static int gr_internal_string_clipped_template(grs_canvas &canvas, const grs_font &cv_font, int x, int y, const char *const s)
|
|
{
|
|
const char * text_ptr, * next_row, * text_ptr1;
|
|
int letter;
|
|
int x1 = x, last_x;
|
|
|
|
next_row = s;
|
|
|
|
const auto &&INFONT = font_character_extent(cv_font);
|
|
const auto ft_flags = cv_font.ft_flags;
|
|
const auto proportional = ft_flags & FT_PROPORTIONAL;
|
|
const auto cv_font_bg_color = canvas.cv_font_bg_color;
|
|
|
|
while (next_row != NULL )
|
|
{
|
|
text_ptr1 = next_row;
|
|
next_row = NULL;
|
|
|
|
x = x1;
|
|
if (x==0x8000) //centered
|
|
x = get_centered_x(canvas, cv_font, text_ptr1);
|
|
|
|
last_x = x;
|
|
|
|
for (int r=0; r < cv_font.ft_h; r++) {
|
|
text_ptr = text_ptr1;
|
|
x = last_x;
|
|
|
|
for (; const uint8_t c0 = *text_ptr; ++text_ptr)
|
|
{
|
|
if (c0 == '\n')
|
|
{
|
|
next_row = &text_ptr[1];
|
|
break;
|
|
}
|
|
if (c0 == CC_COLOR)
|
|
{
|
|
canvas.cv_font_fg_color = static_cast<uint8_t>(*++text_ptr);
|
|
continue;
|
|
}
|
|
|
|
if (c0 == CC_LSPACING)
|
|
{
|
|
Int3(); // Warning: skip lines not supported for clipped strings.
|
|
text_ptr += 1;
|
|
continue;
|
|
}
|
|
|
|
const auto underline = unlikely(c0 == CC_UNDERLINE)
|
|
? ++text_ptr, r == cv_font.ft_baseline + 2 || r == cv_font.ft_baseline + 3
|
|
: 0;
|
|
const uint8_t c = *text_ptr;
|
|
const auto &result = get_char_width<int>(cv_font, c, text_ptr[1]);
|
|
const auto &width = result.width;
|
|
const auto &spacing = result.spacing;
|
|
|
|
letter = c - cv_font.ft_minchar;
|
|
|
|
if (!INFONT(letter)) { //not in font, draw as space
|
|
x += spacing;
|
|
continue;
|
|
}
|
|
const auto cv_font_fg_color = canvas.cv_font_fg_color;
|
|
auto color = cv_font_fg_color;
|
|
if (width)
|
|
{
|
|
if (underline) {
|
|
for (uint_fast32_t i = width; i--;)
|
|
{
|
|
gr_pixel(canvas.cv_bitmap, x++, y, color);
|
|
}
|
|
} else {
|
|
auto fp = proportional ? cv_font.ft_chars[letter] : cv_font.ft_data + letter * BITS_TO_BYTES(width) * cv_font.ft_h;
|
|
fp += BITS_TO_BYTES(width)*r;
|
|
|
|
/* Setting bits=0 is a dead store, but is necessary to
|
|
* prevent -Og -Wuninitialized from issuing a bogus
|
|
* warning. -Og does not see that bits_remaining=0
|
|
* guarantees that bits will be initialized from *fp before
|
|
* it is read.
|
|
*/
|
|
uint8_t bits_remaining = 0, bits = 0;
|
|
|
|
for (uint_fast32_t i = width; i--; ++x, --bits_remaining)
|
|
{
|
|
if (!bits_remaining)
|
|
{
|
|
bits = *fp++;
|
|
bits_remaining = 8;
|
|
}
|
|
const auto bit_enabled = (bits & 0x80);
|
|
bits <<= 1;
|
|
if (masked_draws_background)
|
|
color = bit_enabled ? cv_font_fg_color : cv_font_bg_color;
|
|
else
|
|
{
|
|
if (!bit_enabled)
|
|
continue;
|
|
}
|
|
gr_pixel(canvas.cv_bitmap, x, y, color);
|
|
}
|
|
}
|
|
}
|
|
x += spacing-width; //for kerning
|
|
}
|
|
y++;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int gr_internal_string_clipped_m(grs_canvas &canvas, const grs_font &cv_font, const int x, const int y, const char *const s)
|
|
{
|
|
return gr_internal_string_clipped_template<true>(canvas, cv_font, x, y, s);
|
|
}
|
|
|
|
static int gr_internal_string_clipped(grs_canvas &canvas, const grs_font &cv_font, const int x, const int y, const char *const s)
|
|
{
|
|
return gr_internal_string_clipped_template<false>(canvas, cv_font, x, y, s);
|
|
}
|
|
|
|
}
|