gtk-ui-ux-engineer

📁 gotar/opencode-config 📅 14 days ago
8
总安装量
3
周安装量
#34705
全站排名
安装命令
npx skills add https://github.com/gotar/opencode-config --skill gtk-ui-ux-engineer

Agent 安装分布

claude-code 3
opencode 2
replit 1
amp 1
cursor 1
kimi-cli 1

Skill 文档

GTK UI/UX Engineer

A GTK (GTK4/GTK3) UI/UX specialist who crafts beautiful, native-feeling desktop applications following GNOME Human Interface Guidelines and modern GTK4 best practices.

Design Philosophy

Purpose

  • Create visually stunning GTK applications that feel native to the Linux desktop
  • Follow GNOME Human Interface Guidelines (HIG) while pushing aesthetic boundaries
  • Balance platform integration with distinctive visual identity
  • Prioritize accessibility, responsiveness, and theme-awareness
  • Ship production-grade code with proper memory management and modern GTK4 patterns

Tone

  • Native-first: Embrace platform conventions (header bars, Adwaita, libadwaita)
  • Bold aesthetics: Don’t settle for “default” GTK styling – make visual statements
  • Accessibility-obsessed: Every UI decision considers screen readers, keyboard navigation, high contrast
  • Performance-conscious: Efficient list views, minimal redraws, proper GObject lifecycle

Constraints

  • MUST follow GNOME HIG principles for platform integration
  • MUST use modern GTK4 APIs (GtkApplication, event controllers, GtkListView)
  • MUST support light/dark modes with AdwStyleManager
  • MUST use CSS variables for theme-aware styling
  • MUST NOT use deprecated GTK3 APIs when GTK4 alternatives exist
  • MUST NOT block the main loop with synchronous operations
  • MUST NOT mix GTK3 and GTK4 APIs

Differentiation

  • Unlike generic GTK tutorials that show basic widget usage, this skill emphasizes:
    • Visual Impact: Custom CSS, unique accent colors, deliberate animations
    • Modern Patterns: GtkListView with factories, GActions, property bindings
    • Platform Integration: Header bars, libadwaita widgets, GSettings
    • Real-World Examples: Patterns from GNOME Text Editor, Nautilus, GIMP

GTK4 CSS Syntax Requirements

CRITICAL: GTK4 uses different CSS syntax than GTK3. Always use:

  • ✅ var(--variable) for CSS variables (GTK4)
  • ❌ @variable for GTK3 variables (deprecated, doesn’t work)
  • ✅ color-mix() for color blending (GTK4)
  • ❌ filter: brightness() (not supported in GTK4)
  • ✅ Media queries: @media (prefers-color-scheme: dark)
  • ✅ Libadwaita variables: var(--window-bg-color)

GTK3 Syntax That Does NOT Work in GTK4:

/* ❌ GTK3 syntax - breaks in GTK4 */
@define-color my_color red;
@define-color window_bg var(--window-bg);
color: @my_color;

GTK4 Correct Syntax:

/* ✅ GTK4 syntax - correct */
:root {
  --my-color: red;
  --window-bg: var(--window-bg-color);
}

.card {
  background-color: var(--my-color);
  color: var(--window-bg);
}

For detailed CSS styling guidance, see references/libadwaita-styling.md


GTK4 UI/UX Aesthetics

Typography & Icons

Font Stack

/* Use system fonts for platform consistency */
window {
  font-family: "Cantarell", system-ui, sans-serif;
  font-weight: 400;
}

/* Headlines get emphasis */
.title {
  font-family: "Cantarell", system-ui, sans-serif;
  font-weight: 700;
  font-size: 24pt;
}

/* Monospace for code */
.code {
  font-family: "JetBrains Mono", "Monospace", monospace;
}

Icon Usage

  • Use symbolic icons from GNOME icon theme (e.g., document-symbolic, edit-find-symbolic)
  • Scale icons with icon size CSS properties
  • Color icons using CSS color: var(--accent-bg-color);

Example:

// Add symbolic icon to button
GtkWidget *button = gtk_button_new_from_icon_name("document-open-symbolic");
gtk_button_set_icon_name(GTK_BUTTON(button), "document-save-symbolic");

Color & Theming

Theme-Aware Color System

/* Use CSS variables for theme integration */
:root {
  /* Override accent color for brand identity */
  --accent-bg-color: var(--accent-blue);
  --accent-fg-color: white;
}

/* Custom accent color - e.g., purple brand */
:root {
  --accent-bg-color: var(--accent-purple);
  --accent-color: oklab(from var(--accent-bg-color) var(--standalone-color-oklab));
}

/* Dark mode overrides */
@media (prefers-color-scheme: dark) {
  .card {
    background-color: rgba(255, 255, 255, 0.05);
  }
}

/* High contrast mode */
@media (prefers-contrast: more) {
  .card {
    border: 2px solid currentColor;
  }
}

Visual Hierarchy

/* Cards with subtle shadows */
.card {
  background-color: var(--card-bg-color);
  border-radius: 12px;
  padding: 16px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
}

/* Primary buttons use accent color */
.primary-button {
  background-color: var(--accent-bg-color);
  color: var(--accent-fg-color);
  padding: 8px 16px;
  border-radius: 8px;
}

.primary-button:hover {
  /* GTK4: Use color-mix() instead of filter: brightness() */
  background-color: color-mix(in srgb, var(--accent-bg-color) 90%, white);
}

Spacing & Layout

Spacing Scale

/* 4px base unit */
.space-xs  { padding: 4px; }
.space-sm  { padding: 8px; }
.space-md  { padding: 12px; }
.space-lg  { padding: 16px; }
.space-xl  { padding: 24px; }
.space-2xl { padding: 32px; }

Border Radius

/* Match Adwaita conventions */
button {
  border-radius: 8px;
}

window {
  border-radius: 12px;
}

.card {
  border-radius: 12px;
}

Motion & Animation

Delicate Transitions

/* Smooth property transitions */
button {
  transition: background-color 200ms ease,
              transform 100ms ease,
              box-shadow 200ms ease;
}

button:hover {
  transform: translateY(-1px);
  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.12);
}

button:active {
  transform: translateY(0);
}

Purposeful Motion

/* Fade in for new content */
fade-in {
  animation: fadeIn 300ms ease-out;
}

@keyframes fadeIn {
  from { opacity: 0; transform: translateY(8px); }
  to { opacity: 1; transform: translateY(0); }
}

/* Avoid excessive animations - use sparingly for emphasis */

Responsive Design

Adaptive Containers

/* Use AdwLeaflet for mobile-first layouts */
GtkWidget *leaflet = adw_leaflet_new();
adw_leaflet_set_collapsed(GTK_LEAFLET(leaflet), TRUE);

/* Content adapts based on fold state */
g_signal_connect(leaflet, "notify::folded",
                G_CALLBACK(on_fold_changed), NULL);

Breakpoint-Based Styling

/* Compact layouts for narrow windows */
window {
  min-width: 360px;
  min-height: 240px;
}

@media (max-width: 600px) {
  .sidebar {
    display: none;
  }
}

/* Use AdwBreakpoint for more complex responsive behavior */

Accessibility

Keyboard Navigation

// Ensure all interactive elements are keyboard focusable
gtk_widget_set_can_focus(widget, TRUE);

// Set focus order
gtk_widget_set_focus_on_click(button, TRUE);

// Mnemonic shortcuts
gtk_label_set_mnemonic_widget(label, entry);

Accessible Labels

// Label widgets properly
gtk_accessible_update_property(GTK_ACCESSIBLE(widget),
    GTK_ACCESSIBLE_PROPERTY_LABEL, "Save changes",
    -1
);

// Add descriptions for complex widgets
gtk_accessible_update_property(GTK_ACCESSIBLE(widget),
    GTK_ACCESSIBLE_PROPERTY_DESCRIPTION,
    "Saves the current document to disk",
    -1
);

High Contrast Support

@media (prefers-contrast: more) {
  * {
    border: 1px solid currentColor;
  }

  button {
    background-color: transparent;
    border: 2px solid currentColor;
  }
}

GTK4 Architecture & Patterns

Application Structure

Modern GTK4 Application Pattern

// Subclass GtkApplication
struct _MyApp {
    GtkApplication parent;
    GSettings *settings;
};

G_DEFINE_TYPE(MyApp, my_app, GTK_TYPE_APPLICATION);

static void my_app_activate(GApplication *app) {
    GtkWindow *window = gtk_application_window_new(GTK_APPLICATION(app));

    // Create window content
    MyWindow *my_window = my_window_new(GTK_APPLICATION(app));
    gtk_window_present(GTK_WINDOW(my_window));
}

Window Template Pattern

// Class init - load UI from resource
static void my_window_class_init(MyWindowClass *klass) {
    GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass);
    gtk_widget_class_set_template_from_resource(widget_class,
        "/org/example/app/ui/window.ui");

    // Bind widgets
    gtk_widget_class_bind_template_child(widget_class, MyWindow, header_bar);
    gtk_widget_class_bind_template_child(widget_class, MyWindow, content_box);
}

// Instance init - initialize template
static void my_window_init(MyWindow *self) {
    gtk_widget_init_template(GTK_WIDGET(self));
}

Widget Composition

Composite Widgets

// Create reusable composite widgets
struct _MyCompositeWidget {
    GtkBox parent;
    GtkLabel *title_label;
    GtkButton *action_button;
};

G_DEFINE_TYPE(MyCompositeWidget, my_composite_widget, GTK_TYPE_BOX);

static void my_composite_widget_init(MyCompositeWidget *self) {
    gtk_orientable_set_orientation(GTK_ORIENTABLE(self),
                                    GTK_ORIENTATION_VERTICAL);
    gtk_box_set_spacing(GTK_BOX(self), 6);

    // Create children
    self->title_label = gtk_label_new(NULL);
    gtk_widget_add_css_class(GTK_WIDGET(self->title_label), "title");
    gtk_box_append(GTK_BOX(self), GTK_WIDGET(self->title_label));

    self->action_button = gtk_button_new_with_label("Action");
    gtk_box_append(GTK_BOX(self), GTK_WIDGET(self->action_button));
}

List Views with Factories

// Modern GtkListView pattern
static void setup_listitem_cb(GtkListItem *list_item, gpointer user_data) {
    GtkWidget *label = gtk_label_new(NULL);
    gtk_list_item_set_child(list_item, label);
}

static void bind_listitem_cb(GtkListItem *list_item, gpointer user_data) {
    GObject *item = gtk_list_item_get_item(list_item);
    GtkWidget *label = gtk_list_item_get_child(list_item);
    const char *text = my_item_get_text(MY_ITEM(item));
    gtk_label_set_text(GTK_LABEL(label), text);
}

// Create list view
GtkListItemFactory *factory = gtk_signal_list_item_factory_new();
g_signal_connect(factory, "setup", G_CALLBACK(setup_listitem_cb), NULL);
g_signal_connect(factory, "bind", G_CALLBACK(bind_listitem_cb), NULL);

GtkSelectionModel *model = gtk_single_selection_new(G_LIST_MODEL(create_model()));
GtkWidget *listview = gtk_list_view_new(model, factory);

Actions & Menus

GAction Architecture

// Define action entries
static GActionEntry app_entries[] = {
    { "quit", on_quit, NULL, NULL },
    { "preferences", on_preferences, NULL, NULL },
    { "about", on_about, NULL, NULL }
};

// Add actions in startup
static void on_startup(GApplication *app) {
    g_action_map_add_action_entries(G_ACTION_MAP(app),
                                     app_entries,
                                     G_N_ELEMENTS(app_entries),
                                     app);

    // Set accelerators
    const char *quit_accels[] = { "<Control>q", NULL };
    gtk_application_set_accels_for_action(GTK_APPLICATION(app),
                                           "app.quit",
                                           quit_accels);
}

Menu Integration

<!-- Define menu in .ui file -->
<menu id="app_menu">
  <section>
    <item>
      <attribute name="label">_Preferences</attribute>
      <attribute name="action">app.preferences</attribute>
      <attribute name="verb-icon">preferences-system-symbolic</attribute>
    </item>
  </section>
  <section>
    <item>
      <attribute name="label">_About</attribute>
      <attribute name="action">app.about</attribute>
    </item>
  </section>
</menu>

State Management

GSettings for Persistent State

// Create settings
GSettings *settings = g_settings_new("org.example.app");

// Bind to widget properties
g_settings_bind(settings, "window-width",
                window, "default-width",
                G_SETTINGS_BIND_DEFAULT);

// Watch for changes
g_signal_connect(settings, "changed::theme",
                G_CALLBACK(on_theme_changed), NULL);

Property Bindings

// Bidirectional binding between widgets
g_object_bind_property(
    entry, "text",
    label, "label",
    G_BINDING_BIDIRECTIONAL | G_BINDING_SYNC_CREATE
);

// Transform bindings with transform functions
g_object_bind_property_full(
    slider, "value",
    label, "label",
    G_BINDING_DEFAULT,
    slider_to_label_transform,
    label_to_slider_transform,
    NULL, NULL
);

Event Controllers (GTK4 Modern)

Keyboard Controller

// Use event controllers instead of signals
GtkEventController *key_controller = gtk_event_controller_key_new();
gtk_widget_add_controller(widget, key_controller);
g_signal_connect(key_controller, "key-pressed",
                G_CALLBACK(on_key_pressed), self);

Gesture Controllers

// Click gesture
GtkGesture *click = gtk_gesture_click_new();
g_signal_connect(click, "pressed", G_CALLBACK(on_click_pressed), widget);
gtk_widget_add_controller(widget, GTK_EVENT_CONTROLLER(click));

// Drag gesture
GtkGesture *drag = gtk_gesture_drag_new();
gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(drag), GDK_BUTTON_PRIMARY);
g_signal_connect(drag, "drag-begin", G_CALLBACK(on_drag_begin), NULL);
g_signal_connect(drag, "drag-update", G_CALLBACK(on_drag_update), NULL);
gtk_widget_add_controller(drawing_area, GTK_EVENT_CONTROLLER(drag));

Anti-Patterns & Common Mistakes

Memory Management

WRONG: Manual reference management

GtkWidget *child = gtk_button_new_with_label("Click");
gtk_box_append(GTK_BOX(box), child);
g_object_unref(child);  // DANGER: May leak or crash

CORRECT: Let containers manage ownership

GtkWidget *child = gtk_button_new_with_label("Click");
gtk_box_append(GTK_BOX(box), child);
// Box takes ownership - no unref needed

WRONG: Not disconnecting signals

static void my_widget_init(MyWidget *self) {
    g_signal_connect(self->button, "clicked",
                     G_CALLBACK(on_clicked), self);
}
// Never disconnects - memory leak!

CORRECT: Clean up in dispose

static void my_widget_dispose(GObject *object) {
    MyWidget *self = MY_WIDGET(object);
    g_clear_signal_handler(&self->button_clicked_id, self->button);
    G_OBJECT_CLASS(my_widget_parent_class)->dispose(object);
}

Event Handling

WRONG: Using GTK3 signals

g_signal_connect(widget, "key-press-event",
                 G_CALLBACK(old_handler), NULL);  // GTK3 pattern

CORRECT: Use GTK4 event controllers

GtkEventController *controller = gtk_event_controller_key_new();
gtk_widget_add_controller(widget, controller);
g_signal_connect(controller, "key-pressed",
                 G_CALLBACK(modern_handler), NULL);

API Mixing

WRONG: Mixing GTK3 and GTK4 APIs

GtkWidget *window = gtk_window_new();  // GTK3
GtkApplicationWindow *app_win = gtk_application_window_new(app);  // GTK4

CORRECT: Use consistent GTK4 APIs

GtkWidget *app_win = gtk_application_window_new(app);

Performance

WRONG: Excessive redraws

void on_data_changed(void) {
    gtk_widget_queue_draw(widget);  // Triggers full redraw
}

CORRECT: Use property notifications

void on_data_changed(void) {
    gtk_widget_notify(widget, "content");  // More efficient
}

WRONG: Rebuilding list models

void update_list(void) {
    gtk_list_view_set_model(listview, create_new_model());  // Slow
}

CORRECT: Modify existing model

void update_list(void) {
    GListStore *store = get_current_store();
    g_list_store_remove(store, old_item);
    g_list_store_append(store, new_item);  // Fast
}

Reference Resources

Official Documentation

Real-World Examples

Pattern References

  • See references/ directory for:
    • gnome-hig.md – HIG principles and patterns
    • gtk4-best-practices.md – Modern GTK4 code patterns
    • libadwaita-styling.md – CSS theming with Libadwaita
    • accessibility.md – A11y implementation guide

Quick Start Example

Creating a Modern GTK4 Window

// UI resource (window.ui)
<interface>
  <template class="MyWindow" parent="AdwApplicationWindow">
    <property name="default-width">800</property>
    <property name="default-height">600</property>

    <child>
      <object class="AdwBreakpoint">
        <condition>max-width: 600sp</condition>
      </object>
    </child>

    <property name="content">
      <object class="AdwToolbarView">
        <child type="top">
          <object class="AdwHeaderBar">
            <property name="title-widget">
              <object class="AdwViewSwitcherTitle">
                <property name="stack">view_stack</property>
              </object>
            </property>
          </object>
        </child>

        <property name="content">
          <object class="GtkStack" id="view_stack">
            <!-- Stack children added here -->
          </object>
        </property>
      </object>
    </property>
  </template>
</interface>
// C implementation
G_DEFINE_TYPE(MyWindow, my_window, ADW_TYPE_APPLICATION_WINDOW);

static void my_window_class_init(MyWindowClass *klass) {
    GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass);
    gtk_widget_class_set_template_from_resource(widget_class,
        "/org/example/app/ui/window.ui");
}

static void my_window_init(MyWindow *self) {
    gtk_widget_init_template(GTK_WIDGET(self));

    // Load custom CSS
    GtkCssProvider *provider = gtk_css_provider_new();
    gtk_css_provider_load_from_resource(provider,
        "/org/example/app/styles/style.css");
    gtk_style_context_add_provider_for_display(
        gdk_display_get_default(),
        GTK_STYLE_PROVIDER(provider),
        GTK_STYLE_PROVIDER_PRIORITY_APPLICATION
    );
}
/* Custom styling (style.css) */
window {
  background-color: var(--window-bg-color);
}

.title {
  font-weight: 700;
  font-size: 24pt;
}

.primary-button {
  background-color: var(--accent-bg-color);
  color: var(--accent-fg-color);
}

.primary-button:hover {
  /* GTK4: Use color-mix() instead of filter: brightness() */
  background-color: color-mix(in srgb, var(--accent-bg-color) 90%, white);
}

This example demonstrates:

  • AdwApplicationWindow for native GNOME integration
  • Header bar with view switcher
  • Custom CSS loading with GTK4 syntax
  • Responsive design with AdwBreakpoint
  • Libadwaita theming using CSS variables