// Scintilla source code edit control // PlatGTK.cxx - implementation of platform facilities on GTK+/Linux // Copyright 1998-2004 by Neil Hodgson // The License.txt file describes the conditions under which this software may be distributed. #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if defined(GDK_WINDOWING_WAYLAND) #include #endif #include "ScintillaTypes.h" #include "ScintillaMessages.h" #include "Debugging.h" #include "Geometry.h" #include "Platform.h" #include "Scintilla.h" #include "ScintillaWidget.h" #include "XPM.h" #include "UniConversion.h" #include "Wrappers.h" #include "Converter.h" #ifdef _MSC_VER // Ignore unreferenced local functions in GTK+ headers #pragma warning(disable: 4505) #endif using namespace Scintilla; using namespace Scintilla::Internal; namespace { constexpr double kPi = 3.14159265358979323846; constexpr double degrees = kPi / 180.0; struct IntegerRectangle { int left; int top; int right; int bottom; explicit IntegerRectangle(PRectangle rc) noexcept : left(static_cast(rc.left)), top(static_cast(rc.top)), right(static_cast(rc.right)), bottom(static_cast(rc.bottom)) { } int Width() const noexcept { return right - left; } int Height() const noexcept { return bottom - top; } }; GtkWidget *PWidget(WindowID wid) noexcept { return static_cast(wid); } void SetFractionalPositions([[maybe_unused]] PangoContext *pcontext) noexcept { #if PANGO_VERSION_CHECK(1,44,3) pango_context_set_round_glyph_positions(pcontext, FALSE); #endif } void LayoutSetText(PangoLayout *layout, std::string_view text) noexcept { pango_layout_set_text(layout, text.data(), static_cast(text.length())); } enum class EncodingType { singleByte, utf8, dbcs }; // Holds a PangoFontDescription*. class FontHandle : public Font { public: UniquePangoFontDescription fd; CharacterSet characterSet; explicit FontHandle(const FontParameters &fp) : fd(pango_font_description_new()), characterSet(fp.characterSet) { if (fd) { pango_font_description_set_family(fd.get(), (fp.faceName[0] == '!') ? fp.faceName + 1 : fp.faceName); pango_font_description_set_size(fd.get(), pango_units_from_double(fp.size)); pango_font_description_set_weight(fd.get(), static_cast(fp.weight)); pango_font_description_set_style(fd.get(), fp.italic ? PANGO_STYLE_ITALIC : PANGO_STYLE_NORMAL); } } ~FontHandle() override = default; }; // X has a 16 bit coordinate space, so stop drawing here to avoid wrapping constexpr int maxCoordinate = 32000; const FontHandle *PFont(const Font *f) noexcept { return dynamic_cast(f); } } std::shared_ptr Font::Allocate(const FontParameters &fp) { return std::make_shared(fp); } namespace Scintilla { // SurfaceID is a cairo_t* class SurfaceImpl : public Surface { SurfaceMode mode; EncodingType et= EncodingType::singleByte; WindowID widSave = nullptr; cairo_t *context = nullptr; UniqueCairo cairoOwned; UniqueCairoSurface surf; bool inited = false; UniquePangoContext pcontext; double resolution = 1.0; PangoDirection direction = PANGO_DIRECTION_LTR; const cairo_font_options_t *fontOptions = nullptr; PangoLanguage *language = nullptr; UniquePangoLayout layout; Converter conv; CharacterSet characterSet = static_cast(-1); void PenColourAlpha(ColourRGBA fore) noexcept; void SetConverter(CharacterSet characterSet_); void CairoRectangle(PRectangle rc) noexcept; public: SurfaceImpl() noexcept; SurfaceImpl(cairo_t *context_, int width, int height, SurfaceMode mode_, WindowID wid) noexcept; // Deleted so SurfaceImpl objects can not be copied. SurfaceImpl(const SurfaceImpl&) = delete; SurfaceImpl(SurfaceImpl&&) = delete; SurfaceImpl&operator=(const SurfaceImpl&) = delete; SurfaceImpl&operator=(SurfaceImpl&&) = delete; ~SurfaceImpl() override = default; void GetContextState() noexcept; UniquePangoContext MeasuringContext(); void Init(WindowID wid) override; void Init(SurfaceID sid, WindowID wid) override; std::unique_ptr AllocatePixMap(int width, int height) override; void SetMode(SurfaceMode mode_) override; void Release() noexcept override; int SupportsFeature(Supports feature) noexcept override; bool Initialised() override; int LogPixelsY() override; int PixelDivisions() override; int DeviceHeightFont(int points) override; void LineDraw(Point start, Point end, Stroke stroke) override; void PolyLine(const Point *pts, size_t npts, Stroke stroke) override; void Polygon(const Point *pts, size_t npts, FillStroke fillStroke) override; void RectangleDraw(PRectangle rc, FillStroke fillStroke) override; void RectangleFrame(PRectangle rc, Stroke stroke) override; void FillRectangle(PRectangle rc, Fill fill) override; void FillRectangleAligned(PRectangle rc, Fill fill) override; void FillRectangle(PRectangle rc, Surface &surfacePattern) override; void RoundedRectangle(PRectangle rc, FillStroke fillStroke) override; void AlphaRectangle(PRectangle rc, XYPOSITION cornerSize, FillStroke fillStroke) override; void GradientRectangle(PRectangle rc, const std::vector &stops, GradientOptions options) override; void DrawRGBAImage(PRectangle rc, int width, int height, const unsigned char *pixelsImage) override; void Ellipse(PRectangle rc, FillStroke fillStroke) override; void Stadium(PRectangle rc, FillStroke fillStroke, Ends ends) override; void Copy(PRectangle rc, Point from, Surface &surfaceSource) override; std::unique_ptr Layout(const IScreenLine *screenLine) override; void DrawTextBase(PRectangle rc, const Font *font_, XYPOSITION ybase, std::string_view text, ColourRGBA fore); void DrawTextNoClip(PRectangle rc, const Font *font_, XYPOSITION ybase, std::string_view text, ColourRGBA fore, ColourRGBA back) override; void DrawTextClipped(PRectangle rc, const Font *font_, XYPOSITION ybase, std::string_view text, ColourRGBA fore, ColourRGBA back) override; void DrawTextTransparent(PRectangle rc, const Font *font_, XYPOSITION ybase, std::string_view text, ColourRGBA fore) override; void MeasureWidths(const Font *font_, std::string_view text, XYPOSITION *positions) override; XYPOSITION WidthText(const Font *font_, std::string_view text) override; void DrawTextBaseUTF8(PRectangle rc, const Font *font_, XYPOSITION ybase, std::string_view text, ColourRGBA fore); void DrawTextNoClipUTF8(PRectangle rc, const Font *font_, XYPOSITION ybase, std::string_view text, ColourRGBA fore, ColourRGBA back) override; void DrawTextClippedUTF8(PRectangle rc, const Font *font_, XYPOSITION ybase, std::string_view text, ColourRGBA fore, ColourRGBA back) override; void DrawTextTransparentUTF8(PRectangle rc, const Font *font_, XYPOSITION ybase, std::string_view text, ColourRGBA fore) override; void MeasureWidthsUTF8(const Font *font_, std::string_view text, XYPOSITION *positions) override; XYPOSITION WidthTextUTF8(const Font *font_, std::string_view text) override; XYPOSITION Ascent(const Font *font_) override; XYPOSITION Descent(const Font *font_) override; XYPOSITION InternalLeading(const Font *font_) override; XYPOSITION Height(const Font *font_) override; XYPOSITION AverageCharWidth(const Font *font_) override; void SetClip(PRectangle rc) override; void PopClip() override; void FlushCachedState() override; void FlushDrawing() override; }; const Supports SupportsGTK[] = { Supports::LineDrawsFinal, Supports::FractionalStrokeWidth, Supports::TranslucentStroke, Supports::PixelModification, Supports::ThreadSafeMeasureWidths, }; } const char *CharacterSetID(CharacterSet characterSet) noexcept { switch (characterSet) { case CharacterSet::Ansi: return ""; case CharacterSet::Default: return "ISO-8859-1"; case CharacterSet::Baltic: return "ISO-8859-13"; case CharacterSet::ChineseBig5: return "BIG-5"; case CharacterSet::EastEurope: return "ISO-8859-2"; case CharacterSet::GB2312: return "CP936"; case CharacterSet::Greek: return "ISO-8859-7"; case CharacterSet::Hangul: return "CP949"; case CharacterSet::Mac: return "MACINTOSH"; case CharacterSet::Oem: return "ASCII"; case CharacterSet::Russian: return "KOI8-R"; case CharacterSet::Oem866: return "CP866"; case CharacterSet::Cyrillic: return "CP1251"; case CharacterSet::ShiftJis: return "SHIFT-JIS"; case CharacterSet::Symbol: return ""; case CharacterSet::Turkish: return "ISO-8859-9"; case CharacterSet::Johab: return "CP1361"; case CharacterSet::Hebrew: return "ISO-8859-8"; case CharacterSet::Arabic: return "ISO-8859-6"; case CharacterSet::Vietnamese: return ""; case CharacterSet::Thai: return "ISO-8859-11"; case CharacterSet::Iso8859_15: return "ISO-8859-15"; default: return ""; } } void SurfaceImpl::PenColourAlpha(ColourRGBA fore) noexcept { if (context) { cairo_set_source_rgba(context, fore.GetRedComponent(), fore.GetGreenComponent(), fore.GetBlueComponent(), fore.GetAlphaComponent()); } } void SurfaceImpl::SetConverter(CharacterSet characterSet_) { if (characterSet != characterSet_) { characterSet = characterSet_; conv.Open("UTF-8", CharacterSetID(characterSet), false); } } void SurfaceImpl::CairoRectangle(PRectangle rc) noexcept { cairo_rectangle(context, rc.left, rc.top, rc.Width(), rc.Height()); } SurfaceImpl::SurfaceImpl() noexcept { } SurfaceImpl::SurfaceImpl(cairo_t *context_, int width, int height, SurfaceMode mode_, WindowID wid) noexcept { if (height > 0 && width > 0) { cairo_surface_t *psurfContext = cairo_get_target(context_); surf.reset(cairo_surface_create_similar( psurfContext, CAIRO_CONTENT_COLOR_ALPHA, width, height)); cairoOwned.reset(cairo_create(surf.get())); context = cairoOwned.get(); pcontext.reset(gtk_widget_create_pango_context(PWidget(wid))); PLATFORM_ASSERT(pcontext); SetFractionalPositions(pcontext.get()); GetContextState(); layout.reset(pango_layout_new(pcontext.get())); PLATFORM_ASSERT(layout); cairo_rectangle(context, 0, 0, width, height); cairo_set_source_rgb(context, 1.0, 0, 0); cairo_fill(context); cairo_set_line_width(context, 1); inited = true; mode = mode_; } } void SurfaceImpl::Release() noexcept { et = EncodingType::singleByte; cairoOwned.reset(); context = nullptr; surf.reset(); layout.reset(); // fontOptions and language are owned by original context and don't need to be freed fontOptions = nullptr; language = nullptr; pcontext.reset(); conv.Close(); characterSet = static_cast(-1); inited = false; } bool SurfaceImpl::Initialised() { if (inited && context) { if (cairo_status(context) == CAIRO_STATUS_SUCCESS) { // Even when status is success, the target surface may have been // finished which may cause an assertion to fail crashing the application. // The cairo_surface_has_show_text_glyphs call checks the finished flag // and when set, sets the status to CAIRO_STATUS_SURFACE_FINISHED // which leads to warning messages instead of crashes. // Performing the check in this method as it is called rarely and has no // other side effects. cairo_surface_t *psurfContext = cairo_get_target(context); if (psurfContext) { cairo_surface_has_show_text_glyphs(psurfContext); } } return cairo_status(context) == CAIRO_STATUS_SUCCESS; } return inited; } void SurfaceImpl::GetContextState() noexcept { resolution = pango_cairo_context_get_resolution(pcontext.get()); direction = pango_context_get_base_dir(pcontext.get()); fontOptions = pango_cairo_context_get_font_options(pcontext.get()); language = pango_context_get_language(pcontext.get()); } UniquePangoContext SurfaceImpl::MeasuringContext() { UniquePangoFontMap fmMeasure(pango_cairo_font_map_get_default()); PLATFORM_ASSERT(fmMeasure); UniquePangoContext contextMeasure(pango_font_map_create_context(fmMeasure.release())); PLATFORM_ASSERT(contextMeasure); SetFractionalPositions(contextMeasure.get()); pango_cairo_context_set_resolution(contextMeasure.get(), resolution); pango_context_set_base_dir(contextMeasure.get(), direction); pango_cairo_context_set_font_options(contextMeasure.get(), fontOptions); pango_context_set_language(contextMeasure.get(), language); return contextMeasure; } void SurfaceImpl::Init(WindowID wid) { widSave = wid; Release(); PLATFORM_ASSERT(wid); // if we are only created from a window ID, we can't perform drawing context = nullptr; pcontext.reset(gtk_widget_create_pango_context(PWidget(wid))); PLATFORM_ASSERT(pcontext); SetFractionalPositions(pcontext.get()); GetContextState(); layout.reset(pango_layout_new(pcontext.get())); PLATFORM_ASSERT(layout); inited = true; } void SurfaceImpl::Init(SurfaceID sid, WindowID wid) { widSave = wid; PLATFORM_ASSERT(sid); Release(); PLATFORM_ASSERT(wid); cairoOwned.reset(cairo_reference(static_cast(sid))); context = cairoOwned.get(); pcontext.reset(gtk_widget_create_pango_context(PWidget(wid))); SetFractionalPositions(pcontext.get()); // update the Pango context in case sid isn't the widget's surface pango_cairo_update_context(context, pcontext.get()); GetContextState(); layout.reset(pango_layout_new(pcontext.get())); cairo_set_line_width(context, 1); inited = true; } std::unique_ptr SurfaceImpl::AllocatePixMap(int width, int height) { // widSave must be alive now so safe for creating a PangoContext return std::make_unique(context, width, height, mode, widSave); } void SurfaceImpl::SetMode(SurfaceMode mode_) { mode = mode_; if (mode.codePage == SC_CP_UTF8) { et = EncodingType::utf8; } else if (mode.codePage) { et = EncodingType::dbcs; } else { et = EncodingType::singleByte; } } int SurfaceImpl::SupportsFeature(Supports feature) noexcept { for (const Supports f : SupportsGTK) { if (f == feature) return 1; } return 0; } int SurfaceImpl::LogPixelsY() { return 72; } int SurfaceImpl::PixelDivisions() { // GTK uses device pixels. return 1; } int SurfaceImpl::DeviceHeightFont(int points) { const int logPix = LogPixelsY(); return (points * logPix + logPix / 2) / 72; } void SurfaceImpl::LineDraw(Point start, Point end, Stroke stroke) { PLATFORM_ASSERT(context); if (!context) return; PenColourAlpha(stroke.colour); cairo_set_line_width(context, stroke.width); cairo_move_to(context, start.x, start.y); cairo_line_to(context, end.x, end.y); cairo_stroke(context); } void SurfaceImpl::PolyLine(const Point *pts, size_t npts, Stroke stroke) { // TODO: set line joins and caps PLATFORM_ASSERT(context && npts > 1); if (!context) return; PenColourAlpha(stroke.colour); cairo_set_line_width(context, stroke.width); cairo_move_to(context, pts[0].x, pts[0].y); for (size_t i = 1; i < npts; i++) { cairo_line_to(context, pts[i].x, pts[i].y); } cairo_stroke(context); } void SurfaceImpl::Polygon(const Point *pts, size_t npts, FillStroke fillStroke) { PLATFORM_ASSERT(context); PenColourAlpha(fillStroke.fill.colour); cairo_move_to(context, pts[0].x, pts[0].y); for (size_t i = 1; i < npts; i++) { cairo_line_to(context, pts[i].x, pts[i].y); } cairo_close_path(context); cairo_fill_preserve(context); PenColourAlpha(fillStroke.stroke.colour); cairo_set_line_width(context, fillStroke.stroke.width); cairo_stroke(context); } void SurfaceImpl::RectangleDraw(PRectangle rc, FillStroke fillStroke) { if (context) { CairoRectangle(rc.Inset(fillStroke.stroke.width / 2)); PenColourAlpha(fillStroke.fill.colour); cairo_fill_preserve(context); PenColourAlpha(fillStroke.stroke.colour); cairo_set_line_width(context, fillStroke.stroke.width); cairo_stroke(context); } } void SurfaceImpl::RectangleFrame(PRectangle rc, Stroke stroke) { if (context) { CairoRectangle(rc.Inset(stroke.width / 2)); PenColourAlpha(stroke.colour); cairo_set_line_width(context, stroke.width); cairo_stroke(context); } } void SurfaceImpl::FillRectangle(PRectangle rc, Fill fill) { PenColourAlpha(fill.colour); if (context && (rc.left < maxCoordinate)) { // Protect against out of range CairoRectangle(rc); cairo_fill(context); } } void SurfaceImpl::FillRectangleAligned(PRectangle rc, Fill fill) { FillRectangle(PixelAlign(rc, 1), fill); } void SurfaceImpl::FillRectangle(PRectangle rc, Surface &surfacePattern) { SurfaceImpl &surfi = dynamic_cast(surfacePattern); if (context && surfi.surf) { // Tile pattern over rectangle cairo_set_source_surface(context, surfi.surf.get(), rc.left, rc.top); cairo_pattern_set_extend(cairo_get_source(context), CAIRO_EXTEND_REPEAT); cairo_rectangle(context, rc.left, rc.top, rc.Width(), rc.Height()); cairo_fill(context); } } void SurfaceImpl::RoundedRectangle(PRectangle rc, FillStroke fillStroke) { if (((rc.right - rc.left) > 4) && ((rc.bottom - rc.top) > 4)) { // Approximate a round rect with some cut off corners Point pts[] = { Point(rc.left + 2, rc.top), Point(rc.right - 2, rc.top), Point(rc.right, rc.top + 2), Point(rc.right, rc.bottom - 2), Point(rc.right - 2, rc.bottom), Point(rc.left + 2, rc.bottom), Point(rc.left, rc.bottom - 2), Point(rc.left, rc.top + 2), }; Polygon(pts, std::size(pts), fillStroke); } else { RectangleDraw(rc, fillStroke); } } static void PathRoundRectangle(cairo_t *context, double left, double top, double width, double height, double radius) noexcept { cairo_new_sub_path(context); cairo_arc(context, left + width - radius, top + radius, radius, -90 * degrees, 0 * degrees); cairo_arc(context, left + width - radius, top + height - radius, radius, 0 * degrees, 90 * degrees); cairo_arc(context, left + radius, top + height - radius, radius, 90 * degrees, 180 * degrees); cairo_arc(context, left + radius, top + radius, radius, 180 * degrees, 270 * degrees); cairo_close_path(context); } void SurfaceImpl::AlphaRectangle(PRectangle rc, XYPOSITION cornerSize, FillStroke fillStroke) { if (context && rc.Width() > 0) { const XYPOSITION halfStroke = fillStroke.stroke.width / 2.0; const XYPOSITION doubleStroke = fillStroke.stroke.width * 2.0; PenColourAlpha(fillStroke.fill.colour); if (cornerSize > 0) PathRoundRectangle(context, rc.left + fillStroke.stroke.width, rc.top + fillStroke.stroke.width, rc.Width() - doubleStroke, rc.Height() - doubleStroke, cornerSize); else cairo_rectangle(context, rc.left + fillStroke.stroke.width, rc.top + fillStroke.stroke.width, rc.Width() - doubleStroke, rc.Height() - doubleStroke); cairo_fill(context); PenColourAlpha(fillStroke.stroke.colour); if (cornerSize > 0) PathRoundRectangle(context, rc.left + halfStroke, rc.top + halfStroke, rc.Width() - fillStroke.stroke.width, rc.Height() - fillStroke.stroke.width, cornerSize); else cairo_rectangle(context, rc.left + halfStroke, rc.top + halfStroke, rc.Width() - fillStroke.stroke.width, rc.Height() - fillStroke.stroke.width); cairo_set_line_width(context, fillStroke.stroke.width); cairo_stroke(context); } } void SurfaceImpl::GradientRectangle(PRectangle rc, const std::vector &stops, GradientOptions options) { if (context) { cairo_pattern_t *pattern; switch (options) { case GradientOptions::leftToRight: pattern = cairo_pattern_create_linear(rc.left, rc.top, rc.right, rc.top); break; case GradientOptions::topToBottom: default: pattern = cairo_pattern_create_linear(rc.left, rc.top, rc.left, rc.bottom); break; } for (const ColourStop &stop : stops) { cairo_pattern_add_color_stop_rgba(pattern, stop.position, stop.colour.GetRedComponent(), stop.colour.GetGreenComponent(), stop.colour.GetBlueComponent(), stop.colour.GetAlphaComponent()); } cairo_rectangle(context, rc.left, rc.top, rc.Width(), rc.Height()); cairo_set_source(context, pattern); cairo_fill(context); cairo_pattern_destroy(pattern); } } void SurfaceImpl::DrawRGBAImage(PRectangle rc, int width, int height, const unsigned char *pixelsImage) { PLATFORM_ASSERT(context); if (rc.Width() > width) rc.left += (rc.Width() - width) / 2; rc.right = rc.left + width; if (rc.Height() > height) rc.top += (rc.Height() - height) / 2; rc.bottom = rc.top + height; const int stride = cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, width); const int ucs = stride * height; std::vector image(ucs); for (ptrdiff_t iy=0; iy(static_cast(ends) & 0xf); const Ends rightSide = static_cast(static_cast(ends) & 0xf0); switch (leftSide) { case Ends::leftFlat: cairo_move_to(context, rc.left + halfStroke, rc.top + halfStroke); cairo_line_to(context, rc.left + halfStroke, rc.bottom - halfStroke); break; case Ends::leftAngle: cairo_move_to(context, rcInner.left + halfStroke, rc.top + halfStroke); cairo_line_to(context, rc.left + halfStroke, rc.Centre().y); cairo_line_to(context, rcInner.left + halfStroke, rc.bottom - halfStroke); break; case Ends::semiCircles: default: cairo_move_to(context, rcInner.left + halfStroke, rc.top + halfStroke); cairo_arc_negative(context, rcInner.left + halfStroke, midLine, radius, 270 * degrees, 90 * degrees); break; } switch (rightSide) { case Ends::rightFlat: cairo_line_to(context, rc.right - halfStroke, rc.bottom - halfStroke); cairo_line_to(context, rc.right - halfStroke, rc.top + halfStroke); break; case Ends::rightAngle: cairo_line_to(context, rcInner.right - halfStroke, rc.bottom - halfStroke); cairo_line_to(context, rc.right - halfStroke, rc.Centre().y); cairo_line_to(context, rcInner.right - halfStroke, rc.top + halfStroke); break; case Ends::semiCircles: default: cairo_line_to(context, rcInner.right - halfStroke, rc.bottom - halfStroke); cairo_arc_negative(context, rcInner.right - halfStroke, midLine, radius, 90 * degrees, 270 * degrees); break; } // Close the path to enclose it for stroking and for filling, then draw it cairo_close_path(context); PenColourAlpha(fillStroke.fill.colour); cairo_fill_preserve(context); PenColourAlpha(fillStroke.stroke.colour); cairo_set_line_width(context, fillStroke.stroke.width); cairo_stroke(context); } void SurfaceImpl::Copy(PRectangle rc, Point from, Surface &surfaceSource) { SurfaceImpl &surfi = static_cast(surfaceSource); const bool canDraw = surfi.surf != nullptr; if (canDraw) { PLATFORM_ASSERT(context); cairo_set_source_surface(context, surfi.surf.get(), rc.left - from.x, rc.top - from.y); cairo_rectangle(context, rc.left, rc.top, rc.Width(), rc.Height()); cairo_fill(context); } } std::unique_ptr SurfaceImpl::Layout(const IScreenLine *) { return {}; } std::string UTF8FromLatin1(std::string_view text) { std::string utfForm(text.length()*2 + 1, '\0'); size_t lenU = 0; for (const char ch : text) { const unsigned char uch = ch; if (uch < 0x80) { utfForm[lenU++] = uch; } else { utfForm[lenU++] = static_cast(0xC0 | (uch >> 6)); utfForm[lenU++] = static_cast(0x80 | (uch & 0x3f)); } } utfForm.resize(lenU); return utfForm; } namespace { std::string UTF8FromIconv(const Converter &conv, std::string_view text) { if (conv) { std::string utfForm(text.length()*3+1, '\0'); char *pin = const_cast(text.data()); gsize inLeft = text.length(); char *putf = &utfForm[0]; char *pout = putf; gsize outLeft = text.length()*3+1; const gsize conversions = conv.Convert(&pin, &inLeft, &pout, &outLeft); if (conversions != sizeFailure) { *pout = '\0'; utfForm.resize(pout - putf); return utfForm; } } return std::string(); } // Work out how many bytes are in a character by trying to convert using iconv, // returning the first length that succeeds. size_t MultiByteLenFromIconv(const Converter &conv, const char *s, size_t len) noexcept { for (size_t lenMB=1; (lenMB<4) && (lenMB <= len); lenMB++) { char wcForm[2] {}; char *pin = const_cast(s); gsize inLeft = lenMB; char *pout = wcForm; gsize outLeft = 2; const gsize conversions = conv.Convert(&pin, &inLeft, &pout, &outLeft); if (conversions != sizeFailure) { return lenMB; } } return 1; } } void SurfaceImpl::DrawTextBase(PRectangle rc, const Font *font_, XYPOSITION ybase, std::string_view text, ColourRGBA fore) { if (context) { PenColourAlpha(fore); const XYPOSITION xText = rc.left; if (PFont(font_)->fd) { if (et == EncodingType::utf8) { LayoutSetText(layout.get(), text); } else { SetConverter(PFont(font_)->characterSet); std::string utfForm = UTF8FromIconv(conv, text); if (utfForm.empty()) { // iconv failed so treat as Latin1 utfForm = UTF8FromLatin1(text); } LayoutSetText(layout.get(), utfForm); } pango_layout_set_font_description(layout.get(), PFont(font_)->fd.get()); pango_cairo_update_layout(context, layout.get()); PangoLayoutLine *pll = pango_layout_get_line_readonly(layout.get(), 0); cairo_move_to(context, xText, ybase); pango_cairo_show_layout_line(context, pll); } } } void SurfaceImpl::DrawTextNoClip(PRectangle rc, const Font *font_, XYPOSITION ybase, std::string_view text, ColourRGBA fore, ColourRGBA back) { FillRectangleAligned(rc, back); DrawTextBase(rc, font_, ybase, text, fore); } // On GTK+, exactly same as DrawTextNoClip void SurfaceImpl::DrawTextClipped(PRectangle rc, const Font *font_, XYPOSITION ybase, std::string_view text, ColourRGBA fore, ColourRGBA back) { FillRectangleAligned(rc, back); DrawTextBase(rc, font_, ybase, text, fore); } void SurfaceImpl::DrawTextTransparent(PRectangle rc, const Font *font_, XYPOSITION ybase, std::string_view text, ColourRGBA fore) { // Avoid drawing spaces in transparent mode for (size_t i=0; i(text.length())) { LayoutSetText(layout, text); iter.reset(pango_layout_get_iter(layout)); curIndex = pango_layout_iter_get_index(iter.get()); pango_layout_iter_get_cluster_extents(iter.get(), nullptr, &pos); } void Next() noexcept { positionStart = position; if (pango_layout_iter_next_cluster(iter.get())) { pango_layout_iter_get_cluster_extents(iter.get(), nullptr, &pos); position = pango_units_to_double(pos.x); curIndex = pango_layout_iter_get_index(iter.get()); } else { finished = true; position = pango_units_to_double(pos.x + pos.width); curIndex = pango_layout_iter_get_index(iter.get()); } distance = position - positionStart; } }; // Something has gone wrong so set all the characters as equally spaced. void EquallySpaced(PangoLayout *layout, XYPOSITION *positions, size_t lenPositions) { int widthLayout = 0; pango_layout_get_size(layout, &widthLayout, nullptr); const XYPOSITION widthTotal = pango_units_to_double(widthLayout); for (size_t bytePos=0; bytePosfd) { UniquePangoContext contextMeasure = MeasuringContext(); UniquePangoLayout layoutMeasure(pango_layout_new(contextMeasure.get())); PLATFORM_ASSERT(layoutMeasure); pango_layout_set_font_description(layoutMeasure.get(), PFont(font_)->fd.get()); if (et == EncodingType::utf8) { // Simple and direct as UTF-8 is native Pango encoding ClusterIterator iti(layoutMeasure.get(), text); int i = iti.curIndex; if (i != 0) { // Unexpected start to iteration, could be bidirectional text EquallySpaced(layoutMeasure.get(), positions, text.length()); return; } while (!iti.finished) { iti.Next(); const int places = iti.curIndex - i; while (i < iti.curIndex) { // Evenly distribute space among bytes of this cluster. // Would be better to find number of characters and then // divide evenly between characters with each byte of a character // being at the same position. positions[i] = iti.position - (iti.curIndex - 1 - i) * iti.distance / places; i++; } } PLATFORM_ASSERT(static_cast(i) == text.length()); } else { int positionsCalculated = 0; const char *charSetID = CharacterSetID(PFont(font_)->characterSet); std::string utfForm; { gsize bytesRead = 0; gsize bytesWritten = 0; GError *error = nullptr; UniqueStr textInUTF8(g_convert(text.data(), text.length(), "UTF-8", charSetID, &bytesRead, &bytesWritten, &error)); if ((bytesWritten > 0) && (bytesRead == text.length()) && !error) { // Extra allocation here but avoiding it makes code more complex utfForm.assign(textInUTF8.get(), bytesWritten); } if (error) { #ifdef DEBUG fprintf(stderr, "MeasureWidths: %s.\n", error->message); #endif g_error_free(error); } } if (et == EncodingType::dbcs) { if (!utfForm.empty()) { // Convert to UTF-8 so can ask Pango for widths, then // Loop through UTF-8 and DBCS forms, taking account of different // character byte lengths. Converter convMeasure("UCS-2", charSetID, false); int i = 0; ClusterIterator iti(layoutMeasure.get(), utfForm); int clusterStart = iti.curIndex; if (clusterStart != 0) { // Unexpected start to iteration, could be bidirectional text EquallySpaced(layoutMeasure.get(), positions, text.length()); return; } while (!iti.finished) { iti.Next(); const int clusterEnd = iti.curIndex; const int places = g_utf8_strlen(utfForm.data() + clusterStart, clusterEnd - clusterStart); int place = 1; while (clusterStart < clusterEnd) { size_t lenChar = MultiByteLenFromIconv(convMeasure, text.data()+i, text.length()-i); while (lenChar--) { positions[i++] = iti.position - (places - place) * iti.distance / places; positionsCalculated++; } clusterStart += UTF8BytesOfLead[static_cast(utfForm[clusterStart])]; place++; } } PLATFORM_ASSERT(static_cast(i) == text.length()); } } if (positionsCalculated < 1) { const size_t lenPositions = text.length(); // Either 8-bit or DBCS conversion failed so treat as 8-bit. const bool rtlCheck = PFont(font_)->characterSet == CharacterSet::Hebrew || PFont(font_)->characterSet == CharacterSet::Arabic; if (utfForm.empty()) { utfForm = UTF8FromLatin1(text); #ifdef DEBUG fprintf(stderr, "MeasureWidths: Fall back to Latin1 [%s]\n", utfForm.c_str()); #endif } size_t i = 0; // Each 8-bit input character may take 1 or 2 bytes in UTF-8 // and groups of up to 3 may be repreHTTP/1.1 200 OK Connection: keep-alive Connection: keep-alive Content-Disposition: inline; filename="PlatGTK.cxx" Content-Disposition: inline; filename="PlatGTK.cxx" Content-Length: 73051 Content-Length: 73051 Content-Security-Policy: default-src 'none' Content-Security-Policy: default-src 'none' Content-Type: text/plain; charset=UTF-8 Content-Type: text/plain; charset=UTF-8 Date: Tue, 21 Oct 2025 12:00:24 UTC ETag: "e939b87fbcb61338bb1bbdeedffc3cb65e16b2b9" ETag: "e939b87fbcb61338bb1bbdeedffc3cb65e16b2b9" Expires: Fri, 19 Oct 2035 12:00:24 GMT Expires: Fri, 19 Oct 2035 12:00:25 GMT Last-Modified: Tue, 21 Oct 2025 12:00:24 GMT Last-Modified: Tue, 21 Oct 2025 12:00:25 GMT Server: OpenBSD httpd Server: OpenBSD httpd X-Content-Type-Options: nosniff X-Content-Type-Options: nosniff // Scintilla source code edit control // PlatGTK.cxx - implementation of platform facilities on GTK+/Linux // Copyright 1998-2004 by Neil Hodgson // The License.txt file describes the conditions under which this software may be distributed. #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if defined(GDK_WINDOWING_WAYLAND) #include #endif #include "ScintillaTypes.h" #include "ScintillaMessages.h" #include "Debugging.h" #include "Geometry.h" #include "Platform.h" #include "Scintilla.h" #include "ScintillaWidget.h" #include "XPM.h" #include "UniConversion.h" #include "Wrappers.h" #include "Converter.h" #ifdef _MSC_VER // Ignore unreferenced local functions in GTK+ headers #pragma warning(disable: 4505) #endif using namespace Scintilla; using namespace Scintilla::Internal; namespace { constexpr double kPi = 3.14159265358979323846; constexpr double degrees = kPi / 180.0; struct IntegerRectangle { int left; int top; int right; int bottom; explicit IntegerRectangle(PRectangle rc) noexcept : left(static_cast(rc.left)), top(static_cast(rc.top)), right(static_cast(rc.right)), bottom(static_cast(rc.bottom)) { } int Width() const noexcept { return right - left; } int Height() const noexcept { return bottom - top; } }; GtkWidget *PWidget(WindowID wid) noexcept { return static_cast(wid); } void SetFractionalPositions([[maybe_unused]] PangoContext *pcontext) noexcept { #if PANGO_VERSION_CHECK(1,44,3) pango_context_set_round_glyph_positions(pcontext, FALSE); #endif } void LayoutSetText(PangoLayout *layout, std::string_view text) noexcept { pango_layout_set_text(layout, text.data(), static_cast(text.length())); } enum class EncodingType { singleByte, utf8, dbcs }; // Holds a PangoFontDescription*. class FontHandle : public Font { public: UniquePangoFontDescription fd; CharacterSet characterSet; explicit FontHandle(const FontParameters &fp) : fd(pango_font_description_new()), characterSet(fp.characterSet) { if (fd) { pango_font_description_set_family(fd.get(), (fp.faceName[0] == '!') ? fp.faceName + 1 : fp.faceName); pango_font_description_set_size(fd.get(), pango_units_from_double(fp.size)); pango_font_description_set_weight(fd.get(), static_cast(fp.weight)); pango_font_description_set_style(fd.get(), fp.italic ? PANGO_STYLE_ITALIC : PANGO_STYLE_NORMAL); } } ~FontHandle() override = default; }; // X has a 16 bit coordinate space, so stop drawing here to avoid wrapping constexpr int maxCoordinate = 32000; const FontHandle *PFont(const Font *f) noexcept { return dynamic_cast(f); } } std::shared_ptr Font::Allocate(const FontParameters &fp) { return std::make_shared(fp); } namespace Scintilla { // SurfaceID is a cairo_t* class SurfaceImpl : public Surface { SurfaceMode mode; EncodingType et= EncodingType::singleByte; WindowID widSave = nullptr; cairo_t *context = nullptr; UniqueCairo cairoOwned; UniqueCairoSurface surf; bool inited = false; UniquePangoContext pcontext; double resolution = 1.0; PangoDirection direction = PANGO_DIRECTION_LTR; const cairo_font_options_t *fontOptions = nullptr; PangoLanguage *language = nullptr; UniquePangoLayout layout; Converter conv; CharacterSet characterSet = static_cast(-1); void PenColourAlpha(ColourRGBA fore) noexcept; void SetConverter(CharacterSet characterSet_); void CairoRectangle(PRectangle rc) noexcept; public: SurfaceImpl() noexcept; SurfaceImpl(cairo_t *context_, int width, int height, SurfaceMode mode_, WindowID wid) noexcept; // Deleted so SurfaceImpl objects can not be copied. SurfaceImpl(const SurfaceImpl&) = delete; SurfaceImpl(SurfaceImpl&&) = delete; SurfaceImpl&operator=(const SurfaceImpl&) = delete; SurfaceImpl&operator=(SurfaceImpl&&) = delete; ~SurfaceImpl() override = default; void GetContextState() noexcept; UniquePangoContext MeasuringContext(); void Init(WindowID wid) override; void Init(SurfaceID sid, WindowID wid) override; std::unique_ptr AllocatePixMap(int width, int height) override; void SetMode(SurfaceMode mode_) override; void Release() noexcept override; int SupportsFeature(Supports feature) noexcept override; bool Initialised() override; int LogPixelsY() override; int PixelDivisions() override; int DeviceHeightFont(int points) override; void LineDraw(Point start, Point end, Stroke stroke) override; void PolyLine(const Point *pts, size_t npts, Stroke stroke) override; void Polygon(const Point *pts, size_t npts, FillStroke fillStroke) override; void RectangleDraw(PRectangle rc, FillStroke fillStroke) override; void RectangleFrame(PRectangle rc, Stroke stroke) override; void FillRectangle(PRectangle rc, Fill fill) override; void FillRectangleAligned(PRectangle rc, Fill fill) override; void FillRectangle(PRectangle rc, Surface &surfacePattern) override; void RoundedRectangle(PRectangle rc, FillStroke fillStroke) override; void AlphaRectangle(PRectangle rc, XYPOSITION cornerSize, FillStroke fillStroke) override; void GradientRectangle(PRectangle rc, const std::vector &stops, GradientOptions options) override; void DrawRGBAImage(PRectangle rc, int width, int height, const unsigned char *pixelsImage) override; void Ellipse(PRectangle rc, FillStroke fillStroke) override; void Stadium(PRectangle rc, FillStroke fillStroke, Ends ends) override; void Copy(PRectangle rc, Point from, Surface &surfaceSource) override; std::unique_ptr Layout(const IScreenLine *screenLine) override; void DrawTextBase(PRectangle rc, const Font *font_, XYPOSITION ybase, std::string_view text, ColourRGBA fore); void DrawTextNoClip(PRectangle rc, const Font *font_, XYPOSITION ybase, std::string_view text, ColourRGBA fore, ColourRGBA back) override; void DrawTextClipped(PRectangle rc, const Font *font_, XYPOSITION ybase, std::string_view text, ColourRGBA fore, ColourRGBA back) override; void DrawTextTransparent(PRectangle rc, const Font *font_, XYPOSITION ybase, std::string_view text, ColourRGBA fore) override; void MeasureWidths(const Font *font_, std::string_view text, XYPOSITION *positions) override; XYPOSITION WidthText(const Font *font_, std::string_view text) override; void DrawTextBaseUTF8(PRectangle rc, const Font *font_, XYPOSITION ybase, std::string_view text, ColourRGBA fore); void DrawTextNoClipUTF8(PRectangle rc, const Font *font_, XYPOSITION ybase, std::string_view text, ColourRGBA fore, ColourRGBA back) override; void DrawTextClippedUTF8(PRectangle rc, const Font *font_, XYPOSITION ybase, std::string_view text, ColourRGBA fore, ColourRGBA back) override; void DrawTextTransparentUTF8(PRectangle rc, const Font *font_, XYPOSITION ybase, std::string_view text, ColourRGBA fore) override; void MeasureWidthsUTF8(const Font *font_, std::string_view text, XYPOSITION *positions) override; XYPOSITION WidthTextUTF8(const Font *font_, std::string_view text) override; XYPOSITION Ascent(const Font *font_) override; XYPOSITION Descent(const Font *font_) override; XYPOSITION InternalLeading(const Font *font_) override; XYPOSITION Height(const Font *font_) override; XYPOSITION AverageCharWidth(const Font *font_) override; void SetClip(PRectangle rc) override; void PopClip() override; void FlushCachedState() override; void FlushDrawing() override; }; const Supports SupportsGTK[] = { Supports::LineDrawsFinal, Supports::FractionalStrokeWidth, Supports::TranslucentStroke, Supports::PixelModification, Supports::ThreadSafeMeasureWidths, }; } const char *CharacterSetID(CharacterSet characterSet) noexcept { switch (characterSet) { case CharacterSet::Ansi: return ""; case CharacterSet::Default: return "ISO-8859-1"; case CharacterSet::Baltic: return "ISO-8859-13"; case CharacterSet::ChineseBig5: return "BIG-5"; case CharacterSet::EastEurope: return "ISO-8859-2"; case CharacterSet::GB2312: return "CP936"; case CharacterSet::Greek: return "ISO-8859-7"; case CharacterSet::Hangul: return "CP949"; case CharacterSet::Mac: return "MACINTOSH"; case CharacterSet::Oem: return "ASCII"; case CharacterSet::Russian: return "KOI8-R"; case CharacterSet::Oem866: return "CP866"; case CharacterSet::Cyrillic: return "CP1251"; case CharacterSet::ShiftJis: return "SHIFT-JIS"; case CharacterSet::Symbol: return ""; case CharacterSet::Turkish: return "ISO-8859-9"; case CharacterSet::Johab: return "CP1361"; case CharacterSet::Hebrew: return "ISO-8859-8"; case CharacterSet::Arabic: return "ISO-8859-6"; case CharacterSet::Vietnamese: return ""; case CharacterSet::Thai: return "ISO-8859-11"; case CharacterSet::Iso8859_15: return "ISO-8859-15"; default: return ""; } } void SurfaceImpl::PenColourAlpha(ColourRGBA fore) noexcept { if (context) { cairo_set_source_rgba(context, fore.GetRedComponent(), fore.GetGreenComponent(), fore.GetBlueComponent(), fore.GetAlphaComponent()); } } void SurfaceImpl::SetConverter(CharacterSet characterSet_) { if (characterSet != characterSet_) { characterSet = characterSet_; conv.Open("UTF-8", CharacterSetID(characterSet), false); } } void SurfaceImpl::CairoRectangle(PRectangle rc) noexcept { cairo_rectangle(context, rc.left, rc.top, rc.Width(), rc.Height()); } SurfaceImpl::SurfaceImpl() noexcept { } SurfaceImpl::SurfaceImpl(cairo_t *context_, int width, int height, SurfaceMode mode_, WindowID wid) noexcept { if (height > 0 && width > 0) { cairo_surface_t *psurfContext = cairo_get_target(context_); surf.reset(cairo_surface_create_similar( psurfContext, CAIRO_CONTENT_COLOR_ALPHA, width, height)); cairoOwned.reset(cairo_create(surf.get())); context = cairoOwned.get(); pcontext.reset(gtk_widget_create_pango_context(PWidget(wid))); PLATFORM_ASSERT(pcontext); SetFractionalPositions(pcontext.get()); GetContextState(); layout.reset(pango_layout_new(pcontext.get())); PLATFORM_ASSERT(layout); cairo_rectangle(context, 0, 0, width, height); cairo_set_source_rgb(context, 1.0, 0, 0); cairo_fill(context); cairo_set_line_width(context, 1); inited = true; mode = mode_; } } void SurfaceImpl::Release() noexcept { et = EncodingType::singleByte; cairoOwned.reset(); context = nullptr; surf.reset(); layout.reset(); // fontOptions and language are owned by original context and don't need to be freed fontOptions = nullptr; language = nullptr; pcontext.reset(); conv.Close(); characterSet = static_cast(-1); inited = false; } bool SurfaceImpl::Initialised() { if (inited && context) { if (cairo_status(context) == CAIRO_STATUS_SUCCESS) { // Even when status is success, the target surface may have been // finished which may cause an assertion to fail crashing the application. // The cairo_surface_has_show_text_glyphs call checks the finished flag // and when set, sets the status to CAIRO_STATUS_SURFACE_FINISHED // which leads to warning messages instead of crashes. // Performing the check in this method as it is called rarely and has no // other side effects. cairo_surface_t *psurfContext = cairo_get_target(context); if (psurfContext) { cairo_surface_has_show_text_glyphs(psurfContext); } } return cairo_status(context) == CAIRO_STATUS_SUCCESS; } return inited; } void SurfaceImpl::GetContextState() noexcept { resolution = pango_cairo_context_get_resolution(pcontext.get()); direction = pango_context_get_base_dir(pcontext.get()); fontOptions = pango_cairo_context_get_font_options(pcontext.get()); language = pango_context_get_language(pcontext.get()); } UniquePangoContext SurfaceImpl::MeasuringContext() { UniquePangoFontMap fmMeasure(pango_cairo_font_map_get_default()); PLATFORM_ASSERT(fmMeasure); UniquePangoContext contextMeasure(pango_font_map_create_context(fmMeasure.release())); PLATFORM_ASSERT(contextMeasure); SetFractionalPositions(contextMeasure.get()); pango_cairo_context_set_resolution(contextMeasure.get(), resolution); pango_context_set_base_dir(contextMeasure.get(), direction); pango_cairo_context_set_font_options(contextMeasure.get(), fontOptions); pango_context_set_language(contextMeasure.get(), language); return contextMeasure; } void SurfaceImpl::Init(WindowID wid) { widSave = wid; Release(); PLATFORM_ASSERT(wid); // if we are only created from a window ID, we can't perform drawing context = nullptr; pcontext.reset(gtk_widget_create_pango_context(PWidget(wid))); PLATFORM_ASSERT(pcontext); SetFractionalPositions(pcontext.get()); GetContextState(); layout.reset(pango_layout_new(pcontext.get())); PLATFORM_ASSERT(layout); inited = true; } void SurfaceImpl::Init(SurfaceID sid, WindowID wid) { widSave = wid; PLATFORM_ASSERT(sid); Release(); PLATFORM_ASSERT(wid); cairoOwned.reset(cairo_reference(static_cast(sid))); context = cairoOwned.get(); pcontext.reset(gtk_widget_create_pango_context(PWidget(wid))); SetFractionalPositions(pcontext.get()); // update the Pango context in case sid isn't the widget's surface pango_cairo_update_context(context, pcontext.get()); GetContextState(); layout.reset(pango_layout_new(pcontext.get())); cairo_set_line_width(context, 1); inited = true; } std::unique_ptr SurfaceImpl::AllocatePixMap(int width, int height) { // widSave must be alive now so safe for creating a PangoContext return std::make_unique(context, width, height, mode, widSave); } void SurfaceImpl::SetMode(SurfaceMode mode_) { mode = mode_; if (mode.codePage == SC_CP_UTF8) { et = EncodingType::utf8; } else if (mode.codePage) { et = EncodingType::dbcs; } else { et = EncodingType::singleByte; } } int SurfaceImpl::SupportsFeature(Supports feature) noexcept { for (const Supports f : SupportsGTK) { if (f == feature) return 1; } return 0; } int SurfaceImpl::LogPixelsY() { return 72; } int SurfaceImpl::PixelDivisions() { // GTK uses device pixels. return 1; } int SurfaceImpl::DeviceHeightFont(int points) { const int logPix = LogPixelsY(); return (points * logPix + logPix / 2) / 72; } void SurfaceImpl::LineDraw(Point start, Point end, Stroke stroke) { PLATFORM_ASSERT(context); if (!context) return; PenColourAlpha(stroke.colour); cairo_set_line_width(context, stroke.width); cairo_move_to(context, start.x, start.y); cairo_line_to(context, end.x, end.y); cairo_stroke(context); } void SurfaceImpl::PolyLine(const Point *pts, size_t npts, Stroke stroke) { // TODO: set line joins and caps PLATFORM_ASSERT(context && npts > 1); if (!context) return; PenColourAlpha(stroke.colour); cairo_set_line_width(context, stroke.width); cairo_move_to(context, pts[0].x, pts[0].y); for (size_t i = 1; i < npts; i++) { cairo_line_to(context, pts[i].x, pts[i].y); } cairo_stroke(context); } void SurfaceImpl::Polygon(const Point *pts, size_t npts, FillStroke fillStroke) { PLATFORM_ASSERT(context); PenColourAlpha(fillStroke.fill.colour); cairo_move_to(context, pts[0].x, pts[0].y); for (size_t i = 1; i < npts; i++) { cairo_line_to(context, pts[i].x, pts[i].y); } cairo_close_path(context); cairo_fill_preserve(context); PenColourAlpha(fillStroke.stroke.colour); cairo_set_line_width(context, fillStroke.stroke.width); cairo_stroke(context); } void SurfaceImpl::RectangleDraw(PRectangle rc, FillStroke fillStroke) { if (context) { CairoRectangle(rc.Inset(fillStroke.stroke.width / 2)); PenColourAlpha(fillStroke.fill.colour); cairo_fill_preserve(context); PenColourAlpha(fillStroke.stroke.colour); cairo_set_line_width(context, fillStroke.stroke.width); cairo_stroke(context); } } void SurfaceImpl::RectangleFrame(PRectangle rc, Stroke stroke) { if (context) { CairoRectangle(rc.Inset(stroke.width / 2)); PenColourAlpha(stroke.colour); cairo_set_line_width(context, stroke.width); cairo_stroke(context); } } void SurfaceImpl::FillRectangle(PRectangle rc, Fill fill) { PenColourAlpha(fill.colour); if (context && (rc.left < maxCoordinate)) { // Protect against out of range CairoRectangle(rc); cairo_fill(context); } } void SurfaceImpl::FillRectangleAligned(PRectangle rc, Fill fill) { FillRectangle(PixelAlign(rc, 1), fill); } void SurfaceImpl::FillRectangle(PRectangle rc, Surface &surfacePattern) { SurfaceImpl &surfi = dynamic_cast(surfacePattern); if (context && surfi.surf) { // Tile pattern over rectangle cairo_set_source_surface(context, surfi.surf.get(), rc.left, rc.top); cairo_pattern_set_extend(cairo_get_source(context), CAIRO_EXTEND_REPEAT); cairo_rectangle(context, rc.left, rc.top, rc.Width(), rc.Height()); cairo_fill(context); } } void SurfaceImpl::RoundedRectangle(PRectangle rc, FillStroke fillStroke) { if (((rc.right - rc.left) > 4) && ((rc.bottom - rc.top) > 4)) { // Approximate a round rect with some cut off corners Point pts[] = { Point(rc.left + 2, rc.top), Point(rc.right - 2, rc.top), Point(rc.right, rc.top + 2), Point(rc.right, rc.bottom - 2), Point(rc.right - 2, rc.bottom), Point(rc.left + 2, rc.bottom), Point(rc.left, rc.bottom - 2), Point(rc.left, rc.top + 2), }; Polygon(pts, std::size(pts), fillStroke); } else { RectangleDraw(rc, fillStroke); } } static void PathRoundRectangle(cairo_t *context, double left, double top, double width, double height, double radius) noexcept { cairo_new_sub_path(context); cairo_arc(context, left + width - radius, top + radius, radius, -90 * degrees, 0 * degrees); cairo_arc(context, left + width - radius, top + height - radius, radius, 0 * degrees, 90 * degrees); cairo_arc(context, left + radius, top + height - radius, radius, 90 * degrees, 180 * degrees); cairo_arc(context, left + radius, top + radius, radius, 180 * degrees, 270 * degrees); cairo_close_path(context); } void SurfaceImpl::AlphaRectangle(PRectangle rc, XYPOSITION cornerSize, FillStroke fillStroke) { if (context && rc.Width() > 0) { const XYPOSITION halfStroke = fillStroke.stroke.width / 2.0; const XYPOSITION doubleStroke = fillStroke.stroke.width * 2.0; PenColourAlpha(fillStroke.fill.colour); if (cornerSize > 0) PathRoundRectangle(context, rc.left + fillStroke.stroke.width, rc.top + fillStroke.stroke.width, rc.Width() - doubleStroke, rc.Height() - doubleStroke, cornerSize); else cairo_rectangle(context, rc.left + fillStroke.stroke.width, rc.top + fillStroke.stroke.width, rc.Width() - doubleStroke, rc.Height() - doubleStroke); cairo_fill(context); PenColourAlpha(fillStroke.stroke.colour); if (cornerSize > 0) PathRoundRectangle(context, rc.left + halfStroke, rc.top + halfStroke, rc.Width() - fillStroke.stroke.width, rc.Height() - fillStroke.stroke.width, cornerSize); else cairo_rectangle(context, rc.left + halfStroke, rc.top + halfStroke, rc.Width() - fillStroke.stroke.width, rc.Height() - fillStroke.stroke.width); cairo_set_line_width(context, fillStroke.stroke.width); cairo_stroke(context); } } void SurfaceImpl::GradientRectangle(PRectangle rc, const std::vector &stops, GradientOptions options) { if (context) { cairo_pattern_t *pattern; switch (options) { case GradientOptions::leftToRight: pattern = cairo_pattern_create_linear(rc.left, rc.top, rc.right, rc.top); break; case GradientOptions::topToBottom: default: pattern = cairo_pattern_create_linear(rc.left, rc.top, rc.left, rc.bottom); break; } for (const ColourStop &stop : stops) { cairo_pattern_add_color_stop_rgba(pattern, stop.position, stop.colour.GetRedComponent(), stop.colour.GetGreenComponent(), stop.colour.GetBlueComponent(), stop.colour.GetAlphaComponent()); } cairo_rectangle(context, rc.left, rc.top, rc.Width(), rc.Height()); cairo_set_source(context, pattern); cairo_fill(context); cairo_pattern_destroy(pattern); } } void SurfaceImpl::DrawRGBAImage(PRectangle rc, int width, int height, const unsigned char *pixelsImage) { PLATFORM_ASSERT(context); if (rc.Width() > width) rc.left += (rc.Width() - width) / 2; rc.right = rc.left + width; if (rc.Height() > height) rc.top += (rc.Height() - height) / 2; rc.bottom = rc.top + height; const int stride = cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, width); const int ucs = stride * height; std::vector image(ucs); for (ptrdiff_t iy=0; iy(static_cast(ends) & 0xf); const Ends rightSide = static_cast(static_cast(ends) & 0xf0); switch (leftSide) { case Ends::leftFlat: cairo_move_to(context, rc.left + halfStroke, rc.top + halfStroke); cairo_line_to(context, rc.left + halfStroke, rc.bottom - halfStroke); break; case Ends::leftAngle: cairo_move_to(context, rcInner.left + halfStroke, rc.top + halfStroke); cairo_line_to(context, rc.left + halfStroke, rc.Centre().y); cairo_line_to(context, rcInner.left + halfStroke, rc.bottom - halfStroke); break; case Ends::semiCircles: default: cairo_move_to(context, rcInner.left + halfStroke, rc.top + halfStroke); cairo_arc_negative(context, rcInner.left + halfStroke, midLine, radius, 270 * degrees, 90 * degrees); break; } switch (rightSide) { case Ends::rightFlat: cairo_line_to(context, rc.right - halfStroke, rc.bottom - halfStroke); cairo_line_to(context, rc.right - halfStroke, rc.top + halfStroke); break; case Ends::rightAngle: cairo_line_to(context, rcInner.right - halfStroke, rc.bottom - halfStroke); cairo_line_to(context, rc.right - halfStroke, rc.Centre().y); cairo_line_to(context, rcInner.right - halfStroke, rc.top + halfStroke); break; case Ends::semiCircles: default: cairo_line_to(context, rcInner.right - halfStroke, rc.bottom - halfStroke); cairo_arc_negative(context, rcInner.right - halfStroke, midLine, radius, 90 * degrees, 270 * degrees); break; } // Close the path to enclose it for stroking and for filling, then draw it cairo_close_path(context); PenColourAlpha(fillStroke.fill.colour); cairo_fill_preserve(context); PenColourAlpha(fillStroke.stroke.colour); cairo_set_line_width(context, fillStroke.stroke.width); cairo_stroke(context); } void SurfaceImpl::Copy(PRectangle rc, Point from, Surface &surfaceSource) { SurfaceImpl &surfi = static_cast(surfaceSource); const bool canDraw = surfi.surf != nullptr; if (canDraw) { PLATFORM_ASSERT(context); cairo_set_source_surface(context, surfi.surf.get(), rc.left - from.x, rc.top - from.y); cairo_rectangle(context, rc.left, rc.top, rc.Width(), rc.Height()); cairo_fill(context); } } std::unique_ptr SurfaceImpl::Layout(const IScreenLine *) { return {}; } std::string UTF8FromLatin1(std::string_view text) { std::string utfForm(text.length()*2 + 1, '\0'); size_t lenU = 0; for (const char ch : text) { const unsigned char uch = ch; if (uch < 0x80) { utfForm[lenU++] = uch; } else { utfForm[lenU++] = static_cast(0xC0 | (uch >> 6)); utfForm[lenU++] = static_cast(0x80 | (uch & 0x3f)); } } utfForm.resize(lenU); return utfForm; } namespace { std::string UTF8FromIconv(const Converter &conv, std::string_view text) { if (conv) { std::string utfForm(text.length()*3+1, '\0'); char *pin = const_cast(text.data()); gsize inLeft = text.length(); char *putf = &utfForm[0]; char *pout = putf; gsize outLeft = text.length()*3+1; const gsize conversions = conv.Convert(&pin, &inLeft, &pout, &outLeft); if (conversions != sizeFailure) { *pout = '\0'; utfForm.resize(pout - putf); return utfForm; } } return std::string(); } // Work out how many bytes are in a character by trying to convert using iconv, // returning the first length that succeeds. size_t MultiByteLenFromIconv(const Converter &conv, const char *s, size_t len) noexcept { for (size_t lenMB=1; (lenMB<4) && (lenMB <= len); lenMB++) { char wcForm[2] {}; char *pin = const_cast(s); gsize inLeft = lenMB; char *pout = wcForm; gsize outLeft = 2; const gsize conversions = conv.Convert(&pin, &inLeft, &pout, &outLeft); if (conversions != sizeFailure) { return lenMB; } } return 1; } } void SurfaceImpl::DrawTextBase(PRectangle rc, const Font *font_, XYPOSITION ybase, std::string_view text, ColourRGBA fore) { if (context) { PenColourAlpha(fore); const XYPOSITION xText = rc.left; if (PFont(font_)->fd) { if (et == EncodingType::utf8) { LayoutSetText(layout.get(), text); } else { SetConverter(PFont(font_)->characterSet); std::string utfForm = UTF8FromIconv(conv, text); if (utfForm.empty()) { // iconv failed so treat as Latin1 utfForm = UTF8FromLatin1(text); } LayoutSetText(layout.get(), utfForm); } pango_layout_set_font_description(layout.get(), PFont(font_)->fd.get()); pango_cairo_update_layout(context, layout.get()); PangoLayoutLine *pll = pango_layout_get_line_readonly(layout.get(), 0); cairo_move_to(context, xText, ybase); pango_cairo_show_layout_line(context, pll); } } } void SurfaceImpl::DrawTextNoClip(PRectangle rc, const Font *font_, XYPOSITION ybase, std::string_view text, ColourRGBA fore, ColourRGBA back) { FillRectangleAligned(rc, back); DrawTextBase(rc, font_, ybase, text, fore); } // On GTK+, exactly same as DrawTextNoClip void SurfaceImpl::DrawTextClipped(PRectangle rc, const Font *font_, XYPOSITION ybase, std::string_view text, ColourRGBA fore, ColourRGBA back) { FillRectangleAligned(rc, back); DrawTextBase(rc, font_, ybase, text, fore); } void SurfaceImpl::DrawTextTransparent(PRectangle rc, const Font *font_, XYPOSITION ybase, std::string_view text, ColourRGBA fore) { // Avoid drawing spaces in transparent mode for (size_t i=0; i(text.length())) { LayoutSetText(layout, text); iter.reset(pango_layout_get_iter(layout)); curIndex = pango_layout_iter_get_index(iter.get()); pango_layout_iter_get_cluster_extents(iter.get(), nullptr, &pos); } void Next() noexcept { positionStart = position; if (pango_layout_iter_next_cluster(iter.get())) { pango_layout_iter_get_cluster_extents(iter.get(), nullptr, &pos); position = pango_units_to_double(pos.x); curIndex = pango_layout_iter_get_index(iter.get()); } else { finished = true; position = pango_units_to_double(pos.x + pos.width); curIndex = pango_layout_iter_get_index(iter.get()); } distance = position - positionStart; } }; // Something has gone wrong so set all the characters as equally spaced. void EquallySpaced(PangoLayout *layout, XYPOSITION *positions, size_t lenPositions) { int widthLayout = 0; pango_layout_get_size(layout, &widthLayout, nullptr); const XYPOSITION widthTotal = pango_units_to_double(widthLayout); for (size_t bytePos=0; bytePosfd) { UniquePangoContext contextMeasure = MeasuringContext(); UniquePangoLayout layoutMeasure(pango_layout_new(contextMeasure.get())); PLATFORM_ASSERT(layoutMeasure); pango_layout_set_font_description(layoutMeasure.get(), PFont(font_)->fd.get()); if (et == EncodingType::utf8) { // Simple and direct as UTF-8 is native Pango encoding ClusterIterator iti(layoutMeasure.get(), text); int i = iti.curIndex; if (i != 0) { // Unexpected start to iteration, could be bidirectional text EquallySpaced(layoutMeasure.get(), positions, text.length()); return; } while (!iti.finished) { iti.Next(); const int places = iti.curIndex - i; while (i < iti.curIndex) { // Evenly distribute space among bytes of this cluster. // Would be better to find number of characters and then // divide evenly between characters with each byte of a character // being at the same position. positions[i] = iti.position - (iti.curIndex - 1 - i) * iti.distance / places; i++; } } PLATFORM_ASSERT(static_cast(i) == text.length()); } else { int positionsCalculated = 0; const char *charSetID = CharacterSetID(PFont(font_)->characterSet); std::string utfForm; { gsize bytesRead = 0; gsize bytesWritten = 0; GError *error = nullptr; UniqueStr textInUTF8(g_convert(text.data(), text.length(), "UTF-8", charSetID, &bytesRead, &bytesWritten, &error)); if ((bytesWritten > 0) && (bytesRead == text.length()) && !error) { // Extra allocation here but avoiding it makes code more complex utfForm.assign(textInUTF8.get(), bytesWritten); } if (error) { #ifdef DEBUG fprintf(stderr, "MeasureWidths: %s.\n", error->message); #endif g_error_free(error); } } if (et == EncodingType::dbcs) { if (!utfForm.empty()) { // Convert to UTF-8 so can ask Pango for widths, then // Loop through UTF-8 and DBCS forms, taking account of different // character byte lengths. Converter convMeasure("UCS-2", charSetID, false); int i = 0; ClusterIterator iti(layoutMeasure.get(), utfForm); int clusterStart = iti.curIndex; if (clusterStart != 0) { // Unexpected start to iteration, could be bidirectional text EquallySpaced(layoutMeasure.get(), positions, text.length()); return; } while (!iti.finished) { iti.Next(); const int clusterEnd = iti.curIndex; const int places = g_utf8_strlen(utfForm.data() + clusterStart, clusterEnd - clusterStart); int place = 1; while (clusterStart < clusterEnd) { size_t lenChar = MultiByteLenFromIconv(convMeasure, text.data()+i, text.length()-i); while (lenChar--) { positions[i++] = iti.position - (places - place) * iti.distance / places; positionsCalculated++; } clusterStart += UTF8BytesOfLead[static_cast(utfForm[clusterStart])]; place++; } } PLATFORM_ASSERT(static_cast(i) == text.length()); } } if (positionsCalculated < 1) { const size_t lenPositions = text.length(); // Either 8-bit or DBCS conversion failed so treat as 8-bit. const bool rtlCheck = PFont(font_)->characterSet == CharacterSet::Hebrew || PFont(font_)->characterSet == CharacterSet::Arabic; if (utfForm.empty()) { utfForm = UTF8FromLatin1(text); #ifdef DEBUG fprintf(stderr, "MeasureWidths: Fall back to Latin1 [%s]\n", utfForm.c_str()); #endif } size_t i = 0; // Each 8-bit input character may take 1 or 2 bytes in UTF-8 // and groups of up to 3 may be represented as ligatures. ClusterIterator iti(layoutMeasure.get(), utfForm); int clusterStart = iti.curIndex; if (clusterStart != 0) { // Unexpected start to iteration, could be bidirectional text EquallySpaced(layoutMeasure.get(), positions, lenPositions); return; } while (!iti.finished) { iti.Next(); const int clusterEnd = iti.curIndex; const int ligatureLength = g_utf8_strlen(utfForm.data() + clusterStart, clusterEnd - clusterStart); if (((i + ligatureLength) > lenPositions) || (rtlCheck && ((clusterEnd <= clusterStart) || (ligatureLength == 0) || (ligatureLength > 3)))) { // Something has gone wrong: exit quickly but pretend all the characters are equally spaced: #ifdef DEBUG fprintf(stderr, "MeasureWidths: result too long.\n"); #endif EquallySpaced(layoutMeasure.get(), positions, lenPositions); return; } PLATFORM_ASSERT(ligatureLength > 0 && ligatureLength <= 3); for (int charInLig=0; charInLigfd) { pango_layout_set_font_description(layout.get(), PFont(font_)->fd.get()); if (et == EncodingType::utf8) { LayoutSetText(layout.get(), text); } else { SetConverter(PFont(font_)->characterSet); std::string utfForm = UTF8FromIconv(conv, text); if (utfForm.empty()) { // iconv failed so treat as Latin1 utfForm = UTF8FromLatin1(text); } LayoutSetText(layout.get(), utfForm); } PangoLayoutLine *pangoLine = pango_layout_get_line_readonly(layout.get(), 0); PangoRectangle pos {}; pango_layout_line_get_extents(pangoLine, nullptr, &pos); return pango_units_to_double(pos.width); } return 1; } void SurfaceImpl::DrawTextBaseUTF8(PRectangle rc, const Font *font_, XYPOSITION ybase, std::string_view text, ColourRGBA fore) { if (context) { PenColourAlpha(fore); const XYPOSITION xText = rc.left; if (PFont(font_)->fd) { LayoutSetText(layout.get(), text); pango_layout_set_font_description(layout.get(), PFont(font_)->fd.get()); pango_cairo_update_layout(context, layout.get()); PangoLayoutLine *pll = pango_layout_get_line_readonly(layout.get(), 0); cairo_move_to(context, xText, ybase); pango_cairo_show_layout_line(context, pll); } } } void SurfaceImpl::DrawTextNoClipUTF8(PRectangle rc, const Font *font_, XYPOSITION ybase, std::string_view text, ColourRGBA fore, ColourRGBA back) { FillRectangleAligned(rc, back); DrawTextBaseUTF8(rc, font_, ybase, text, fore); } // On GTK+, exactly same as DrawTextNoClip void SurfaceImpl::DrawTextClippedUTF8(PRectangle rc, const Font *font_, XYPOSITION ybase, std::string_view text, ColourRGBA fore, ColourRGBA back) { FillRectangleAligned(rc, back); DrawTextBaseUTF8(rc, font_, ybase, text, fore); } void SurfaceImpl::DrawTextTransparentUTF8(PRectangle rc, const Font *font_, XYPOSITION ybase, std::string_view text, ColourRGBA fore) { // Avoid drawing spaces in transparent mode for (size_t i = 0; i < text.length(); i++) { if (text[i] != ' ') { DrawTextBaseUTF8(rc, font_, ybase, text, fore); return; } } } void SurfaceImpl::MeasureWidthsUTF8(const Font *font_, std::string_view text, XYPOSITION *positions) { if (PFont(font_)->fd) { UniquePangoContext contextMeasure = MeasuringContext(); UniquePangoLayout layoutMeasure(pango_layout_new(contextMeasure.get())); PLATFORM_ASSERT(layoutMeasure); pango_layout_set_font_description(layoutMeasure.get(), PFont(font_)->fd.get()); // Simple and direct as UTF-8 is native Pango encoding ClusterIterator iti(layoutMeasure.get(), text); int i = iti.curIndex; if (i != 0) { // Unexpected start to iteration, could be bidirectional text EquallySpaced(layoutMeasure.get(), positions, text.length()); return; } while (!iti.finished) { iti.Next(); const int places = iti.curIndex - i; while (i < iti.curIndex) { // Evenly distribute space among bytes of this cluster. // Would be better to find number of characters and then // divide evenly between characters with each byte of a character // being at the same position. positions[i] = iti.position - (iti.curIndex - 1 - i) * iti.distance / places; i++; } } PLATFORM_ASSERT(static_cast(i) == text.length()); } else { // No font so return an ascending range of values for (size_t i = 0; i < text.length(); i++) { positions[i] = i + 1.0; } } } XYPOSITION SurfaceImpl::WidthTextUTF8(const Font *font_, std::string_view text) { if (PFont(font_)->fd) { pango_layout_set_font_description(layout.get(), PFont(font_)->fd.get()); LayoutSetText(layout.get(), text); PangoLayoutLine *pangoLine = pango_layout_get_line_readonly(layout.get(), 0); PangoRectangle pos{}; pango_layout_line_get_extents(pangoLine, nullptr, &pos); return pango_units_to_double(pos.width); } return 1; } // Ascent and descent determined by Pango font metrics. XYPOSITION SurfaceImpl::Ascent(const Font *font_) { if (!PFont(font_)->fd) { return 1.0; } UniquePangoFontMetrics metrics(pango_context_get_metrics(pcontext.get(), PFont(font_)->fd.get(), language)); return std::max(1.0, std::ceil(pango_units_to_double( pango_font_metrics_get_ascent(metrics.get())))); } XYPOSITION SurfaceImpl::Descent(const Font *font_) { if (!PFont(font_)->fd) { return 0.0; } UniquePangoFontMetrics metrics(pango_context_get_metrics(pcontext.get(), PFont(font_)->fd.get(), language)); return std::ceil(pango_units_to_double(pango_font_metrics_get_descent(metrics.get()))); } XYPOSITION SurfaceImpl::InternalLeading(const Font *) { return 0; } XYPOSITION SurfaceImpl::Height(const Font *font_) { return Ascent(font_) + Descent(font_); } XYPOSITION SurfaceImpl::AverageCharWidth(const Font *font_) { return WidthText(font_, "n"); } void SurfaceImpl::SetClip(PRectangle rc) { PLATFORM_ASSERT(context); cairo_save(context); CairoRectangle(rc); cairo_clip(context); } void SurfaceImpl::PopClip() { PLATFORM_ASSERT(context); cairo_restore(context); } void SurfaceImpl::FlushCachedState() {} void SurfaceImpl::FlushDrawing() { } std::unique_ptr Surface::Allocat