mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-03 08:25:34 +00:00
68063633f2
When the control key is pressed, clear event.state bits before calling Xutf8LookupString in order to get the unmodified key name. This allows proper handling of all keys in combination with ModCommand. `key.Event.Name` is however layout dependent. Client code should be careful about this when picking key shortcuts like CTRL-'+': on a QWERTY keyboard, only CTRL-'=' and CTRL-SHIFT-'=' are generated when pressing the '=' key of the top row. The keypad '+' key generates events with `Name = '+'` as expected. Fixes gio#57 Signed-off-by: Denis Bernard <db047h@gmail.com>
623 lines
16 KiB
Go
623 lines
16 KiB
Go
// SPDX-License-Identifier: Unlicense OR MIT
|
|
|
|
// +build linux,!android,!nox11 freebsd
|
|
|
|
package window
|
|
|
|
/*
|
|
#cgo LDFLAGS: -lX11
|
|
#include <stdlib.h>
|
|
#include <locale.h>
|
|
#include <X11/Xlib.h>
|
|
#include <X11/Xatom.h>
|
|
#include <X11/Xutil.h>
|
|
#include <X11/Xresource.h>
|
|
#define GIO_FIELD_OFFSET(typ, field) const int gio_##typ##_##field##_off = offsetof(typ, field)
|
|
GIO_FIELD_OFFSET(XClientMessageEvent, data);
|
|
GIO_FIELD_OFFSET(XExposeEvent, count);
|
|
GIO_FIELD_OFFSET(XConfigureEvent, width);
|
|
GIO_FIELD_OFFSET(XConfigureEvent, height);
|
|
GIO_FIELD_OFFSET(XButtonEvent, x);
|
|
GIO_FIELD_OFFSET(XButtonEvent, y);
|
|
GIO_FIELD_OFFSET(XButtonEvent, state);
|
|
GIO_FIELD_OFFSET(XButtonEvent, button);
|
|
GIO_FIELD_OFFSET(XButtonEvent, time);
|
|
GIO_FIELD_OFFSET(XMotionEvent, x);
|
|
GIO_FIELD_OFFSET(XMotionEvent, y);
|
|
GIO_FIELD_OFFSET(XMotionEvent, time);
|
|
GIO_FIELD_OFFSET(XKeyEvent, state);
|
|
|
|
void gio_x11_init_ime(Display *dpy, Window win, XIM *xim, XIC *xic) {
|
|
// adjust locale temporarily for XOpenIM
|
|
char *lc = setlocale(LC_CTYPE, NULL);
|
|
setlocale(LC_CTYPE, "");
|
|
XSetLocaleModifiers("");
|
|
|
|
*xim = XOpenIM(dpy, 0, 0, 0);
|
|
if (!*xim) {
|
|
// fallback to internal input method
|
|
XSetLocaleModifiers("@im=none");
|
|
*xim = XOpenIM(dpy, 0, 0, 0);
|
|
}
|
|
|
|
// revert locale to prevent any unexpected side effects
|
|
setlocale(LC_CTYPE, lc);
|
|
|
|
*xic = XCreateIC(*xim,
|
|
XNInputStyle, XIMPreeditNothing | XIMStatusNothing,
|
|
XNClientWindow, win,
|
|
XNFocusWindow, win,
|
|
NULL);
|
|
|
|
XSetICFocus(*xic);
|
|
}
|
|
*/
|
|
import "C"
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"image"
|
|
"strconv"
|
|
"sync"
|
|
"time"
|
|
"unicode"
|
|
"unicode/utf8"
|
|
"unsafe"
|
|
|
|
"gioui.org/f32"
|
|
"gioui.org/io/key"
|
|
"gioui.org/io/pointer"
|
|
"gioui.org/io/system"
|
|
syscall "golang.org/x/sys/unix"
|
|
)
|
|
|
|
type x11Window struct {
|
|
w Callbacks
|
|
x *C.Display
|
|
xw C.Window
|
|
|
|
evDelWindow C.Atom
|
|
stage system.Stage
|
|
cfg config
|
|
width int
|
|
height int
|
|
xim C.XIM
|
|
xic C.XIC
|
|
notify struct {
|
|
read, write int
|
|
}
|
|
dead bool
|
|
|
|
mu sync.Mutex
|
|
animating bool
|
|
}
|
|
|
|
func (w *x11Window) SetAnimating(anim bool) {
|
|
w.mu.Lock()
|
|
w.animating = anim
|
|
w.mu.Unlock()
|
|
if anim {
|
|
w.wakeup()
|
|
}
|
|
}
|
|
|
|
func (w *x11Window) ShowTextInput(show bool) {}
|
|
|
|
var x11OneByte = make([]byte, 1)
|
|
|
|
func (w *x11Window) wakeup() {
|
|
if _, err := syscall.Write(w.notify.write, x11OneByte); err != nil && err != syscall.EAGAIN {
|
|
panic(fmt.Errorf("failed to write to pipe: %v", err))
|
|
}
|
|
}
|
|
|
|
func (w *x11Window) display() unsafe.Pointer {
|
|
return unsafe.Pointer(w.x)
|
|
}
|
|
|
|
func (w *x11Window) setStage(s system.Stage) {
|
|
if s == w.stage {
|
|
return
|
|
}
|
|
w.stage = s
|
|
w.w.Event(system.StageEvent{s})
|
|
}
|
|
|
|
func (w *x11Window) loop() {
|
|
h := x11EventHandler{w: w, xev: new(xEvent), text: make([]byte, 4)}
|
|
xfd := C.XConnectionNumber(w.x)
|
|
|
|
// Poll for events and notifications.
|
|
pollfds := []syscall.PollFd{
|
|
{Fd: int32(xfd), Events: syscall.POLLIN | syscall.POLLERR},
|
|
{Fd: int32(w.notify.read), Events: syscall.POLLIN | syscall.POLLERR},
|
|
}
|
|
xEvents := &pollfds[0].Revents
|
|
// Plenty of room for a backlog of notifications.
|
|
buf := make([]byte, 100)
|
|
|
|
loop:
|
|
for !w.dead {
|
|
var syn, redraw bool
|
|
// Check for pending draw events before checking animation or blocking.
|
|
// This fixes an issue on Xephyr where on startup XPending() > 0 but
|
|
// poll will still block. This also prevents no-op calls to poll.
|
|
if syn = h.handleEvents(); !syn {
|
|
w.mu.Lock()
|
|
animating := w.animating
|
|
w.mu.Unlock()
|
|
if animating {
|
|
redraw = true
|
|
} else {
|
|
// Clear poll events.
|
|
*xEvents = 0
|
|
// Wait for X event or gio notification.
|
|
if _, err := syscall.Poll(pollfds, -1); err != nil && err != syscall.EINTR {
|
|
panic(fmt.Errorf("x11 loop: poll failed: %w", err))
|
|
}
|
|
switch {
|
|
case *xEvents&syscall.POLLIN != 0:
|
|
syn = h.handleEvents()
|
|
if w.dead {
|
|
break loop
|
|
}
|
|
case *xEvents&(syscall.POLLERR|syscall.POLLHUP) != 0:
|
|
break loop
|
|
}
|
|
}
|
|
}
|
|
// Clear notifications.
|
|
for {
|
|
_, err := syscall.Read(w.notify.read, buf)
|
|
if err == syscall.EAGAIN {
|
|
break
|
|
}
|
|
if err != nil {
|
|
panic(fmt.Errorf("x11 loop: read from notify pipe failed: %w", err))
|
|
}
|
|
redraw = true
|
|
}
|
|
|
|
if redraw || syn {
|
|
w.cfg.now = time.Now()
|
|
w.w.Event(FrameEvent{
|
|
FrameEvent: system.FrameEvent{
|
|
Size: image.Point{
|
|
X: w.width,
|
|
Y: w.height,
|
|
},
|
|
Config: &w.cfg,
|
|
},
|
|
Sync: syn,
|
|
})
|
|
}
|
|
}
|
|
w.w.Event(system.DestroyEvent{Err: nil})
|
|
}
|
|
|
|
func (w *x11Window) destroy() {
|
|
if w.notify.write != 0 {
|
|
syscall.Close(w.notify.write)
|
|
w.notify.write = 0
|
|
}
|
|
if w.notify.read != 0 {
|
|
syscall.Close(w.notify.read)
|
|
w.notify.read = 0
|
|
}
|
|
C.XDestroyIC(w.xic)
|
|
C.XCloseIM(w.xim)
|
|
C.XDestroyWindow(w.x, w.xw)
|
|
C.XCloseDisplay(w.x)
|
|
}
|
|
|
|
// x11EventHandler wraps static variables for the main event loop.
|
|
// Its sole purpose is to prevent heap allocation and reduce clutter
|
|
// in x11window.loop.
|
|
//
|
|
type x11EventHandler struct {
|
|
w *x11Window
|
|
text []byte
|
|
xev *xEvent
|
|
status C.Status
|
|
keysym C.KeySym
|
|
}
|
|
|
|
// handleEvents returns true if the window needs to be redrawn.
|
|
//
|
|
func (h *x11EventHandler) handleEvents() bool {
|
|
w := h.w
|
|
xev := h.xev
|
|
redraw := false
|
|
for C.XPending(w.x) != 0 {
|
|
C.XNextEvent(w.x, (*C.XEvent)(unsafe.Pointer(xev)))
|
|
if C.XFilterEvent((*C.XEvent)(unsafe.Pointer(xev)), C.None) == C.True {
|
|
continue
|
|
}
|
|
switch xev.Type {
|
|
case C.KeyPress:
|
|
lookup:
|
|
// Save state then clear CTRL & Shift bits in order to have
|
|
// Xutf8LookupString return the unmodified key name in text[:l].
|
|
//
|
|
// Note that this enables sending a key.Event for key combinations
|
|
// like CTRL-SHIFT-/ on QWERTY layouts, but CTRL-? is completely
|
|
// masked. The same applies to AZERTY layouts where CTRL-SHIFT-É is
|
|
// available but not CTRL-2.
|
|
state := xev.GetKeyState()
|
|
mods := x11KeyStateToModifiers(state)
|
|
if mods.Contain(key.ModCommand) {
|
|
xev.SetKeyState(state & ^(C.uint(C.ControlMask) | C.uint(C.ShiftMask)))
|
|
}
|
|
l := int(C.Xutf8LookupString(w.xic,
|
|
(*C.XKeyPressedEvent)(unsafe.Pointer(xev)),
|
|
(*C.char)(unsafe.Pointer(&h.text[0])), C.int(len(h.text)),
|
|
&h.keysym, &h.status))
|
|
switch h.status {
|
|
case C.XBufferOverflow:
|
|
h.text = make([]byte, l)
|
|
goto lookup
|
|
case C.XLookupChars:
|
|
// Synthetic event from XIM.
|
|
w.w.Event(key.EditEvent{Text: string(h.text[:l])})
|
|
case C.XLookupKeySym:
|
|
// Special keys.
|
|
if r, ok := x11SpecialKeySymToRune(h.keysym); ok {
|
|
w.w.Event(key.Event{
|
|
Name: r,
|
|
Modifiers: mods,
|
|
})
|
|
}
|
|
case C.XLookupBoth:
|
|
if r, ok := x11SpecialKeySymToRune(h.keysym); ok {
|
|
w.w.Event(key.Event{Name: r, Modifiers: mods})
|
|
} else {
|
|
if r, _ = utf8.DecodeRune(h.text[:l]); r != utf8.RuneError {
|
|
w.w.Event(key.Event{Name: unicode.ToUpper(r), Modifiers: mods})
|
|
}
|
|
// Send EditEvent only when not a CTRL key combination.
|
|
if !mods.Contain(key.ModCommand) {
|
|
w.w.Event(key.EditEvent{Text: string(h.text[:l])})
|
|
}
|
|
}
|
|
}
|
|
case C.KeyRelease:
|
|
case C.ButtonPress, C.ButtonRelease:
|
|
ev := pointer.Event{
|
|
Type: pointer.Press,
|
|
Source: pointer.Mouse,
|
|
Position: f32.Point{
|
|
X: float32(xev.GetButtonX()),
|
|
Y: float32(xev.GetButtonY()),
|
|
},
|
|
Time: xev.GetButtonTime(),
|
|
}
|
|
if xev.Type == C.ButtonRelease {
|
|
ev.Type = pointer.Release
|
|
}
|
|
const scrollScale = 10
|
|
switch xev.GetButtonButton() {
|
|
case C.Button1:
|
|
// left click by default
|
|
case C.Button4:
|
|
// scroll up
|
|
ev.Type = pointer.Move
|
|
ev.Scroll.Y = -scrollScale
|
|
case C.Button5:
|
|
// scroll down
|
|
ev.Type = pointer.Move
|
|
ev.Scroll.Y = +scrollScale
|
|
default:
|
|
continue
|
|
}
|
|
w.w.Event(ev)
|
|
case C.MotionNotify:
|
|
w.w.Event(pointer.Event{
|
|
Type: pointer.Move,
|
|
Source: pointer.Mouse,
|
|
Position: f32.Point{
|
|
X: float32(xev.GetMotionX()),
|
|
Y: float32(xev.GetMotionY()),
|
|
},
|
|
Time: xev.GetMotionTime(),
|
|
})
|
|
case C.Expose: // update
|
|
// redraw only on the last expose event
|
|
redraw = xev.GetExposeCount() == 0
|
|
case C.FocusIn:
|
|
w.w.Event(key.FocusEvent{Focus: true})
|
|
case C.FocusOut:
|
|
w.w.Event(key.FocusEvent{Focus: false})
|
|
case C.ConfigureNotify: // window configuration change
|
|
w.width = int(xev.GetConfigureWidth())
|
|
w.height = int(xev.GetConfigureHeight())
|
|
// redraw will be done by a later expose event
|
|
case C.ClientMessage: // extensions
|
|
switch xev.GetClientDataLong()[0] {
|
|
case C.long(w.evDelWindow):
|
|
w.dead = true
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
return redraw
|
|
}
|
|
|
|
func x11KeyStateToModifiers(s C.uint) key.Modifiers {
|
|
var m key.Modifiers
|
|
if s&C.ControlMask != 0 {
|
|
m |= key.ModCommand
|
|
}
|
|
if s&C.ShiftMask != 0 {
|
|
m |= key.ModShift
|
|
}
|
|
return m
|
|
}
|
|
|
|
func x11SpecialKeySymToRune(s C.KeySym) (rune, bool) {
|
|
var n rune
|
|
switch s {
|
|
case C.XK_Escape:
|
|
n = key.NameEscape
|
|
case C.XK_Left, C.XK_KP_Left:
|
|
n = key.NameLeftArrow
|
|
case C.XK_Right, C.XK_KP_Right:
|
|
n = key.NameRightArrow
|
|
case C.XK_Return:
|
|
n = key.NameReturn
|
|
case C.XK_KP_Enter:
|
|
n = key.NameEnter
|
|
case C.XK_Up, C.XK_KP_Up:
|
|
n = key.NameUpArrow
|
|
case C.XK_Down, C.XK_KP_Down:
|
|
n = key.NameDownArrow
|
|
case C.XK_Home, C.XK_KP_Home:
|
|
n = key.NameHome
|
|
case C.XK_End, C.XK_KP_End:
|
|
n = key.NameEnd
|
|
case C.XK_BackSpace:
|
|
n = key.NameDeleteBackward
|
|
case C.XK_Delete, C.XK_KP_Delete:
|
|
n = key.NameDeleteForward
|
|
case C.XK_Page_Up, C.XK_KP_Prior:
|
|
n = key.NamePageUp
|
|
case C.XK_Page_Down, C.XK_KP_Next:
|
|
n = key.NamePageDown
|
|
default:
|
|
return 0, false
|
|
}
|
|
return n, true
|
|
}
|
|
|
|
const xEventSize = unsafe.Sizeof(C.XEvent{})
|
|
|
|
// Make sure the Go struct has the same size.
|
|
// We can't use C.XEvent directly because it's a union.
|
|
var _ = [1]struct{}{}[unsafe.Sizeof(xEvent{})-xEventSize]
|
|
|
|
type xEvent struct {
|
|
Type C.int
|
|
Data [xEventSize - unsafe.Sizeof(C.int(0))]byte
|
|
}
|
|
|
|
func (e *xEvent) getInt(off int) C.int {
|
|
return *(*C.int)(unsafe.Pointer(uintptr(unsafe.Pointer(e)) + uintptr(off)))
|
|
}
|
|
|
|
func (e *xEvent) getUint(off int) C.uint {
|
|
return *(*C.uint)(unsafe.Pointer(uintptr(unsafe.Pointer(e)) + uintptr(off)))
|
|
}
|
|
|
|
func (e *xEvent) setUint(off int, v C.uint) {
|
|
*(*C.uint)(unsafe.Pointer(uintptr(unsafe.Pointer(e)) + uintptr(off))) = v
|
|
}
|
|
|
|
func (e *xEvent) getUlong(off int) C.ulong {
|
|
return *(*C.ulong)(unsafe.Pointer(uintptr(unsafe.Pointer(e)) + uintptr(off)))
|
|
}
|
|
|
|
func (e *xEvent) getUlongMs(off int) time.Duration {
|
|
return time.Duration(e.getUlong(off)) * time.Millisecond
|
|
}
|
|
|
|
// GetExposeCount returns a XEvent.xexpose.count field.
|
|
func (e *xEvent) GetExposeCount() C.int {
|
|
return e.getInt(int(C.gio_XExposeEvent_count_off))
|
|
}
|
|
|
|
// GetConfigureWidth returns a XEvent.xconfigure.width field.
|
|
func (e *xEvent) GetConfigureWidth() C.int {
|
|
return e.getInt(int(C.gio_XConfigureEvent_width_off))
|
|
}
|
|
|
|
// GetConfigureWidth returns a XEvent.xconfigure.height field.
|
|
func (e *xEvent) GetConfigureHeight() C.int {
|
|
return e.getInt(int(C.gio_XConfigureEvent_height_off))
|
|
}
|
|
|
|
// GetButtonX returns a XEvent.xbutton.x field.
|
|
func (e *xEvent) GetButtonX() C.int {
|
|
return e.getInt(int(C.gio_XButtonEvent_x_off))
|
|
}
|
|
|
|
// GetButtonY returns a XEvent.xbutton.y field.
|
|
func (e *xEvent) GetButtonY() C.int {
|
|
return e.getInt(int(C.gio_XButtonEvent_y_off))
|
|
}
|
|
|
|
// GetButtonState returns a XEvent.xbutton.state field.
|
|
func (e *xEvent) GetButtonState() C.uint {
|
|
return e.getUint(int(C.gio_XButtonEvent_state_off))
|
|
}
|
|
|
|
// GetButtonButton returns a XEvent.xbutton.button field.
|
|
func (e *xEvent) GetButtonButton() C.uint {
|
|
return e.getUint(int(C.gio_XButtonEvent_button_off))
|
|
}
|
|
|
|
// GetButtonTime returns a XEvent.xbutton.time field.
|
|
func (e *xEvent) GetButtonTime() time.Duration {
|
|
return e.getUlongMs(int(C.gio_XButtonEvent_time_off))
|
|
}
|
|
|
|
// GetMotionX returns a XEvent.xmotion.x field.
|
|
func (e *xEvent) GetMotionX() C.int {
|
|
return e.getInt(int(C.gio_XMotionEvent_x_off))
|
|
}
|
|
|
|
// GetMotionY returns a XEvent.xmotion.y field.
|
|
func (e *xEvent) GetMotionY() C.int {
|
|
return e.getInt(int(C.gio_XMotionEvent_y_off))
|
|
}
|
|
|
|
// GetMotionTime returns a XEvent.xmotion.time field.
|
|
func (e *xEvent) GetMotionTime() time.Duration {
|
|
return e.getUlongMs(int(C.gio_XMotionEvent_time_off))
|
|
}
|
|
|
|
// GetClientDataLong returns a XEvent.xclient.data.l field.
|
|
func (e *xEvent) GetClientDataLong() [5]C.long {
|
|
ptr := (*[5]C.long)(unsafe.Pointer(uintptr(unsafe.Pointer(e)) + uintptr(C.gio_XClientMessageEvent_data_off)))
|
|
return *ptr
|
|
}
|
|
|
|
// GetKeyState returns a XKeyEvent.state field.
|
|
func (e *xEvent) GetKeyState() C.uint {
|
|
return e.getUint(int(C.gio_XKeyEvent_state_off))
|
|
}
|
|
|
|
// GetKeyState returns a XKeyEvent.state field.
|
|
func (e *xEvent) SetKeyState(v C.uint) {
|
|
e.setUint(int(C.gio_XKeyEvent_state_off), v)
|
|
}
|
|
|
|
var (
|
|
x11Threads sync.Once
|
|
)
|
|
|
|
func init() {
|
|
x11Driver = newX11Window
|
|
}
|
|
|
|
func newX11Window(gioWin Callbacks, opts *Options) error {
|
|
var err error
|
|
|
|
pipe := make([]int, 2)
|
|
if err := syscall.Pipe2(pipe, syscall.O_NONBLOCK|syscall.O_CLOEXEC); err != nil {
|
|
return fmt.Errorf("NewX11Window: failed to create pipe: %w", err)
|
|
}
|
|
|
|
x11Threads.Do(func() {
|
|
if C.XInitThreads() == 0 {
|
|
err = errors.New("x11: threads init failed")
|
|
}
|
|
C.XrmInitialize()
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
dpy := C.XOpenDisplay(nil)
|
|
if dpy == nil {
|
|
return errors.New("x11: cannot connect to the X server")
|
|
}
|
|
|
|
root := C.XDefaultRootWindow(dpy)
|
|
screen := C.XDefaultScreen(dpy)
|
|
ppsp := x11DetectUIScale(dpy, screen)
|
|
cfg := config{pxPerDp: ppsp, pxPerSp: ppsp}
|
|
var (
|
|
swa C.XSetWindowAttributes
|
|
xim C.XIM
|
|
xic C.XIC
|
|
)
|
|
swa.event_mask = C.ExposureMask | C.PointerMotionMask | C.KeyPressMask
|
|
win := C.XCreateWindow(dpy, root,
|
|
0, 0, C.uint(cfg.Px(opts.Width)), C.uint(cfg.Px(opts.Height)), 0,
|
|
C.CopyFromParent, C.InputOutput,
|
|
nil, C.CWEventMask|C.CWBackPixel,
|
|
&swa)
|
|
C.gio_x11_init_ime(dpy, win, &xim, &xic)
|
|
C.XSelectInput(dpy, win, 0|
|
|
C.ExposureMask|C.FocusChangeMask| // update
|
|
C.KeyPressMask|C.KeyReleaseMask| // keyboard
|
|
C.ButtonPressMask|C.ButtonReleaseMask| // mouse clicks
|
|
C.PointerMotionMask| // mouse movement
|
|
C.StructureNotifyMask, // resize
|
|
)
|
|
|
|
w := &x11Window{
|
|
w: gioWin, x: dpy, xw: win,
|
|
width: cfg.Px(opts.Width),
|
|
height: cfg.Px(opts.Height),
|
|
cfg: cfg,
|
|
xim: xim,
|
|
xic: xic,
|
|
}
|
|
w.notify.read = pipe[0]
|
|
w.notify.write = pipe[1]
|
|
|
|
var xattr C.XSetWindowAttributes
|
|
xattr.override_redirect = C.False
|
|
C.XChangeWindowAttributes(dpy, win, C.CWOverrideRedirect, &xattr)
|
|
|
|
var hints C.XWMHints
|
|
hints.input = C.True
|
|
hints.flags = C.InputHint
|
|
C.XSetWMHints(dpy, win, &hints)
|
|
|
|
// make the window visible on the screen
|
|
C.XMapWindow(dpy, win)
|
|
|
|
// set the name
|
|
ctitle := C.CString(opts.Title)
|
|
C.XStoreName(dpy, win, ctitle)
|
|
C.free(unsafe.Pointer(ctitle))
|
|
|
|
// extensions
|
|
ckey := C.CString("WM_DELETE_WINDOW")
|
|
w.evDelWindow = C.XInternAtom(dpy, ckey, C.False)
|
|
C.free(unsafe.Pointer(ckey))
|
|
C.XSetWMProtocols(dpy, win, &w.evDelWindow, 1)
|
|
|
|
go func() {
|
|
w.w.SetDriver(w)
|
|
w.setStage(system.StageRunning)
|
|
w.loop()
|
|
w.destroy()
|
|
close(mainDone)
|
|
}()
|
|
return nil
|
|
}
|
|
|
|
// detectUIScale reports the system UI scale, or 1.0 if it fails.
|
|
func x11DetectUIScale(dpy *C.Display, screen C.int) float32 {
|
|
// default fixed DPI value used in most desktop UI toolkits
|
|
const defaultDesktopDPI = 96
|
|
var scale float32 = 1.0
|
|
|
|
// Get actual DPI from X resource Xft.dpi (set by GTK and Qt).
|
|
// This value is entirely based on user preferences and conflates both
|
|
// screen (UI) scaling and font scale.
|
|
rms := C.XResourceManagerString(dpy)
|
|
if rms != nil {
|
|
db := C.XrmGetStringDatabase(rms)
|
|
if db != nil {
|
|
var (
|
|
t *C.char
|
|
v C.XrmValue
|
|
)
|
|
if C.XrmGetResource(db, (*C.char)(unsafe.Pointer(&[]byte("Xft.dpi\x00")[0])),
|
|
(*C.char)(unsafe.Pointer(&[]byte("Xft.Dpi\x00")[0])), &t, &v) != C.False {
|
|
if t != nil && C.GoString(t) == "String" {
|
|
f, err := strconv.ParseFloat(C.GoString(v.addr), 32)
|
|
if err == nil {
|
|
scale = float32(f) / defaultDesktopDPI
|
|
}
|
|
}
|
|
}
|
|
C.XrmDestroyDatabase(db)
|
|
}
|
|
}
|
|
|
|
return scale
|
|
}
|