diff --git a/gtk/gtkimcontext.c b/gtk/gtkimcontext.c index bf3f9de7961b7410babf01220cd84e5e02b807a4..f5da414edbe4ffadbd968164df74e01f9c06eb21 100644 --- a/gtk/gtkimcontext.c +++ b/gtk/gtkimcontext.c @@ -18,6 +18,7 @@ #include "config.h" #include #include "gtkimcontext.h" +#include "gtkimcontextprivate.h" #include "gtkprivate.h" #include "gtktypebuiltins.h" #include "gtkmarshalers.h" @@ -82,6 +83,7 @@ typedef struct _GtkIMContextPrivate GtkIMContextPrivate; struct _GtkIMContextPrivate { GtkInputPurpose purpose; GtkInputHints hints; + GtkCssNode *parent_node; }; static void gtk_im_context_real_get_preedit_string (GtkIMContext *context, @@ -112,6 +114,8 @@ static void gtk_im_context_set_property (GObject *obj, const GValue *value, GParamSpec *pspec); +static void gtk_im_context_finalize (GObject *object); + G_DEFINE_ABSTRACT_TYPE_WITH_PRIVATE (GtkIMContext, gtk_im_context, G_TYPE_OBJECT) @@ -195,6 +199,7 @@ gtk_im_context_class_init (GtkIMContextClass *klass) object_class->get_property = gtk_im_context_get_property; object_class->set_property = gtk_im_context_set_property; + object_class->finalize = gtk_im_context_finalize; klass->get_preedit_string = gtk_im_context_real_get_preedit_string; klass->filter_keypress = gtk_im_context_real_filter_keypress; @@ -474,6 +479,23 @@ gtk_im_context_real_get_surrounding_with_selection (GtkIMContext *context, return result; } +void +gtk_im_context_set_parent_node (GtkIMContext *im_context, + GtkCssNode *css_node) +{ + GtkIMContextPrivate *priv = gtk_im_context_get_instance_private (im_context); + + g_set_object (&priv->parent_node, css_node); +} + +GtkCssNode * +gtk_im_context_get_parent_node (GtkIMContext *im_context) +{ + GtkIMContextPrivate *priv = gtk_im_context_get_instance_private (im_context); + + return priv->parent_node; +} + /** * gtk_im_context_set_client_widget: * @context: a `GtkIMContext` @@ -1030,6 +1052,17 @@ gtk_im_context_set_property (GObject *obj, } } +static void +gtk_im_context_finalize (GObject *object) +{ + GtkIMContextPrivate *priv = + gtk_im_context_get_instance_private (GTK_IM_CONTEXT (object)); + + g_clear_object (&priv->parent_node); + + G_OBJECT_CLASS (gtk_im_context_parent_class)->finalize (object); +} + /** * gtk_im_context_activate_osk: * @context: a `GtkIMContext` diff --git a/gtk/gtkimcontextprivate.h b/gtk/gtkimcontextprivate.h new file mode 100644 index 0000000000000000000000000000000000000000..0d37ec46fcc55ab3ab6403f1359ce1fa5665c83a --- /dev/null +++ b/gtk/gtkimcontextprivate.h @@ -0,0 +1,28 @@ +/* GTK - The GIMP Toolkit + * Copyright (C) 2025 Red Hat Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Author: Carlos Garnacho + */ + +#pragma once + +#include "gtkimcontext.h" +#include "gtkcssnodeprivate.h" + +void gtk_im_context_set_parent_node (GtkIMContext *im_context, + GtkCssNode *css_node); + +GtkCssNode * gtk_im_context_get_parent_node (GtkIMContext *im_context); diff --git a/gtk/gtkimcontextwayland.c b/gtk/gtkimcontextwayland.c index a167acdbe796e82dccf66810b2c9fffa63129f66..c99e96df7c9be223f94d0fd36576b02c70ec6d5c 100644 --- a/gtk/gtkimcontextwayland.c +++ b/gtk/gtkimcontextwayland.c @@ -21,15 +21,22 @@ #include #include +#include "gtk/gtkcsscolorvalueprivate.h" +#include "gtk/gtkcssstyleprivate.h" +#include "gtk/gtkimcontextprivate.h" #include "gtk/gtkimcontextwaylandprivate.h" #include "gtk/gtkimmoduleprivate.h" +#include "gtk/gtkwidgetprivate.h" #include "gdk/wayland/gdkwayland.h" #include "text-input-unstable-v3-client-protocol.h" +#define TEXT_INPUT_SUPPORTED_PROTOCOL_VERSION 2 + typedef struct _GtkIMContextWaylandGlobal GtkIMContextWaylandGlobal; typedef struct _GtkIMContextWayland GtkIMContextWayland; typedef struct _GtkIMContextWaylandClass GtkIMContextWaylandClass; +typedef struct _GtkIMContextPreeditSegment GtkIMContextPreeditSegment; struct _GtkIMContextWaylandGlobal { @@ -55,10 +62,18 @@ struct _GtkIMContextWaylandClass GtkIMContextClass parent_class; }; +struct _GtkIMContextPreeditSegment +{ + unsigned int hint_flags; + int start_idx; + int end_idx; +}; + struct preedit { char *text; int cursor_begin; int cursor_end; + GArray *style_hints; }; struct surrounding_delete { @@ -71,6 +86,8 @@ struct _GtkIMContextWayland GtkIMContext parent_instance; GtkWidget *widget; + GHashTable *style_css_nodes; + struct { char *text; int cursor_idx; @@ -86,6 +103,8 @@ struct _GtkIMContextWayland char *pending_commit; + uint32_t pending_action; + cairo_rectangle_int_t cursor_rect; guint use_preedit : 1; }; @@ -139,6 +158,29 @@ gtk_im_context_wayland_get_text_protocol (GdkDisplay *display) return (struct wl_proxy *) global->text_input; } +static void +notify_handled_actions (GtkIMContextWayland *context) +{ + GtkIMContextWaylandGlobal *global; + struct wl_array array; + uint32_t *action; + + global = gtk_im_context_wayland_get_global (context); + + if (zwp_text_input_v3_get_version (global->text_input) < + ZWP_TEXT_INPUT_V3_SET_AVAILABLE_ACTIONS_SINCE_VERSION) + return; + + wl_array_init (&array); + action = wl_array_add (&array, sizeof (uint32_t)); + *action = ZWP_TEXT_INPUT_V3_ACTION_BACKSPACE; + action = wl_array_add (&array, sizeof (uint32_t)); + *action = ZWP_TEXT_INPUT_V3_ACTION_DELETE; + + zwp_text_input_v3_set_available_actions (global->text_input, &array); + wl_array_release (&array); +} + static void notify_im_change (GtkIMContextWayland *context, enum zwp_text_input_v3_change_cause cause) @@ -156,6 +198,7 @@ notify_im_change (GtkIMContextWayland *context, notify_surrounding_text (context); notify_content_type (context); notify_cursor_location (context); + notify_handled_actions (context); commit_state (context); } @@ -202,6 +245,7 @@ text_input_preedit_apply (GtkIMContextWaylandGlobal *global) g_signal_emit_by_name (context, "preedit-start"); g_free (context->current_preedit.text); + g_clear_pointer (&context->current_preedit.style_hints, g_array_unref); context->current_preedit = context->pending_preedit; context->pending_preedit = defaults; @@ -221,7 +265,7 @@ text_input_commit (void *data, if (!global->current) return; - + context = GTK_IM_CONTEXT_WAYLAND (global->current); g_free (context->pending_commit); @@ -254,7 +298,7 @@ text_input_delete_surrounding_text (void *data, if (!global->current) return; - + context = GTK_IM_CONTEXT_WAYLAND (global->current); /* We already got byte lengths from text_input_v3, but GTK uses char lengths @@ -292,6 +336,26 @@ text_input_delete_surrounding_text_apply (GtkIMContextWaylandGlobal *global) context->pending_surrounding_delete = defaults; } +static void +text_input_action_apply (GtkIMContextWaylandGlobal *global) +{ + GtkIMContextWayland *context; + + context = GTK_IM_CONTEXT_WAYLAND (global->current); + + switch (context->pending_action) + { + case ZWP_TEXT_INPUT_V3_ACTION_BACKSPACE: + gtk_im_context_delete_surrounding (global->current, -1, 1); + break; + case ZWP_TEXT_INPUT_V3_ACTION_DELETE: + gtk_im_context_delete_surrounding (global->current, 0, 1); + break; + default: + break; + } +} + static void text_input_done (void *data, struct zwp_text_input_v3 *text_input, @@ -314,6 +378,7 @@ text_input_done (void *data, text_input_delete_surrounding_text_apply (global); text_input_commit_apply (global); text_input_preedit_apply (global); + text_input_action_apply (global); if (update_im && global->serial == serial) notify_im_change (context, ZWP_TEXT_INPUT_V3_CHANGE_CAUSE_INPUT_METHOD); @@ -402,32 +467,21 @@ notify_cursor_location (GtkIMContextWayland *context) { GtkIMContextWaylandGlobal *global; cairo_rectangle_int_t rect; - double nx, ny; - graphene_point_t p; global = gtk_im_context_wayland_get_global (context); if (global == NULL) return; rect = context->cursor_rect; - if (!gtk_widget_compute_point (context->widget, - GTK_WIDGET (gtk_widget_get_root (context->widget)), - &GRAPHENE_POINT_INIT (rect.x, rect.y), - &p)) - graphene_point_init (&p, rect.x, rect.y); - - gtk_native_get_surface_transform (gtk_widget_get_native (context->widget), &nx, &ny); - - rect.x = p.x + nx; - rect.y = p.y + ny; zwp_text_input_v3_set_cursor_rectangle (global->text_input, rect.x, rect.y, rect.width, rect.height); } static uint32_t -translate_hints (GtkInputHints input_hints, - GtkInputPurpose purpose) +translate_hints (GtkIMContextWayland *context, + GtkInputHints input_hints, + GtkInputPurpose purpose) { uint32_t hints = 0; @@ -443,6 +497,13 @@ translate_hints (GtkInputHints input_hints, hints |= ZWP_TEXT_INPUT_V3_CONTENT_HINT_TITLECASE; if (input_hints & GTK_INPUT_HINT_UPPERCASE_SENTENCES) hints |= ZWP_TEXT_INPUT_V3_CONTENT_HINT_AUTO_CAPITALIZATION; + if (input_hints & GTK_INPUT_HINT_NO_EMOJI) + hints |= ZWP_TEXT_INPUT_V3_CONTENT_HINT_NO_EMOJI; + if (input_hints & GTK_INPUT_HINT_INHIBIT_OSK) + hints |= ZWP_TEXT_INPUT_V3_CONTENT_HINT_ON_SCREEN_INPUT_PROVIDED; + + if (context->use_preedit) + hints |= ZWP_TEXT_INPUT_V3_CONTENT_HINT_PREEDIT_SHOWN; if (purpose == GTK_INPUT_PURPOSE_PIN || purpose == GTK_INPUT_PURPOSE_PASSWORD) @@ -505,7 +566,7 @@ notify_content_type (GtkIMContextWayland *context) NULL); zwp_text_input_v3_set_content_type (global->text_input, - translate_hints (hints, purpose), + translate_hints (context, hints, purpose), translate_purpose (purpose)); } @@ -533,12 +594,36 @@ gtk_im_context_wayland_finalize (GObject *object) g_clear_object (&context->widget); g_free (context->surrounding.text); g_free (context->current_preedit.text); + g_clear_pointer (&context->current_preedit.style_hints, g_array_unref); g_free (context->pending_preedit.text); + g_clear_pointer (&context->pending_preedit.style_hints, g_array_unref); g_free (context->pending_commit); + g_clear_pointer (&context->style_css_nodes, g_hash_table_unref); + G_OBJECT_CLASS (gtk_im_context_wayland_parent_class)->finalize (object); } +static void +update_css_node_state (gpointer key, + gpointer value, + gpointer user_data) +{ + GtkCssNode *css_node = value; + GtkStateFlags state_flags = GPOINTER_TO_UINT (user_data); + + gtk_css_node_set_state (css_node, state_flags & ~GTK_STATE_FLAG_DROP_ACTIVE); +} + +static void +on_widget_state_changed (GtkWidget *widget, + GtkStateFlags flags, + GtkIMContextWayland *context) +{ + g_hash_table_foreach (context->style_css_nodes, + update_css_node_state, GUINT_TO_POINTER (flags)); +} + static void gtk_im_context_wayland_set_client_widget (GtkIMContext *context, GtkWidget *widget) @@ -549,9 +634,195 @@ gtk_im_context_wayland_set_client_widget (GtkIMContext *context, return; if (context_wayland->widget) - gtk_im_context_wayland_focus_out (context); + { + g_hash_table_remove_all (context_wayland->style_css_nodes); + g_signal_handlers_disconnect_by_func (context_wayland->widget, + on_widget_state_changed, + context); + gtk_im_context_wayland_focus_out (context); + } g_set_object (&context_wayland->widget, widget); + + if (context_wayland->widget) + { + g_signal_connect (context_wayland->widget, "state-flags-changed", + G_CALLBACK (on_widget_state_changed), context); + } +} + +static void +on_css_node_style_changed (GtkCssNode *node, + GtkCssStyleChange *change, + GtkIMContextWayland *context) +{ + if (context->widget) + return; + + if (gtk_css_style_change_affects (change, GTK_CSS_AFFECTS_SIZE)) + gtk_widget_queue_resize (context->widget); + else + gtk_widget_queue_draw (context->widget); +} + +static GtkCssNode * +lookup_css_node (GtkIMContextWayland *context_wayland, + unsigned int hint_flags) +{ + GtkCssNode *css_node, *parent_node; + + css_node = g_hash_table_lookup (context_wayland->style_css_nodes, + GUINT_TO_POINTER (hint_flags)); + + if (!css_node) + { + + parent_node = gtk_im_context_get_parent_node (GTK_IM_CONTEXT (context_wayland)); + if (!parent_node) + parent_node = gtk_widget_get_css_node (context_wayland->widget); + + if (!parent_node) + return NULL; + + css_node = gtk_css_node_new (); + gtk_css_node_set_name (css_node, g_quark_from_static_string ("preedit")); + gtk_css_node_set_parent (css_node, parent_node); + gtk_css_node_set_state (css_node, gtk_css_node_get_state (parent_node)); + + if ((hint_flags & (1 << ZWP_TEXT_INPUT_V3_PREEDIT_HINT_WHOLE)) != 0) + gtk_css_node_add_class (css_node, g_quark_from_static_string ("whole")); + if ((hint_flags & (1 << ZWP_TEXT_INPUT_V3_PREEDIT_HINT_SELECTION)) != 0) + gtk_css_node_add_class (css_node, g_quark_from_static_string ("selection")); + if ((hint_flags & (1 << ZWP_TEXT_INPUT_V3_PREEDIT_HINT_PREDICTION)) != 0) + gtk_css_node_add_class (css_node, g_quark_from_static_string ("prediction")); + if ((hint_flags & (1 << ZWP_TEXT_INPUT_V3_PREEDIT_HINT_PREFIX)) != 0) + gtk_css_node_add_class (css_node, g_quark_from_static_string ("prefix")); + if ((hint_flags & (1 << ZWP_TEXT_INPUT_V3_PREEDIT_HINT_SUFFIX)) != 0) + gtk_css_node_add_class (css_node, g_quark_from_static_string ("suffix")); + if ((hint_flags & (1 << ZWP_TEXT_INPUT_V3_PREEDIT_HINT_SPELLING_ERROR)) != 0) + gtk_css_node_add_class (css_node, g_quark_from_static_string ("spelling-error")); + if ((hint_flags & (1 << ZWP_TEXT_INPUT_V3_PREEDIT_HINT_COMPOSE_ERROR)) != 0) + gtk_css_node_add_class (css_node, g_quark_from_static_string ("compose-error")); + + g_signal_connect (css_node, "style-changed", + G_CALLBACK (on_css_node_style_changed), + context_wayland); + + g_hash_table_insert (context_wayland->style_css_nodes, GUINT_TO_POINTER (hint_flags), css_node); + } + + return css_node; +} + +static void +node_to_pango_attrs (GtkIMContextWayland *context_wayland, + GtkIMContextPreeditSegment *segment, + GtkCssNode *node, + PangoAttrList *attrs) +{ + PangoAttrList *node_attrs; + PangoAttribute *attr; + GtkCssStyle *style; + const GdkRGBA *color; + + node_attrs = gtk_css_style_get_pango_attributes (gtk_css_node_get_style (node)); + if (node_attrs) + { + GSList *attr_list, *l; + + attr_list = pango_attr_list_get_attributes (node_attrs); + + /* Change start/end indexes and move to the overall list */ + for (l = attr_list; l; l = l->next) + { + attr = l->data; + attr->start_index = segment->start_idx; + attr->end_index = segment->end_idx; + pango_attr_list_insert (attrs, attr); + } + + pango_attr_list_unref (node_attrs); + g_slist_free (attr_list); + } + + style = gtk_css_node_get_style (node); + + /* Foreground */ + color = gtk_css_color_value_get_rgba (style->used->color); + + if (color->alpha > 0) + { + attr = pango_attr_foreground_new (CLAMP (color->red * 65535. + 0.5, 0, 65535), + CLAMP (color->green * 65535. + 0.5, 0, 65535), + CLAMP (color->blue * 65535. + 0.5, 0, 65535)); + attr->start_index = segment->start_idx; + attr->end_index = segment->end_idx; + pango_attr_list_insert (attrs, attr); + + if (color->alpha < 1) + { + attr = pango_attr_foreground_alpha_new (CLAMP (color->alpha * 65535. + 0.5, 0, 65535)); + attr->start_index = segment->start_idx; + attr->end_index = segment->end_idx; + pango_attr_list_insert (attrs, attr); + } + } + + /* Background */ + color = gtk_css_color_value_get_rgba (style->used->background_color); + + if (color->alpha > 0) + { + attr = pango_attr_background_new (CLAMP (color->red * 65535. + 0.5, 0, 65535), + CLAMP (color->green * 65535. + 0.5, 0, 65535), + CLAMP (color->blue * 65535. + 0.5, 0, 65535)); + attr->start_index = segment->start_idx; + attr->end_index = segment->end_idx; + pango_attr_list_insert (attrs, attr); + + if (color->alpha < 1) + { + attr = pango_attr_background_alpha_new (CLAMP (color->alpha * 65535. + 0.5, 0, 65535)); + attr->start_index = segment->start_idx; + attr->end_index = segment->end_idx; + pango_attr_list_insert (attrs, attr); + } + } +} + +static PangoAttrList * +convert_style_hints_to_pango (GtkIMContextWayland *context_wayland, + const char *text) +{ + PangoAttrList *attrs; + PangoAttribute *attr; + unsigned int i; + + attrs = pango_attr_list_new (); + + /* enable fallback, since IBus will send us things like ⎄ */ + attr = pango_attr_fallback_new (TRUE); + attr->start_index = 0; + attr->end_index = strlen (text); + pango_attr_list_insert (attrs, attr); + + if (!context_wayland->current_preedit.style_hints) + return attrs; + + for (i = 0; i < context_wayland->current_preedit.style_hints->len; i++) + { + GtkIMContextPreeditSegment *segment; + GtkCssNode *node; + + segment = &g_array_index (context_wayland->current_preedit.style_hints, + GtkIMContextPreeditSegment, i); + node = lookup_css_node (context_wayland, segment->hint_flags); + + if (node) + node_to_pango_attrs (context_wayland, segment, node, attrs); + } + + return attrs; } static void @@ -563,9 +834,6 @@ gtk_im_context_wayland_get_preedit_string (GtkIMContext *context, GtkIMContextWayland *context_wayland = GTK_IM_CONTEXT_WAYLAND (context); const char *preedit_str; - if (attrs) - *attrs = NULL; - preedit_str = context_wayland->current_preedit.text ? context_wayland->current_preedit.text : ""; @@ -575,35 +843,9 @@ gtk_im_context_wayland_get_preedit_string (GtkIMContext *context, if (str) *str = g_strdup (preedit_str); - if (attrs) - { - PangoAttribute *attr; - guint len = strlen (preedit_str); - - if (!*attrs) - *attrs = pango_attr_list_new (); - - attr = pango_attr_underline_new (PANGO_UNDERLINE_SINGLE); - attr->start_index = 0; - attr->end_index = len; - pango_attr_list_insert (*attrs, attr); - /* enable fallback, since IBus will send us things like ⎄ */ - attr = pango_attr_fallback_new (TRUE); - attr->start_index = 0; - attr->end_index = len; - pango_attr_list_insert (*attrs, attr); - - if (context_wayland->current_preedit.cursor_begin - != context_wayland->current_preedit.cursor_end) - { - /* FIXME: Oh noes, how to highlight while taking into account user preferences? */ - PangoAttribute *cursor = pango_attr_weight_new (PANGO_WEIGHT_BOLD); - cursor->start_index = context_wayland->current_preedit.cursor_begin; - cursor->end_index = context_wayland->current_preedit.cursor_end; - pango_attr_list_insert (*attrs, cursor); - } - } + if (attrs) + *attrs = convert_style_hints_to_pango (context_wayland, preedit_str); } static void @@ -649,6 +891,7 @@ enable (GtkIMContextWayland *context_wayland, GtkIMContextWaylandGlobal *global) { zwp_text_input_v3_enable (global->text_input); + notify_im_change (context_wayland, ZWP_TEXT_INPUT_V3_CHANGE_CAUSE_OTHER); } @@ -700,6 +943,106 @@ text_input_leave (void *data, disable (GTK_IM_CONTEXT_WAYLAND (global->current), global); } +static void +text_input_action (void *data, + struct zwp_text_input_v3 *text_input, + uint32_t action, + uint32_t serial) +{ + GtkIMContextWaylandGlobal *global = data; + GtkIMContextWayland *context = GTK_IM_CONTEXT_WAYLAND (global->current); + + if (context) + context->pending_action = action; +} + +static void +text_input_language (void *data, + struct zwp_text_input_v3 *text_input, + const char *lang) +{ +} + +static void +text_input_preedit_hint (void *data, + struct zwp_text_input_v3 *text_input, + uint32_t begin, + uint32_t end, + enum zwp_text_input_v3_preedit_hint hint) +{ + GtkIMContextWaylandGlobal *global = data; + GtkIMContextWayland *context = GTK_IM_CONTEXT_WAYLAND (global->current); + GtkIMContextPreeditSegment *segment, new_segment; + int first_start_idx = G_MAXINT, last_end_idx = -1; + unsigned int i; + + if (!context->pending_preedit.style_hints) + { + context->pending_preedit.style_hints = + g_array_new (FALSE, FALSE, sizeof (GtkIMContextPreeditSegment)); + } + + /* Convert to a list of non-overlapping segments */ + for (i = 0; i < context->pending_preedit.style_hints->len; i++) + { + segment = &g_array_index (context->current_preedit.style_hints, + GtkIMContextPreeditSegment, i); + + /* This segment is unaffected by the new hint */ + if (begin >= segment->end_idx || end < segment->start_idx) + continue; + + if (begin <= segment->start_idx && end >= segment->end_idx) + { + /* Segment is affected as a whole */ + segment->hint_flags |= (1 << hint); + } + else + { + if (end < segment->end_idx) + { + /* Partition at the end */ + new_segment.start_idx = end; + new_segment.end_idx = segment->end_idx; + new_segment.hint_flags = (1 << hint); + segment->end_idx = end; + g_array_insert_val (context->pending_preedit.style_hints, i + 1, new_segment); + } + + if (begin > segment->start_idx) + { + /* Partition at the beginning */ + new_segment.start_idx = segment->start_idx; + new_segment.end_idx = begin; + new_segment.hint_flags = (1 << hint); + segment->start_idx = begin; + g_array_insert_val (context->pending_preedit.style_hints, i, new_segment); + } + } + + first_start_idx = MIN (segment->start_idx, first_start_idx); + last_end_idx = segment->end_idx; + } + + /* Prepend any missing segment */ + if (begin < first_start_idx) + { + new_segment.start_idx = begin; + new_segment.end_idx = first_start_idx; + new_segment.hint_flags = (1 << hint); + g_array_prepend_val (context->pending_preedit.style_hints, new_segment); + last_end_idx = MAX (last_end_idx, end); + } + + /* Append any missing segment */ + if (last_end_idx >= 0 && end > last_end_idx) + { + new_segment.start_idx = last_end_idx; + new_segment.end_idx = end; + new_segment.hint_flags = (1 << hint); + g_array_append_val (context->pending_preedit.style_hints, new_segment); + } +} static const struct zwp_text_input_v3_listener text_input_listener = { text_input_enter, @@ -708,6 +1051,9 @@ static const struct zwp_text_input_v3_listener text_input_listener = { text_input_commit, text_input_delete_surrounding_text, text_input_done, + text_input_action, + text_input_language, + text_input_preedit_hint, }; static void @@ -725,7 +1071,8 @@ registry_handle_global (void *data, global->text_input_manager_wl_id = id; global->text_input_manager = wl_registry_bind (global->registry, global->text_input_manager_wl_id, - &zwp_text_input_manager_v3_interface, 1); + &zwp_text_input_manager_v3_interface, + MIN (version, TEXT_INPUT_SUPPORTED_PROTOCOL_VERSION)); global->text_input = zwp_text_input_manager_v3_get_text_input (global->text_input_manager, gdk_wayland_seat_get_wl_seat (seat)); @@ -832,19 +1179,37 @@ gtk_im_context_wayland_reset (GtkIMContext *context) static void gtk_im_context_wayland_set_cursor_location (GtkIMContext *context, - GdkRectangle *rect) + GdkRectangle *cursor_rect) { GtkIMContextWayland *context_wayland; + double nx, ny; + GdkRectangle rect; + graphene_point_t p; context_wayland = GTK_IM_CONTEXT_WAYLAND (context); - if (context_wayland->cursor_rect.x == rect->x && - context_wayland->cursor_rect.y == rect->y && - context_wayland->cursor_rect.width == rect->width && - context_wayland->cursor_rect.height == rect->height) - return; + /* Compute surface-local coordinates */ + rect = *cursor_rect; + if (!gtk_widget_compute_point (context_wayland->widget, + GTK_WIDGET (gtk_widget_get_root (context_wayland->widget)), + &GRAPHENE_POINT_INIT (rect.x, rect.y), + &p)) + graphene_point_init (&p, rect.x, rect.y); - context_wayland->cursor_rect = *rect; + gtk_native_get_surface_transform (gtk_widget_get_native (context_wayland->widget), + &nx, &ny); + rect.x = p.x + nx; + rect.y = p.y + ny; + + if (context_wayland->cursor_rect.x != rect.x || + context_wayland->cursor_rect.y != rect.y || + context_wayland->cursor_rect.width != rect.width || + context_wayland->cursor_rect.height != rect.height) + { + context_wayland->cursor_rect = rect; + notify_im_change (GTK_IM_CONTEXT_WAYLAND (context), + ZWP_TEXT_INPUT_V3_CHANGE_CAUSE_OTHER); + } } static void @@ -917,9 +1282,16 @@ gtk_im_context_wayland_activate_osk_with_event (GtkIMContext *context, if (global == NULL) return FALSE; - zwp_text_input_v3_enable (global->text_input); - notify_im_change (GTK_IM_CONTEXT_WAYLAND (context), - ZWP_TEXT_INPUT_V3_CHANGE_CAUSE_OTHER); + if (zwp_text_input_v3_get_version (global->text_input) >= + ZWP_TEXT_INPUT_V3_SHOW_INPUT_PANEL_SINCE_VERSION) + zwp_text_input_v3_show_input_panel (global->text_input); + else + { + zwp_text_input_v3_enable (global->text_input); + notify_im_change (GTK_IM_CONTEXT_WAYLAND (context), + ZWP_TEXT_INPUT_V3_CHANGE_CAUSE_OTHER); + } + return TRUE; } @@ -952,6 +1324,13 @@ on_content_type_changed (GtkIMContextWayland *context) commit_state (context); } +static void +unset_css_node (GtkCssNode *node) +{ + gtk_css_node_set_parent (node, NULL); + g_object_unref (node); +} + static void gtk_im_context_wayland_init (GtkIMContextWayland *context) { @@ -960,4 +1339,6 @@ gtk_im_context_wayland_init (GtkIMContextWayland *context) G_CALLBACK (on_content_type_changed), context); g_signal_connect_swapped (context, "notify::input-hints", G_CALLBACK (on_content_type_changed), context); + context->style_css_nodes = + g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify) unset_css_node); } diff --git a/gtk/gtkimmulticontext.c b/gtk/gtkimmulticontext.c index c4e32aa69b89b9d5d77bceb64e779e93a095611b..425eb9393dd5cc3dd951f73892fa82879e641cc1 100644 --- a/gtk/gtkimmulticontext.c +++ b/gtk/gtkimmulticontext.c @@ -20,6 +20,7 @@ #include #include +#include "gtkimcontextprivate.h" #include "gtkimmulticontext.h" #include "gtkimmoduleprivate.h" #include "gtklabel.h" @@ -206,6 +207,8 @@ gtk_im_multicontext_set_delegate (GtkIMMulticontext *multicontext, gtk_im_multicontext_delete_surrounding_cb, multicontext); + gtk_im_context_set_parent_node (GTK_IM_CONTEXT (priv->delegate), NULL); + if (priv->client_widget) gtk_im_context_set_client_widget (priv->delegate, NULL); @@ -220,6 +223,8 @@ gtk_im_multicontext_set_delegate (GtkIMMulticontext *multicontext, if (priv->delegate) { + GtkCssNode *parent_node; + g_object_ref (priv->delegate); propagate_purpose (multicontext); @@ -243,6 +248,9 @@ gtk_im_multicontext_set_delegate (GtkIMMulticontext *multicontext, G_CALLBACK (gtk_im_multicontext_delete_surrounding_cb), multicontext); + parent_node = gtk_im_context_get_parent_node (GTK_IM_CONTEXT (multicontext)); + gtk_im_context_set_parent_node (GTK_IM_CONTEXT (delegate), parent_node); + if (!priv->use_preedit) /* Default is TRUE */ gtk_im_context_set_use_preedit (delegate, FALSE); if (priv->client_widget) diff --git a/gtk/gtktext.c b/gtk/gtktext.c index 050ef6a9dc5d9e4bff953c0e370a19fa6a5a0935..7a176fd7957e11fc3629fc0f4451a27a209f221d 100644 --- a/gtk/gtktext.c +++ b/gtk/gtktext.c @@ -155,10 +155,9 @@ * ├── undershoot.left * ├── undershoot.right * ├── [selection] - * ├── [cursor-handle[.top] - * ├── [cursor-handle.bottom] * ├── [block-cursor] * ├── [cursor-handle[.top/.bottom][.insertion-cursor]] + * ├── [preedit[.whole][.selection][.prediction][.prefix/.suffix][.spelling-error][.compose-error]] * ╰── [window.popup] * ``` * @@ -182,6 +181,19 @@ * there is just a single handle for the text cursor, it gets the style class * `.insertion-cursor`. * + * If using an input method with a pre-edit buffer, this string will be styled + * with the `preedit` CSS node, the different style classes express the possible + * roles of a piece of text in the pre-edit buffer: + * + * - `.whole` denotes the parts of the pre-edit buffer without a special role + * - `.selection`, `.prefix` and `.suffix` style classes will be used to + * highlight the specific portions of the pre-edit buffer being edited and its + * surroundings + * - `.prediction` will be used for parts of the pre-edit buffer not typed by the + * user (e.g. autocompletion) + * - `.spelling-error` and `.compose-error` will be respectively used to indicate + * errors in spelling or character composition (e.g. non-existent transliterations). + * * # Accessibility * * `GtkText` uses the [enum@Gtk.AccessibleRole.none] role, which causes it to be diff --git a/gtk/gtktextview.c b/gtk/gtktextview.c index 6da0240c87f5851896566dcd73563af76e31e992..29d6af2d92fa25dfe192e42140451dba19e9bf23 100644 --- a/gtk/gtktextview.c +++ b/gtk/gtktextview.c @@ -40,6 +40,7 @@ #include "gtkdragsourceprivate.h" #include "gtkdropcontrollermotion.h" #include "gtkemojichooser.h" +#include "gtkimcontextprivate.h" #include "gtkimmulticontext.h" #include "gtkjoinedmenuprivate.h" #include "gtkmagnifierprivate.h" @@ -123,6 +124,7 @@ * ├── border.top * ├── border.left * ├── text + * │ ├── [preedit[.whole][.selection][.prediction][.prefix/.suffix][.spelling-error][.compose-error]] * │ ╰── [selection] * ├── border.right * ├── border.bottom @@ -139,6 +141,19 @@ * If a context menu is opened, the window node will appear as a subnode * of the main node. * + * If using an input method with a pre-edit buffer, this string will be styled + * with a `preedit` subnode of the `text` node. the different style classes + * express the possible roles of a piece of text in the pre-edit buffer: + * + * - `.whole` denotes the parts of the pre-edit buffer without a special role + * - `.selection`, `.prefix` and `.suffix` style classes will be used to + * highlight the specific portions of the pre-edit buffer being edited and its + * surroundings + * - `.prediction` will be used for parts of the pre-edit buffer not typed by the + * user (e.g. autocompletion) + * - `.spelling-error` and `.compose-error` will be respectively used to indicate + * errors in spelling or character composition (e.g. non-existent transliterations). + * * ## Accessibility * * `GtkTextView` uses the [enum@Gtk.AccessibleRole.text_box] role. @@ -5230,8 +5245,9 @@ gtk_text_view_realize (GtkWidget *widget) if (gtk_widget_is_sensitive (widget)) { - gtk_im_context_set_client_widget (GTK_TEXT_VIEW (widget)->priv->im_context, - widget); + gtk_im_context_set_parent_node (priv->im_context, + priv->text_window->css_node); + gtk_im_context_set_client_widget (priv->im_context, widget); } gtk_text_view_ensure_layout (text_view); @@ -5267,6 +5283,7 @@ gtk_text_view_unrealize (GtkWidget *widget) g_clear_pointer (&priv->popup_menu, gtk_widget_unparent); gtk_im_context_set_client_widget (priv->im_context, NULL); + gtk_im_context_set_parent_node (priv->im_context, NULL); GTK_WIDGET_CLASS (gtk_text_view_parent_class)->unrealize (widget); } diff --git a/gtk/theme/Default/_common.scss b/gtk/theme/Default/_common.scss index c39aaadc735e0622f227fa8b641a6f3be2868868..96ff1eda890c23de123b551534d0e74c30fb344a 100644 --- a/gtk/theme/Default/_common.scss +++ b/gtk/theme/Default/_common.scss @@ -87,6 +87,35 @@ textview { } } +preedit { + &.whole { + text-decoration: underline; + } + + &.selection { + background-color: $backdrop_selected_bg_color; + color: transparent; + } + + &.prediction { + font-style: italic; + } + + &.prefix, &.suffix { + color: gtkalpha(currentColor, 0.5); + } + + &.spelling-error { + text-decoration: $error_color underline wavy; + } + + &.compose-error { + text-decoration: $error_color line-through; + } +} + +textview > text > preedit { @extend preedit; } + textview > border { background-color: mix($bg_color, $base_color, 50%); } iconview { @@ -356,6 +385,10 @@ entry { } } + > text > preedit { + @extend preedit; + } + // entry error and warning style @each $e_type, $e_color in (error, $error_color), (warning, $warning_color) {