2025-04-30 19:12:00 -07:00
|
|
|
using Gtk;
|
|
|
|
|
using Gee;
|
|
|
|
|
|
2025-04-30 21:19:24 -07:00
|
|
|
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;
|
2025-05-02 15:09:12 -07:00
|
|
|
corner_radius = 24.0f / scale_factor;
|
2025-04-30 21:19:24 -07:00
|
|
|
text_padding = 18.0f / scale_factor;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-30 19:12:00 -07:00
|
|
|
private class MessageLayout : Object
|
|
|
|
|
{
|
|
|
|
|
public Message message;
|
|
|
|
|
|
|
|
|
|
private float max_width;
|
|
|
|
|
private Pango.Layout layout;
|
|
|
|
|
private Widget parent;
|
2025-04-30 21:19:24 -07:00
|
|
|
private MessageLayoutConstants constants;
|
2025-04-30 19:12:00 -07:00
|
|
|
|
|
|
|
|
public MessageLayout(Message message, Widget parent, float max_width) {
|
|
|
|
|
this.message = message;
|
|
|
|
|
this.max_width = max_width;
|
|
|
|
|
this.parent = parent;
|
2025-04-30 21:19:24 -07:00
|
|
|
this.constants = MessageLayoutConstants(parent.get_scale_factor());
|
2025-04-30 19:12:00 -07:00
|
|
|
|
2025-04-30 19:50:36 -07:00
|
|
|
layout = parent.create_pango_layout(message.text);
|
2025-04-30 19:12:00 -07:00
|
|
|
|
2025-04-30 21:19:24 -07:00
|
|
|
// 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));
|
2025-04-30 19:12:00 -07:00
|
|
|
layout.set_font_description(font_desc);
|
|
|
|
|
|
|
|
|
|
// Set max width
|
|
|
|
|
layout.set_width((int)text_available_width * Pango.SCALE);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public bool from_me {
|
|
|
|
|
get {
|
2025-04-30 19:50:36 -07:00
|
|
|
return message.from_me;
|
2025-04-30 19:12:00 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private float text_available_width {
|
|
|
|
|
get {
|
2025-04-30 21:19:24 -07:00
|
|
|
return max_width - text_x_offset - constants.text_padding;
|
2025-04-30 19:12:00 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private float text_x_offset {
|
|
|
|
|
get {
|
2025-04-30 21:19:24 -07:00
|
|
|
return from_me ? constants.text_padding : constants.tail_width + constants.text_padding;
|
2025-04-30 19:12:00 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private float text_x_padding {
|
|
|
|
|
get {
|
|
|
|
|
// Opposite of text_x_offset
|
2025-04-30 21:19:24 -07:00
|
|
|
return from_me ? constants.tail_width + constants.text_padding : constants.text_padding;
|
2025-04-30 19:12:00 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
2025-04-30 21:19:24 -07:00
|
|
|
return logical_rect.height + constants.corner_radius + constants.tail_bottom_padding;
|
2025-04-30 19:12:00 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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,
|
2025-04-30 21:19:24 -07:00
|
|
|
y = ((get_height() - constants.tail_bottom_padding) - logical_rect.height) / 2
|
2025-04-30 19:12:00 -07:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
{
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void with_bubble_clip(Snapshot snapshot, Func<Snapshot> func) {
|
|
|
|
|
var width = get_width();
|
|
|
|
|
var height = get_height();
|
2025-05-03 21:45:17 -07:00
|
|
|
|
|
|
|
|
if (width > 10 && height > 10) {
|
|
|
|
|
var path = create_bubble_path(width, height, from_me);
|
|
|
|
|
snapshot.push_fill(path, Gsk.FillRule.WINDING);
|
|
|
|
|
func(snapshot);
|
|
|
|
|
snapshot.pop();
|
|
|
|
|
} else {
|
|
|
|
|
func(snapshot);
|
|
|
|
|
}
|
2025-04-30 19:12:00 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private Gsk.Path create_bubble_path(float width, float height, bool tail_on_right = false)
|
|
|
|
|
{
|
|
|
|
|
var builder = new Gsk.PathBuilder();
|
|
|
|
|
|
2025-04-30 21:19:24 -07:00
|
|
|
float bubble_width = width - constants.tail_width;
|
|
|
|
|
float bubble_height = height - constants.tail_bottom_padding;
|
2025-04-30 19:12:00 -07:00
|
|
|
|
|
|
|
|
// Base position adjustments based on tail position
|
2025-04-30 21:19:24 -07:00
|
|
|
float x = tail_on_right ? 0.0f : constants.tail_width;
|
2025-04-30 19:12:00 -07:00
|
|
|
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
|
2025-04-30 21:19:24 -07:00
|
|
|
builder.move_to(tail_on_right ? (x + constants.corner_radius) : (x + bubble_width - constants.corner_radius), y);
|
2025-04-30 19:12:00 -07:00
|
|
|
|
|
|
|
|
// Top edge
|
2025-04-30 21:19:24 -07:00
|
|
|
builder.line_to(tail_on_right ? (x + bubble_width - constants.corner_radius) : (x + constants.corner_radius), y);
|
2025-04-30 19:12:00 -07:00
|
|
|
|
|
|
|
|
// Top corner on tail side
|
|
|
|
|
if (tail_on_right) {
|
|
|
|
|
builder.html_arc_to(x + bubble_width, y,
|
2025-04-30 21:19:24 -07:00
|
|
|
x + bubble_width, y + constants.corner_radius,
|
|
|
|
|
constants.corner_radius);
|
2025-04-30 19:12:00 -07:00
|
|
|
} else {
|
|
|
|
|
builder.html_arc_to(x, y,
|
2025-04-30 21:19:24 -07:00
|
|
|
x, y + constants.corner_radius,
|
|
|
|
|
constants.corner_radius);
|
2025-04-30 19:12:00 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Side edge on tail side
|
2025-04-30 21:19:24 -07:00
|
|
|
builder.line_to(tail_side_x, y + bubble_height - constants.corner_radius);
|
2025-04-30 19:12:00 -07:00
|
|
|
|
|
|
|
|
// Corner with tail
|
2025-04-30 21:19:24 -07:00
|
|
|
float tail_tip_x = tail_side_x + (dir * (constants.tail_width - constants.tail_curve_offset));
|
2025-04-30 19:12:00 -07:00
|
|
|
float tail_tip_y = y + bubble_height;
|
|
|
|
|
|
|
|
|
|
// Control points for the bezier curve
|
2025-04-30 21:19:24 -07:00
|
|
|
float ctrl_point1_x = tail_side_x + (dir * constants.tail_side_offset);
|
|
|
|
|
float ctrl_point1_y = y + bubble_height - constants.corner_radius/3;
|
2025-04-30 19:12:00 -07:00
|
|
|
|
2025-04-30 21:19:24 -07:00
|
|
|
float ctrl_point2_x = tail_side_x + (dir * (constants.tail_width / 2.0f));
|
2025-04-30 19:12:00 -07:00
|
|
|
float ctrl_point2_y = y + bubble_height - 2;
|
|
|
|
|
|
|
|
|
|
// Point where the tail meets the bottom edge
|
2025-04-30 21:19:24 -07:00
|
|
|
float tail_base_x = tail_side_x - (dir * constants.corner_radius/2);
|
2025-04-30 19:12:00 -07:00
|
|
|
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);
|
|
|
|
|
|
2025-04-30 21:19:24 -07:00
|
|
|
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,
|
2025-04-30 19:12:00 -07:00
|
|
|
tail_base_x, tail_base_y);
|
|
|
|
|
|
|
|
|
|
// Bottom edge
|
2025-04-30 21:19:24 -07:00
|
|
|
builder.line_to(tail_on_right ? (x + constants.corner_radius) : (x + bubble_width - constants.corner_radius), y + bubble_height);
|
2025-04-30 19:12:00 -07:00
|
|
|
|
|
|
|
|
// Bottom corner opposite to tail
|
|
|
|
|
if (tail_on_right) {
|
|
|
|
|
builder.html_arc_to(x, y + bubble_height,
|
2025-04-30 21:19:24 -07:00
|
|
|
x, y + bubble_height - constants.corner_radius,
|
|
|
|
|
constants.corner_radius);
|
2025-04-30 19:12:00 -07:00
|
|
|
} else {
|
|
|
|
|
builder.html_arc_to(x + bubble_width, y + bubble_height,
|
2025-04-30 21:19:24 -07:00
|
|
|
x + bubble_width, y + bubble_height - constants.corner_radius,
|
|
|
|
|
constants.corner_radius);
|
2025-04-30 19:12:00 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Side edge opposite to tail
|
|
|
|
|
if (tail_on_right) {
|
2025-04-30 21:19:24 -07:00
|
|
|
builder.line_to(x, y + constants.corner_radius);
|
2025-04-30 19:12:00 -07:00
|
|
|
|
|
|
|
|
// Top corner to close path
|
|
|
|
|
builder.html_arc_to(x, y,
|
2025-04-30 21:19:24 -07:00
|
|
|
x + constants.corner_radius, y,
|
|
|
|
|
constants.corner_radius);
|
2025-04-30 19:12:00 -07:00
|
|
|
} else {
|
2025-04-30 21:19:24 -07:00
|
|
|
builder.line_to(x + bubble_width, y + constants.corner_radius);
|
2025-04-30 19:12:00 -07:00
|
|
|
|
|
|
|
|
// Top corner to close path
|
|
|
|
|
builder.html_arc_to(x + bubble_width, y,
|
2025-04-30 21:19:24 -07:00
|
|
|
x + bubble_width - constants.corner_radius, y,
|
|
|
|
|
constants.corner_radius);
|
2025-04-30 19:12:00 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Close the path
|
|
|
|
|
builder.close();
|
|
|
|
|
|
|
|
|
|
return builder.to_path();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|