using Gtk; using Gee; private struct MessageLayoutConstants { public float tail_width; public float tail_curve_offset; public float tail_side_offset; public float tail_bottom_padding; public float corner_radius; public float text_padding; public MessageLayoutConstants(float scale_factor) { tail_width = 15.0f / scale_factor; tail_curve_offset = 2.5f / scale_factor; tail_side_offset = 0.0f / scale_factor; tail_bottom_padding = 4.0f / scale_factor; corner_radius = 24.0f / scale_factor; text_padding = 18.0f / scale_factor; } } private class MessageLayout : Object { public Message message; private float max_width; private Pango.Layout layout; private Widget parent; private MessageLayoutConstants constants; public MessageLayout(Message message, Widget parent, float max_width) { this.message = message; this.max_width = max_width; this.parent = parent; this.constants = MessageLayoutConstants(parent.get_scale_factor()); layout = parent.create_pango_layout(message.text); // Get the system font settings var settings = Gtk.Settings.get_default(); var font_name = settings.gtk_font_name; // Create font description from system font var font_desc = Pango.FontDescription.from_string(font_name); var size = font_desc.get_size(); font_desc.set_size((int)(size)); layout.set_font_description(font_desc); // Set max width layout.set_width((int)text_available_width * Pango.SCALE); } public bool from_me { get { return message.from_me; } } private float text_available_width { get { return max_width - text_x_offset - constants.text_padding; } } private float text_x_offset { get { return from_me ? constants.text_padding : constants.tail_width + constants.text_padding; } } private float text_x_padding { get { // Opposite of text_x_offset return from_me ? constants.tail_width + constants.text_padding : constants.text_padding; } } private Gdk.RGBA background_color { get { return from_me ? parent.get_color() : Gdk.RGBA() { red = 1.0f, green = 1.0f, blue = 1.0f, alpha = 0.08f }; } } public float get_height() { Pango.Rectangle ink_rect, logical_rect; layout.get_pixel_extents(out ink_rect, out logical_rect); return logical_rect.height + constants.corner_radius + constants.tail_bottom_padding; } public float get_width() { Pango.Rectangle ink_rect, logical_rect; layout.get_pixel_extents(out ink_rect, out logical_rect); return logical_rect.width + text_x_offset + text_x_padding; } public void draw(Snapshot snapshot) { with_bubble_clip(snapshot, (snapshot) => { draw_background(snapshot); draw_text(snapshot); }); } private void draw_text(Snapshot snapshot) { snapshot.save(); Pango.Rectangle ink_rect, logical_rect; layout.get_pixel_extents(out ink_rect, out logical_rect); snapshot.translate(Graphene.Point() { x = text_x_offset, y = ((get_height() - constants.tail_bottom_padding) - logical_rect.height) / 2 }); snapshot.append_layout(layout, Gdk.RGBA() { red = 1.0f, green = 1.0f, blue = 1.0f, alpha = 1.0f }); snapshot.restore(); } private void draw_background(Snapshot snapshot) { snapshot.save(); var width = get_width(); var height = get_height(); var color = background_color; var bounds = Graphene.Rect().init(0, 0, width, height); const float gradient_darkening = 0.67f; var start_point = Graphene.Point() { x = 0, y = 0 }; var end_point = Graphene.Point() { x = 0, y = height }; var stops = new Gsk.ColorStop[] { Gsk.ColorStop() { offset = 0.0f, color = color }, Gsk.ColorStop() { offset = 1.0f, color = Gdk.RGBA () { red = color.red * gradient_darkening, green = color.green * gradient_darkening, blue = color.blue * gradient_darkening, alpha = color.alpha } }, }; snapshot.append_linear_gradient(bounds, start_point, end_point, stops); snapshot.restore(); } private void with_bubble_clip(Snapshot snapshot, Func func) { var width = get_width(); var height = get_height(); var path = create_bubble_path(width, height, from_me); snapshot.push_fill(path, Gsk.FillRule.WINDING); func(snapshot); snapshot.pop(); } private Gsk.Path create_bubble_path(float width, float height, bool tail_on_right = false) { var builder = new Gsk.PathBuilder(); float bubble_width = width - constants.tail_width; float bubble_height = height - constants.tail_bottom_padding; // Base position adjustments based on tail position float x = tail_on_right ? 0.0f : constants.tail_width; float y = 0.0f; // Calculate tail direction multiplier (-1 for left, 1 for right) float dir = tail_on_right ? 1.0f : -1.0f; // Calculate tail side positions based on direction float tail_side_x = tail_on_right ? (x + bubble_width) : x; // Start at top corner opposite to the tail builder.move_to(tail_on_right ? (x + constants.corner_radius) : (x + bubble_width - constants.corner_radius), y); // Top edge builder.line_to(tail_on_right ? (x + bubble_width - constants.corner_radius) : (x + constants.corner_radius), y); // Top corner on tail side if (tail_on_right) { builder.html_arc_to(x + bubble_width, y, x + bubble_width, y + constants.corner_radius, constants.corner_radius); } else { builder.html_arc_to(x, y, x, y + constants.corner_radius, constants.corner_radius); } // Side edge on tail side builder.line_to(tail_side_x, y + bubble_height - constants.corner_radius); // Corner with tail float tail_tip_x = tail_side_x + (dir * (constants.tail_width - constants.tail_curve_offset)); float tail_tip_y = y + bubble_height; // Control points for the bezier curve float ctrl_point1_x = tail_side_x + (dir * constants.tail_side_offset); float ctrl_point1_y = y + bubble_height - constants.corner_radius/3; float ctrl_point2_x = tail_side_x + (dir * (constants.tail_width / 2.0f)); float ctrl_point2_y = y + bubble_height - 2; // Point where the tail meets the bottom edge float tail_base_x = tail_side_x - (dir * constants.corner_radius/2); float tail_base_y = y + bubble_height; // Draw the corner with tail using bezier curves builder.cubic_to(ctrl_point1_x, ctrl_point1_y, ctrl_point2_x, ctrl_point2_y, tail_tip_x, tail_tip_y); builder.cubic_to(tail_tip_x - (dir * constants.tail_curve_offset), tail_tip_y + constants.tail_curve_offset, tail_base_x + (dir * constants.tail_width), tail_base_y, tail_base_x, tail_base_y); // Bottom edge builder.line_to(tail_on_right ? (x + constants.corner_radius) : (x + bubble_width - constants.corner_radius), y + bubble_height); // Bottom corner opposite to tail if (tail_on_right) { builder.html_arc_to(x, y + bubble_height, x, y + bubble_height - constants.corner_radius, constants.corner_radius); } else { builder.html_arc_to(x + bubble_width, y + bubble_height, x + bubble_width, y + bubble_height - constants.corner_radius, constants.corner_radius); } // Side edge opposite to tail if (tail_on_right) { builder.line_to(x, y + constants.corner_radius); // Top corner to close path builder.html_arc_to(x, y, x + constants.corner_radius, y, constants.corner_radius); } else { builder.line_to(x + bubble_width, y + constants.corner_radius); // Top corner to close path builder.html_arc_to(x + bubble_width, y, x + bubble_width - constants.corner_radius, y, constants.corner_radius); } // Close the path builder.close(); return builder.to_path(); } }