feat(overlay): add position customization for overlay placement

- Introduce PositionX and PositionY fields in OverlaySettings to allow manual positioning of the overlay.
- Update validation to ensure PositionX and PositionY are not less than -1.
- Modify diskConfig to include new position fields for JSON serialization.
- Implement functionality to save overlay position during drag events in the GUI.
- Enhance overlay placement logic to respect user-defined positions while ensuring proper display behavior.
This commit is contained in:
2026-07-17 19:55:00 +03:00
parent 41867429a5
commit 5b4a0d0785
6 changed files with 313 additions and 70 deletions
+11 -6
View File
@@ -15,13 +15,18 @@ cd -- "$repo_root"
# Avoid host/container DMABUF incompatibilities on mismatched GPU stacks. An
# explicit GTK renderer choice still takes precedence.
export GSK_RENDERER="${GSK_RENDERER:-cairo}"
# gtk4-layer-shell must load before libwayland-client or its Wayland shim is
# skipped and the overlay sits as an unpositioned normal window.
layer_shell_lib="$(pkg-config --variable=libdir gtk4-layer-shell-0)/libgtk4-layer-shell.so.0"
if [[ -f "$layer_shell_lib" ]]; then
export LD_PRELOAD="${layer_shell_lib}${LD_PRELOAD:+:$LD_PRELOAD}"
fi
# gtk4-layer-shell must precede libwayland-client in the link line. Do NOT
# LD_PRELOAD it here: the shim breaks the main GtkApplication window on
# Wayland when forced onto the whole process.
export CGO_LDFLAGS="-Wl,--no-as-needed -lgtk4-layer-shell ${CGO_LDFLAGS:-}"
desktop_bin=build/captioneer-desktop
if [[ -x "$desktop_bin" ]]; then
printf "captioneer: launching %s\\n" "$desktop_bin" >&2
exec "./$desktop_bin" "$@"
fi
printf "captioneer: no build/captioneer-desktop yet; compiling with go run (first launch is slow)\\n" >&2
exec go run -tags gtk ./src/cmd/captioneer-desktop "$@"'
exec distrobox enter --no-workdir --name "$container_name" -- \
+12
View File
@@ -76,6 +76,10 @@ type OverlaySettings struct {
Monitor string
FinalTimeout time.Duration
ClickThrough bool
// PositionX/PositionY are monitor-local logical pixels. -1 means automatic
// placement (horizontally centered, BottomMargin above the bottom edge).
PositionX int
PositionY int
}
func DefaultOverlaySettings() OverlaySettings {
@@ -89,6 +93,8 @@ func DefaultOverlaySettings() OverlaySettings {
Monitor: "auto",
FinalTimeout: MinimumOverlayFinalLifetime,
ClickThrough: true,
PositionX: -1,
PositionY: -1,
}
}
@@ -202,6 +208,12 @@ func (s OverlaySettings) Validate() error {
if s.Monitor == "" {
return errors.New("--overlay-monitor cannot be empty")
}
if s.PositionX < -1 {
return errors.New("overlay position x cannot be less than -1")
}
if s.PositionY < -1 {
return errors.New("overlay position y cannot be less than -1")
}
return nil
}
+15
View File
@@ -44,6 +44,8 @@ type diskConfig struct {
Monitor string `json:"monitor"`
FinalTimeout string `json:"final_timeout"`
ClickThrough bool `json:"click_through"`
PositionX *int `json:"position_x,omitempty"`
PositionY *int `json:"position_y,omitempty"`
} `json:"overlay"`
TranscriptLog struct {
Enabled bool `json:"enabled"`
@@ -163,6 +165,10 @@ func diskFromConfig(config AppConfig) diskConfig {
disk.Overlay.Monitor = config.Overlay.Monitor
disk.Overlay.FinalTimeout = config.Overlay.FinalTimeout.String()
disk.Overlay.ClickThrough = config.Overlay.ClickThrough
positionX := config.Overlay.PositionX
positionY := config.Overlay.PositionY
disk.Overlay.PositionX = &positionX
disk.Overlay.PositionY = &positionY
disk.TranscriptLog.Enabled = config.TranscriptLog.Enabled
disk.TranscriptLog.Directory = config.TranscriptLog.Directory
return disk
@@ -185,6 +191,13 @@ func (disk diskConfig) toConfig() (AppConfig, error) {
if err != nil {
return AppConfig{}, err
}
positionX, positionY := -1, -1
if disk.Overlay.PositionX != nil {
positionX = *disk.Overlay.PositionX
}
if disk.Overlay.PositionY != nil {
positionY = *disk.Overlay.PositionY
}
return AppConfig{
Version: disk.Version,
GUI: GUISettings{
@@ -218,6 +231,8 @@ func (disk diskConfig) toConfig() (AppConfig, error) {
Monitor: disk.Overlay.Monitor,
FinalTimeout: finalTimeout,
ClickThrough: disk.Overlay.ClickThrough,
PositionX: positionX,
PositionY: positionY,
},
TranscriptLog: TranscriptLogSettings{
Enabled: disk.TranscriptLog.Enabled,
+16 -1
View File
@@ -143,13 +143,16 @@ func (a *Application) activate() {
a.buildWindow()
a.installActions()
a.subscribeModels()
overlay.SetPositionSaver(a.saveOverlayPosition)
if a.launch.Warning != nil {
a.log.Add(diagnostics.Warning, "configuration", fmt.Sprintf("%v; using built-in defaults", a.launch.Warning))
}
// Show the main window before creating the overlay so a Layer Shell /
// display failure cannot leave the user with a silent no-GUI launch.
a.window.Present()
if err := a.configureOutputs(a.config, true); err != nil {
a.log.Add(diagnostics.Error, "outputs", err.Error())
}
a.window.Present()
a.applyAlwaysOnTop(a.config.GUI.MainWindowAlwaysOnTop)
if a.config.GUI.StartCaptions {
a.startCaptions()
@@ -395,6 +398,18 @@ func (a *Application) restartCaptions() {
}()
}
func (a *Application) saveOverlayPosition(x, y int) {
a.config.Overlay.PositionX = x
a.config.Overlay.PositionY = y
a.applied.Overlay.PositionX = x
a.applied.Overlay.PositionY = y
if err := config.Save(a.launch.Path, a.config); err != nil {
a.log.Add(diagnostics.Error, "configuration", fmt.Sprintf("Save overlay position: %v", err))
return
}
a.log.Add(diagnostics.Info, "overlay", fmt.Sprintf("Saved caption position (%d, %d)", x, y))
}
func (a *Application) configureOutputs(next config.AppConfig, initial bool) error {
var outputErr error
a.history.SetEnabled(next.Outputs.History)
+3 -1
View File
@@ -170,7 +170,7 @@ func (a *Application) overlaySettingsPage() *gtk.ScrolledWindow {
box.Append(settingRow("Maximum width", "Maximum overlay width in pixels.", a.settings.overlayMaxWidth))
box.Append(settingRow("Monitor", "Use auto or the configured monitor connector/name.", a.settings.overlayMonitor))
box.Append(settingRow("Final caption lifetime", "Zero keeps final text visible; otherwise use 3s or longer.", a.settings.overlayFinalTimeout))
box.Append(settingRow("Click through overlay", "Allow pointer input to pass through the caption surface.", a.settings.overlayClickThrough))
box.Append(settingRow("Click through overlay", "Pass pointer input through empty areas. Hold Ctrl and drag the caption bubble to reposition it; the spot is remembered.", a.settings.overlayClickThrough))
return settingsScroller(box)
}
@@ -272,6 +272,8 @@ func (a *Application) readSettings() (config.AppConfig, error) {
Monitor: strings.TrimSpace(a.settings.overlayMonitor.Text()),
FinalTimeout: finalTimeout,
ClickThrough: a.settings.overlayClickThrough.Active(),
PositionX: a.config.Overlay.PositionX,
PositionY: a.config.Overlay.PositionY,
},
TranscriptLog: config.TranscriptLogSettings{
Enabled: a.settings.transcriptEnabled.Active(),
+253 -59
View File
@@ -33,6 +33,16 @@ typedef struct {
gboolean final_visible;
gboolean preview_visible;
gboolean x11;
gboolean layer_shell;
gboolean click_through;
gboolean dragging;
int bottom_margin;
int position_x;
int position_y;
double drag_start_px;
double drag_start_py;
int drag_origin_x;
int drag_origin_y;
} CaptioneerOverlay;
typedef struct {
@@ -41,6 +51,9 @@ typedef struct {
char *preview_text;
} CaptioneerUpdate;
// Implemented on the Go side via //export so drag placement can be persisted.
extern void captioneerOverlayPositionChanged(int x, int y);
static void captioneer_append_warning(GString *warnings, const char *message) {
if (warnings->len > 0) {
g_string_append(warnings, " ");
@@ -126,15 +139,198 @@ static GdkMonitor *captioneer_select_monitor(
return monitor;
}
static void captioneer_set_input_passthrough(CaptioneerOverlay *overlay, GString *warnings) {
if (!gdk_display_supports_input_shapes(gtk_widget_get_display(GTK_WIDGET(overlay->window)))) {
captioneer_append_warning(warnings, "This display backend does not support pointer click-through.");
static GdkMonitor *captioneer_overlay_monitor(CaptioneerOverlay *overlay) {
if (overlay->monitor != NULL) {
return overlay->monitor;
}
GdkDisplay *display = gtk_widget_get_display(GTK_WIDGET(overlay->window));
GdkSurface *surface = gtk_native_get_surface(GTK_NATIVE(overlay->window));
if (surface != NULL) {
GdkMonitor *monitor = gdk_display_get_monitor_at_surface(display, surface);
if (monitor != NULL) {
return monitor;
}
}
return captioneer_monitor_by_index(display, 0);
}
static void captioneer_update_input_region(CaptioneerOverlay *overlay) {
GdkSurface *surface = gtk_native_get_surface(GTK_NATIVE(overlay->window));
if (surface == NULL) {
return;
}
GdkSurface *surface = gtk_native_get_surface(GTK_NATIVE(overlay->window));
if (!overlay->click_through || overlay->dragging) {
gdk_surface_set_input_region(surface, NULL);
return;
}
// Limit hit-testing to the caption bubble so the transparent full-width
// chrome stays click-through. CTRL+drag still works because clicks that
// land on the bubble include modifier state.
int bubble_width = gtk_widget_get_width(overlay->bubble);
int bubble_height = gtk_widget_get_height(overlay->bubble);
if (bubble_width <= 0 || bubble_height <= 0) {
cairo_region_t *empty = cairo_region_create();
gdk_surface_set_input_region(surface, empty);
cairo_region_destroy(empty);
return;
}
double native_x = 0, native_y = 0;
if (!gtk_widget_translate_coordinates(overlay->bubble, GTK_WIDGET(overlay->window), 0, 0, &native_x, &native_y)) {
native_x = 0;
native_y = 0;
}
cairo_rectangle_int_t rect = {
.x = (int)native_x,
.y = (int)native_y,
.width = bubble_width,
.height = bubble_height,
};
cairo_region_t *region = cairo_region_create_rectangle(&rect);
gdk_surface_set_input_region(surface, region);
cairo_region_destroy(region);
}
static void captioneer_apply_placement(CaptioneerOverlay *overlay) {
GdkSurface *surface = gtk_native_get_surface(GTK_NATIVE(overlay->window));
if (surface == NULL) {
return;
}
GdkMonitor *monitor = captioneer_overlay_monitor(overlay);
if (monitor == NULL) {
return;
}
GdkRectangle geometry;
gdk_monitor_get_geometry(monitor, &geometry);
int width = gdk_surface_get_width(surface);
int height = gdk_surface_get_height(surface);
if (width <= 1 || height <= 1) {
return;
}
int x = overlay->position_x;
int y = overlay->position_y;
if (x < 0) {
x = (geometry.width - width) / 2;
}
if (y < 0) {
y = geometry.height - height - overlay->bottom_margin;
}
if (x < 0) {
x = 0;
}
if (y < 0) {
y = 0;
}
if (x + width > geometry.width) {
x = MAX(0, geometry.width - width);
}
if (y + height > geometry.height) {
y = MAX(0, geometry.height - height);
}
if (overlay->layer_shell) {
gtk_layer_set_anchor(overlay->window, GTK_LAYER_SHELL_EDGE_LEFT, TRUE);
gtk_layer_set_anchor(overlay->window, GTK_LAYER_SHELL_EDGE_TOP, TRUE);
gtk_layer_set_anchor(overlay->window, GTK_LAYER_SHELL_EDGE_RIGHT, FALSE);
gtk_layer_set_anchor(overlay->window, GTK_LAYER_SHELL_EDGE_BOTTOM, FALSE);
gtk_layer_set_margin(overlay->window, GTK_LAYER_SHELL_EDGE_LEFT, x);
gtk_layer_set_margin(overlay->window, GTK_LAYER_SHELL_EDGE_TOP, y);
gtk_layer_set_margin(overlay->window, GTK_LAYER_SHELL_EDGE_RIGHT, 0);
gtk_layer_set_margin(overlay->window, GTK_LAYER_SHELL_EDGE_BOTTOM, 0);
} else if (overlay->x11) {
Display *display = GDK_SURFACE_XDISPLAY(surface);
Window xid = GDK_SURFACE_XID(surface);
int root_x = geometry.x + x;
int root_y = geometry.y + y;
XSizeHints *size_hints = XAllocSizeHints();
if (size_hints != NULL) {
size_hints->flags = USPosition | PPosition;
size_hints->x = root_x;
size_hints->y = root_y;
XSetWMNormalHints(display, xid, size_hints);
XFree(size_hints);
}
XMoveWindow(display, xid, root_x, root_y);
XFlush(display);
}
captioneer_update_input_region(overlay);
}
static gboolean captioneer_reposition_x11(gpointer data) {
captioneer_apply_placement(data);
return G_SOURCE_REMOVE;
}
static void captioneer_surface_size_changed(GObject *object, GParamSpec *spec, gpointer data) {
(void)object;
(void)spec;
g_idle_add(captioneer_reposition_x11, data);
}
static gboolean captioneer_ctrl_held(GdkEvent *event) {
return (gdk_event_get_modifier_state(event) & GDK_CONTROL_MASK) != 0;
}
static void captioneer_drag_begin(GtkGestureDrag *gesture, double x, double y, gpointer data) {
(void)x;
(void)y;
CaptioneerOverlay *overlay = data;
GdkEvent *event = gtk_event_controller_get_current_event(GTK_EVENT_CONTROLLER(gesture));
if (event == NULL || !captioneer_ctrl_held(event)) {
gtk_gesture_set_state(GTK_GESTURE(gesture), GTK_EVENT_SEQUENCE_DENIED);
return;
}
GdkSurface *surface = gtk_native_get_surface(GTK_NATIVE(overlay->window));
if (surface == NULL) {
gtk_gesture_set_state(GTK_GESTURE(gesture), GTK_EVENT_SEQUENCE_DENIED);
return;
}
overlay->dragging = TRUE;
captioneer_update_input_region(overlay);
gtk_gesture_set_state(GTK_GESTURE(gesture), GTK_EVENT_SEQUENCE_CLAIMED);
GdkMonitor *monitor = captioneer_overlay_monitor(overlay);
GdkRectangle geometry = {0};
if (monitor != NULL) {
gdk_monitor_get_geometry(monitor, &geometry);
}
int width = gdk_surface_get_width(surface);
int height = gdk_surface_get_height(surface);
int origin_x = overlay->position_x;
int origin_y = overlay->position_y;
if (origin_x < 0) {
origin_x = (geometry.width - width) / 2;
}
if (origin_y < 0) {
origin_y = geometry.height - height - overlay->bottom_margin;
}
overlay->drag_origin_x = origin_x;
overlay->drag_origin_y = origin_y;
gtk_gesture_drag_get_start_point(gesture, &overlay->drag_start_px, &overlay->drag_start_py);
}
static void captioneer_drag_update(GtkGestureDrag *gesture, double offset_x, double offset_y, gpointer data) {
CaptioneerOverlay *overlay = data;
if (!overlay->dragging) {
return;
}
(void)gesture;
overlay->position_x = overlay->drag_origin_x + (int)offset_x;
overlay->position_y = overlay->drag_origin_y + (int)offset_y;
captioneer_apply_placement(overlay);
}
static void captioneer_drag_end(GtkGestureDrag *gesture, double offset_x, double offset_y, gpointer data) {
(void)gesture;
CaptioneerOverlay *overlay = data;
if (!overlay->dragging) {
return;
}
overlay->position_x = overlay->drag_origin_x + (int)offset_x;
overlay->position_y = overlay->drag_origin_y + (int)offset_y;
overlay->dragging = FALSE;
captioneer_apply_placement(overlay);
captioneerOverlayPositionChanged(overlay->position_x, overlay->position_y);
}
static void captioneer_apply_x11_hints(CaptioneerOverlay *overlay) {
@@ -177,51 +373,11 @@ static void captioneer_apply_x11_hints(CaptioneerOverlay *overlay) {
XFlush(display);
}
static gboolean captioneer_reposition_x11(gpointer data) {
CaptioneerOverlay *overlay = data;
if (!overlay->x11 || overlay->monitor == NULL) {
return G_SOURCE_REMOVE;
}
GdkSurface *surface = gtk_native_get_surface(GTK_NATIVE(overlay->window));
if (surface == NULL) {
return G_SOURCE_REMOVE;
}
GdkRectangle geometry;
gdk_monitor_get_geometry(overlay->monitor, &geometry);
int width = gdk_surface_get_width(surface);
int height = gdk_surface_get_height(surface);
if (width <= 1 || height <= 1) {
return G_SOURCE_REMOVE;
}
int bottom_margin = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(overlay->window), "bottom-margin"));
int x = geometry.x + (geometry.width - width) / 2;
int y = geometry.y + geometry.height - height - bottom_margin;
Display *display = GDK_SURFACE_XDISPLAY(surface);
Window xid = GDK_SURFACE_XID(surface);
XSizeHints *size_hints = XAllocSizeHints();
if (size_hints != NULL) {
size_hints->flags = USPosition | PPosition;
size_hints->x = x;
size_hints->y = y;
XSetWMNormalHints(display, xid, size_hints);
XFree(size_hints);
}
XMoveWindow(display, xid, x, y);
XFlush(display);
return G_SOURCE_REMOVE;
}
static void captioneer_surface_size_changed(GObject *object, GParamSpec *spec, gpointer data) {
(void)object;
(void)spec;
g_idle_add(captioneer_reposition_x11, data);
}
static void captioneer_update_window_visibility(CaptioneerOverlay *overlay) {
gboolean visible = overlay->final_visible || overlay->preview_visible;
gtk_widget_set_visible(overlay->bubble, visible);
gtk_widget_set_visible(GTK_WIDGET(overlay->window), visible);
if (visible && overlay->x11) {
if (visible) {
g_idle_add(captioneer_reposition_x11, overlay);
}
}
@@ -236,9 +392,7 @@ static gboolean captioneer_apply_update(gpointer data) {
overlay->preview_visible = update->preview_text[0] != '\0';
if (overlay->preview_visible) {
char *preview = g_strdup_printf("… %s", update->preview_text);
gtk_label_set_text(GTK_LABEL(overlay->preview_label), preview);
g_free(preview);
gtk_label_set_text(GTK_LABEL(overlay->preview_label), update->preview_text);
} else {
gtk_label_set_text(GTK_LABEL(overlay->preview_label), "");
}
@@ -287,6 +441,8 @@ static CaptioneerOverlay *captioneer_overlay_new(
int max_width,
const char *monitor_selection,
int click_through,
int position_x,
int position_y,
char **warning_message,
char **error_message
) {
@@ -315,6 +471,10 @@ static CaptioneerOverlay *captioneer_overlay_new(
CaptioneerOverlay *overlay = g_new0(CaptioneerOverlay, 1);
overlay->loop = g_main_loop_new(NULL, FALSE);
overlay->x11 = x11;
overlay->click_through = click_through != 0;
overlay->bottom_margin = bottom_margin;
overlay->position_x = position_x;
overlay->position_y = position_y;
char *monitor_error = NULL;
if (wayland && g_strcmp0(monitor_selection, "auto") == 0) {
@@ -336,20 +496,17 @@ static CaptioneerOverlay *captioneer_overlay_new(
gtk_window_set_resizable(overlay->window, FALSE);
gtk_widget_set_focusable(GTK_WIDGET(overlay->window), FALSE);
gtk_widget_add_css_class(GTK_WIDGET(overlay->window), "captioneer-overlay");
g_object_set_data(G_OBJECT(overlay->window), "bottom-margin", GINT_TO_POINTER(bottom_margin));
g_signal_connect(overlay->window, "close-request", G_CALLBACK(captioneer_window_close), overlay);
if (wayland && gtk_layer_is_supported()) {
overlay->layer_shell = TRUE;
gtk_layer_init_for_window(overlay->window);
gtk_layer_set_namespace(overlay->window, "captioneer");
gtk_layer_set_layer(overlay->window, GTK_LAYER_SHELL_LAYER_OVERLAY);
// Stretch across the bottom edge so the compositor pins the surface
// there (single-edge anchoring can leave it floating mid-screen on
// Plasma). The bubble itself stays content-width and centered.
// Pin with LEFT+TOP margins so placement is exact. Stretching LEFT+RIGHT
// made Plasma size the surface oddly and parked captions mid-screen.
gtk_layer_set_anchor(overlay->window, GTK_LAYER_SHELL_EDGE_LEFT, TRUE);
gtk_layer_set_anchor(overlay->window, GTK_LAYER_SHELL_EDGE_RIGHT, TRUE);
gtk_layer_set_anchor(overlay->window, GTK_LAYER_SHELL_EDGE_BOTTOM, TRUE);
gtk_layer_set_margin(overlay->window, GTK_LAYER_SHELL_EDGE_BOTTOM, bottom_margin);
gtk_layer_set_anchor(overlay->window, GTK_LAYER_SHELL_EDGE_TOP, TRUE);
gtk_layer_set_exclusive_zone(overlay->window, 0);
gtk_layer_set_keyboard_mode(overlay->window, GTK_LAYER_SHELL_KEYBOARD_MODE_NONE);
if (overlay->monitor != NULL) {
@@ -384,7 +541,7 @@ static CaptioneerOverlay *captioneer_overlay_new(
overlay->bubble = box;
gtk_widget_add_css_class(box, "captioneer-bubble");
gtk_widget_set_halign(box, GTK_ALIGN_CENTER);
gtk_widget_set_valign(box, GTK_ALIGN_END);
gtk_widget_set_valign(box, GTK_ALIGN_START);
gtk_widget_set_hexpand(box, FALSE);
gtk_widget_set_vexpand(box, FALSE);
gtk_widget_set_size_request(box, effective_width, -1);
@@ -417,6 +574,14 @@ static CaptioneerOverlay *captioneer_overlay_new(
gtk_widget_add_css_class(overlay->preview_label, "captioneer-preview");
gtk_window_set_child(overlay->window, box);
GtkGesture *drag = gtk_gesture_drag_new();
gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(drag), GDK_BUTTON_PRIMARY);
gtk_event_controller_set_propagation_phase(GTK_EVENT_CONTROLLER(drag), GTK_PHASE_CAPTURE);
g_signal_connect(drag, "drag-begin", G_CALLBACK(captioneer_drag_begin), overlay);
g_signal_connect(drag, "drag-update", G_CALLBACK(captioneer_drag_update), overlay);
g_signal_connect(drag, "drag-end", G_CALLBACK(captioneer_drag_end), overlay);
gtk_widget_add_controller(overlay->bubble, GTK_EVENT_CONTROLLER(drag));
char *font_family_rule = captioneer_font_family_rule(font_family);
char *css = g_strdup_printf(
"window.captioneer-overlay { background-color: transparent; box-shadow: none; }"
@@ -433,15 +598,20 @@ static CaptioneerOverlay *captioneer_overlay_new(
gtk_widget_realize(GTK_WIDGET(overlay->window));
GdkSurface *surface = gtk_native_get_surface(GTK_NATIVE(overlay->window));
if (click_through) {
captioneer_set_input_passthrough(overlay, warnings);
if (overlay->click_through) {
if (!gdk_display_supports_input_shapes(display)) {
captioneer_append_warning(warnings, "This display backend does not support pointer click-through.");
} else {
captioneer_update_input_region(overlay);
}
}
if (x11) {
captioneer_apply_x11_hints(overlay);
}
g_signal_connect(surface, "notify::width", G_CALLBACK(captioneer_surface_size_changed), overlay);
g_signal_connect(surface, "notify::height", G_CALLBACK(captioneer_surface_size_changed), overlay);
}
gtk_widget_set_visible(GTK_WIDGET(overlay->window), FALSE);
captioneer_apply_placement(overlay);
if (warnings->len > 0) {
*warning_message = g_string_free(warnings, FALSE);
@@ -516,6 +686,28 @@ type Sink struct {
closed bool
}
var (
positionSaverMu sync.Mutex
positionSaver func(x, y int)
)
// SetPositionSaver registers a callback invoked when the user finishes a CTRL-drag.
func SetPositionSaver(save func(x, y int)) {
positionSaverMu.Lock()
positionSaver = save
positionSaverMu.Unlock()
}
//export captioneerOverlayPositionChanged
func captioneerOverlayPositionChanged(x, y C.int) {
positionSaverMu.Lock()
save := positionSaver
positionSaverMu.Unlock()
if save != nil {
save(int(x), int(y))
}
}
func New(settings config.OverlaySettings) (*Sink, string, error) {
backend := C.CString(string(settings.Backend))
fontFamily := C.CString(settings.FontFamily)
@@ -535,6 +727,8 @@ func New(settings config.OverlaySettings) (*Sink, string, error) {
C.int(settings.MaxWidth),
monitor,
boolInt(settings.ClickThrough),
C.int(settings.PositionX),
C.int(settings.PositionY),
&warningMessage,
&errorMessage,
)