Rewrite WMFontPanel in Rust #25
@@ -41,7 +41,6 @@ libWINGs_la_SOURCES = \
|
||||
wevent.c \
|
||||
wfilepanel.c \
|
||||
wframe.c \
|
||||
wfontpanel.c \
|
||||
widgets.c \
|
||||
winputmethod.c \
|
||||
wlabel.c \
|
||||
|
||||
@@ -4,14 +4,17 @@ AUTOMAKE_OPTIONS =
|
||||
|
||||
noinst_PROGRAMS = wtest wmquery wmfile testmywidget
|
||||
|
||||
LDADD= $(top_builddir)/WINGs/libWINGs.la $(top_builddir)/wrlib/libwraster.la \
|
||||
LDADD= $(top_builddir)/WINGs/libWINGs.la \
|
||||
$(top_builddir)/wrlib/libwraster.la \
|
||||
$(top_builddir)/WINGs/libWUtil.la \
|
||||
@XFT_LIBS@ @INTLIBS@ @XLIBS@
|
||||
$(top_builddir)/WINGs/wings-rs/target/debug/libwings_rs.a \
|
||||
@XFT_LIBS@ @INTLIBS@ @XLIBS@ @FCLIBS@ @PANGO_LIBS@
|
||||
|
||||
|
||||
testmywidget_SOURCES = testmywidget.c mywidget.c mywidget.h
|
||||
|
||||
wtest_DEPENDENCIES = $(top_builddir)/WINGs/libWINGs.la
|
||||
wtest_DEPENDENCIES = $(top_builddir)/WINGs/libWINGs.la \
|
||||
$(top_builddir)/WINGs/wings-rs/target/debug/libwings_rs.a
|
||||
|
||||
|
||||
EXTRA_DIST = logo.xpm upbtn.xpm wm.html wm.png
|
||||
|
||||
@@ -132,7 +132,7 @@ typedef enum {
|
||||
} WMButtonType;
|
||||
|
||||
/* button behaviour masks */
|
||||
enum {
|
||||
typedef enum {
|
||||
WBBSpringLoadedMask = (1 << 0),
|
||||
WBBPushInMask = (1 << 1),
|
||||
WBBPushChangeMask = (1 << 2),
|
||||
@@ -140,7 +140,7 @@ enum {
|
||||
WBBStateLightMask = (1 << 5),
|
||||
WBBStateChangeMask = (1 << 6),
|
||||
WBBStatePushMask = (1 << 7)
|
||||
};
|
||||
} WMButtonBehaviorMask;
|
||||
|
||||
|
||||
/* frame title positions */
|
||||
|
||||
@@ -122,8 +122,6 @@ typedef struct W_Screen {
|
||||
WMOpenPanel *sharedOpenPanel;
|
||||
WMSavePanel *sharedSavePanel;
|
||||
|
||||
struct W_FontPanel *sharedFontPanel;
|
||||
|
||||
struct W_ColorPanel *sharedColorPanel;
|
||||
|
||||
Pixmap stipple;
|
||||
|
||||
@@ -1,825 +0,0 @@
|
||||
|
||||
#include "WINGsP.h"
|
||||
#include "WUtil.h"
|
||||
#include "wconfig.h"
|
||||
|
||||
#include <ctype.h>
|
||||
#include <string.h>
|
||||
#include <strings.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include <X11/Xft/Xft.h>
|
||||
#include <fontconfig/fontconfig.h>
|
||||
|
||||
/* XXX TODO */
|
||||
char *WMFontPanelFontChangedNotification = "WMFontPanelFontChangedNotification";
|
||||
|
||||
typedef struct W_FontPanel {
|
||||
WMWindow *win;
|
||||
|
||||
WMFrame *upperF;
|
||||
WMTextField *sampleT;
|
||||
|
||||
WMSplitView *split;
|
||||
|
||||
WMFrame *lowerF;
|
||||
WMLabel *famL;
|
||||
WMList *famLs;
|
||||
WMLabel *typL;
|
||||
WMList *typLs;
|
||||
WMLabel *sizL;
|
||||
WMTextField *sizT;
|
||||
WMList *sizLs;
|
||||
|
||||
WMAction2 *action;
|
||||
void *data;
|
||||
|
||||
WMButton *revertB;
|
||||
WMButton *setB;
|
||||
|
||||
WMPropList *fdb;
|
||||
} FontPanel;
|
||||
|
||||
#define MIN_UPPER_HEIGHT 20
|
||||
#define MIN_LOWER_HEIGHT 140
|
||||
|
||||
#define BUTTON_SPACE_HEIGHT 40
|
||||
|
||||
#define MIN_WIDTH 250
|
||||
#define MIN_HEIGHT (MIN_UPPER_HEIGHT+MIN_LOWER_HEIGHT+BUTTON_SPACE_HEIGHT)
|
||||
|
||||
#define DEF_UPPER_HEIGHT 60
|
||||
#define DEF_LOWER_HEIGHT 310
|
||||
|
||||
#define DEF_WIDTH 320
|
||||
#define DEF_HEIGHT (DEF_UPPER_HEIGHT+DEF_LOWER_HEIGHT)
|
||||
|
||||
static const int scalableFontSizes[] = {
|
||||
8,
|
||||
10,
|
||||
11,
|
||||
12,
|
||||
14,
|
||||
16,
|
||||
18,
|
||||
20,
|
||||
24,
|
||||
36,
|
||||
48,
|
||||
64
|
||||
};
|
||||
|
||||
static void setFontPanelFontName(FontPanel * panel, const char *family, const char *style, double size);
|
||||
|
||||
static int isXLFD(const char *font, int *length_ret);
|
||||
|
||||
static void arrangeLowerFrame(FontPanel * panel);
|
||||
|
||||
static void familyClick(WMWidget *, void *);
|
||||
static void typefaceClick(WMWidget *, void *);
|
||||
static void sizeClick(WMWidget *, void *);
|
||||
|
||||
static void listFamilies(WMScreen * scr, WMFontPanel * panel);
|
||||
|
||||
static void splitViewConstrainCallback(WMSplitView * sPtr, int indView, int *min, int *max)
|
||||
{
|
||||
/* Parameter not used, but tell the compiler that it is ok */
|
||||
(void) sPtr;
|
||||
(void) max;
|
||||
|
||||
if (indView == 0)
|
||||
*min = MIN_UPPER_HEIGHT;
|
||||
else
|
||||
*min = MIN_LOWER_HEIGHT;
|
||||
}
|
||||
|
||||
static void notificationObserver(void *self, WMNotification * notif)
|
||||
{
|
||||
WMFontPanel *panel = (WMFontPanel *) self;
|
||||
void *object = WMGetNotificationObject(notif);
|
||||
|
||||
if (WMGetNotificationName(notif) == WMViewSizeDidChangeNotification) {
|
||||
if (object == WMWidgetView(panel->win)) {
|
||||
int h = WMWidgetHeight(panel->win);
|
||||
int w = WMWidgetWidth(panel->win);
|
||||
|
||||
WMResizeWidget(panel->split, w, h - BUTTON_SPACE_HEIGHT);
|
||||
|
||||
WMMoveWidget(panel->setB, w - 80, h - (BUTTON_SPACE_HEIGHT - 5));
|
||||
|
||||
WMMoveWidget(panel->revertB, w - 240, h - (BUTTON_SPACE_HEIGHT - 5));
|
||||
|
||||
} else if (object == WMWidgetView(panel->upperF)) {
|
||||
|
||||
if (WMWidgetHeight(panel->upperF) < MIN_UPPER_HEIGHT) {
|
||||
WMResizeWidget(panel->upperF, WMWidgetWidth(panel->upperF), MIN_UPPER_HEIGHT);
|
||||
} else {
|
||||
WMResizeWidget(panel->sampleT, WMWidgetWidth(panel->upperF) - 20,
|
||||
WMWidgetHeight(panel->upperF) - 10);
|
||||
}
|
||||
|
||||
} else if (object == WMWidgetView(panel->lowerF)) {
|
||||
|
||||
if (WMWidgetHeight(panel->lowerF) < MIN_LOWER_HEIGHT) {
|
||||
WMResizeWidget(panel->upperF, WMWidgetWidth(panel->upperF), MIN_UPPER_HEIGHT);
|
||||
|
||||
WMMoveWidget(panel->lowerF, 0, WMWidgetHeight(panel->upperF)
|
||||
+ WMGetSplitViewDividerThickness(panel->split));
|
||||
|
||||
WMResizeWidget(panel->lowerF, WMWidgetWidth(panel->lowerF),
|
||||
WMWidgetWidth(panel->split) - MIN_UPPER_HEIGHT
|
||||
- WMGetSplitViewDividerThickness(panel->split));
|
||||
} else {
|
||||
arrangeLowerFrame(panel);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void closeWindow(WMWidget * w, void *data)
|
||||
{
|
||||
FontPanel *panel = (FontPanel *) data;
|
||||
|
||||
/* Parameter not used, but tell the compiler that it is ok */
|
||||
(void) w;
|
||||
|
||||
WMHideFontPanel(panel);
|
||||
}
|
||||
|
||||
static void setClickedAction(WMWidget * w, void *data)
|
||||
{
|
||||
FontPanel *panel = (FontPanel *) data;
|
||||
|
||||
/* Parameter not used, but tell the compiler that it is ok */
|
||||
(void) w;
|
||||
|
||||
if (panel->action)
|
||||
(*panel->action) (panel, panel->data);
|
||||
}
|
||||
|
||||
static void revertClickedAction(WMWidget * w, void *data)
|
||||
{
|
||||
/* Parameter not used, but tell the compiler that it is ok */
|
||||
(void) w;
|
||||
(void) data;
|
||||
|
||||
/*FontPanel *panel = (FontPanel*)data; */
|
||||
/* XXX TODO */
|
||||
}
|
||||
|
||||
WMFontPanel *WMGetFontPanel(WMScreen * scr)
|
||||
{
|
||||
FontPanel *panel;
|
||||
WMColor *dark, *white;
|
||||
WMFont *font;
|
||||
int divThickness;
|
||||
|
||||
if (scr->sharedFontPanel)
|
||||
return scr->sharedFontPanel;
|
||||
|
||||
panel = wmalloc(sizeof(FontPanel));
|
||||
|
||||
panel->win = WMCreateWindow(scr, "fontPanel");
|
||||
/* WMSetWidgetBackgroundColor(panel->win, WMWhiteColor(scr)); */
|
||||
WMSetWindowTitle(panel->win, _("Font Panel"));
|
||||
WMResizeWidget(panel->win, DEF_WIDTH, DEF_HEIGHT);
|
||||
WMSetWindowMinSize(panel->win, MIN_WIDTH, MIN_HEIGHT);
|
||||
WMSetViewNotifySizeChanges(WMWidgetView(panel->win), True);
|
||||
|
||||
WMSetWindowCloseAction(panel->win, closeWindow, panel);
|
||||
|
||||
panel->split = WMCreateSplitView(panel->win);
|
||||
WMResizeWidget(panel->split, DEF_WIDTH, DEF_HEIGHT - BUTTON_SPACE_HEIGHT);
|
||||
WMSetSplitViewConstrainProc(panel->split, splitViewConstrainCallback);
|
||||
|
||||
divThickness = WMGetSplitViewDividerThickness(panel->split);
|
||||
|
||||
panel->upperF = WMCreateFrame(panel->win);
|
||||
WMSetFrameRelief(panel->upperF, WRFlat);
|
||||
WMSetViewNotifySizeChanges(WMWidgetView(panel->upperF), True);
|
||||
panel->lowerF = WMCreateFrame(panel->win);
|
||||
/* WMSetWidgetBackgroundColor(panel->lowerF, WMBlackColor(scr)); */
|
||||
WMSetFrameRelief(panel->lowerF, WRFlat);
|
||||
WMSetViewNotifySizeChanges(WMWidgetView(panel->lowerF), True);
|
||||
|
||||
WMAddSplitViewSubview(panel->split, W_VIEW(panel->upperF));
|
||||
WMAddSplitViewSubview(panel->split, W_VIEW(panel->lowerF));
|
||||
|
||||
WMResizeWidget(panel->upperF, DEF_WIDTH, DEF_UPPER_HEIGHT);
|
||||
|
||||
WMResizeWidget(panel->lowerF, DEF_WIDTH, DEF_LOWER_HEIGHT);
|
||||
|
||||
WMMoveWidget(panel->lowerF, 0, 60 + divThickness);
|
||||
|
||||
white = WMWhiteColor(scr);
|
||||
dark = WMDarkGrayColor(scr);
|
||||
|
||||
panel->sampleT = WMCreateTextField(panel->upperF);
|
||||
WMResizeWidget(panel->sampleT, DEF_WIDTH - 20, 50);
|
||||
WMMoveWidget(panel->sampleT, 10, 10);
|
||||
WMSetTextFieldText(panel->sampleT, _("The quick brown fox jumps over the lazy dog"));
|
||||
|
||||
font = WMBoldSystemFontOfSize(scr, 12);
|
||||
|
||||
panel->famL = WMCreateLabel(panel->lowerF);
|
||||
WMSetWidgetBackgroundColor(panel->famL, dark);
|
||||
WMSetLabelText(panel->famL, _("Family"));
|
||||
WMSetLabelFont(panel->famL, font);
|
||||
WMSetLabelTextColor(panel->famL, white);
|
||||
WMSetLabelRelief(panel->famL, WRSunken);
|
||||
WMSetLabelTextAlignment(panel->famL, WACenter);
|
||||
|
||||
panel->famLs = WMCreateList(panel->lowerF);
|
||||
WMSetListAction(panel->famLs, familyClick, panel);
|
||||
|
||||
panel->typL = WMCreateLabel(panel->lowerF);
|
||||
WMSetWidgetBackgroundColor(panel->typL, dark);
|
||||
WMSetLabelText(panel->typL, _("Typeface"));
|
||||
WMSetLabelFont(panel->typL, font);
|
||||
WMSetLabelTextColor(panel->typL, white);
|
||||
WMSetLabelRelief(panel->typL, WRSunken);
|
||||
WMSetLabelTextAlignment(panel->typL, WACenter);
|
||||
|
||||
panel->typLs = WMCreateList(panel->lowerF);
|
||||
WMSetListAction(panel->typLs, typefaceClick, panel);
|
||||
|
||||
panel->sizL = WMCreateLabel(panel->lowerF);
|
||||
WMSetWidgetBackgroundColor(panel->sizL, dark);
|
||||
WMSetLabelText(panel->sizL, _("Size"));
|
||||
WMSetLabelFont(panel->sizL, font);
|
||||
WMSetLabelTextColor(panel->sizL, white);
|
||||
WMSetLabelRelief(panel->sizL, WRSunken);
|
||||
WMSetLabelTextAlignment(panel->sizL, WACenter);
|
||||
|
||||
panel->sizT = WMCreateTextField(panel->lowerF);
|
||||
/* WMSetTextFieldAlignment(panel->sizT, WARight); */
|
||||
|
||||
panel->sizLs = WMCreateList(panel->lowerF);
|
||||
WMSetListAction(panel->sizLs, sizeClick, panel);
|
||||
|
||||
WMReleaseFont(font);
|
||||
WMReleaseColor(white);
|
||||
WMReleaseColor(dark);
|
||||
|
||||
panel->setB = WMCreateCommandButton(panel->win);
|
||||
WMResizeWidget(panel->setB, 70, 24);
|
||||
WMMoveWidget(panel->setB, 240, DEF_HEIGHT - (BUTTON_SPACE_HEIGHT - 5));
|
||||
WMSetButtonText(panel->setB, _("Set"));
|
||||
WMSetButtonAction(panel->setB, setClickedAction, panel);
|
||||
|
||||
panel->revertB = WMCreateCommandButton(panel->win);
|
||||
WMResizeWidget(panel->revertB, 70, 24);
|
||||
WMMoveWidget(panel->revertB, 80, DEF_HEIGHT - (BUTTON_SPACE_HEIGHT - 5));
|
||||
WMSetButtonText(panel->revertB, _("Revert"));
|
||||
WMSetButtonAction(panel->revertB, revertClickedAction, panel);
|
||||
|
||||
WMRealizeWidget(panel->win);
|
||||
|
||||
WMMapSubwidgets(panel->upperF);
|
||||
WMMapSubwidgets(panel->lowerF);
|
||||
WMMapSubwidgets(panel->split);
|
||||
WMMapSubwidgets(panel->win);
|
||||
|
||||
WMUnmapWidget(panel->revertB);
|
||||
|
||||
arrangeLowerFrame(panel);
|
||||
|
||||
scr->sharedFontPanel = panel;
|
||||
|
||||
/* register notification observers */
|
||||
WMAddNotificationObserver(notificationObserver, panel,
|
||||
WMViewSizeDidChangeNotification, WMWidgetView(panel->win));
|
||||
WMAddNotificationObserver(notificationObserver, panel,
|
||||
WMViewSizeDidChangeNotification, WMWidgetView(panel->upperF));
|
||||
WMAddNotificationObserver(notificationObserver, panel,
|
||||
WMViewSizeDidChangeNotification, WMWidgetView(panel->lowerF));
|
||||
|
||||
listFamilies(scr, panel);
|
||||
|
||||
return panel;
|
||||
}
|
||||
|
||||
void WMFreeFontPanel(WMFontPanel * panel)
|
||||
{
|
||||
if (panel == WMWidgetScreen(panel->win)->sharedFontPanel) {
|
||||
WMWidgetScreen(panel->win)->sharedFontPanel = NULL;
|
||||
}
|
||||
WMRemoveNotificationObserver(panel);
|
||||
WMUnmapWidget(panel->win);
|
||||
WMDestroyWidget(panel->win);
|
||||
wfree(panel);
|
||||
}
|
||||
|
||||
void WMShowFontPanel(WMFontPanel * panel)
|
||||
{
|
||||
WMMapWidget(panel->win);
|
||||
}
|
||||
|
||||
void WMHideFontPanel(WMFontPanel * panel)
|
||||
{
|
||||
WMUnmapWidget(panel->win);
|
||||
}
|
||||
|
||||
WMFont *WMGetFontPanelFont(WMFontPanel * panel)
|
||||
{
|
||||
return WMGetTextFieldFont(panel->sampleT);
|
||||
}
|
||||
|
||||
void WMSetFontPanelFont(WMFontPanel * panel, const char *fontName)
|
||||
{
|
||||
int fname_len;
|
||||
FcPattern *pattern;
|
||||
FcChar8 *family, *style;
|
||||
double size;
|
||||
|
||||
if (!isXLFD(fontName, &fname_len)) {
|
||||
/* maybe its proper fontconfig and we can parse it */
|
||||
pattern = FcNameParse((const FcChar8 *) fontName);
|
||||
} else {
|
||||
/* maybe its proper xlfd and we can convert it to an FcPattern */
|
||||
pattern = XftXlfdParse(fontName, False, False);
|
||||
/*//FcPatternPrint(pattern); */
|
||||
}
|
||||
|
||||
if (!pattern)
|
||||
return;
|
||||
|
||||
if (FcPatternGetString(pattern, FC_FAMILY, 0, &family) == FcResultMatch)
|
||||
if (FcPatternGetString(pattern, FC_STYLE, 0, &style) == FcResultMatch)
|
||||
if (FcPatternGetDouble(pattern, "pixelsize", 0, &size) == FcResultMatch)
|
||||
setFontPanelFontName(panel, (char *)family, (char *)style, size);
|
||||
|
||||
FcPatternDestroy(pattern);
|
||||
}
|
||||
|
||||
void WMSetFontPanelAction(WMFontPanel * panel, WMAction2 * action, void *data)
|
||||
{
|
||||
panel->action = action;
|
||||
panel->data = data;
|
||||
}
|
||||
|
||||
static void arrangeLowerFrame(FontPanel * panel)
|
||||
{
|
||||
int width = WMWidgetWidth(panel->lowerF) - 55 - 30;
|
||||
int height = WMWidgetHeight(panel->split) - WMWidgetHeight(panel->upperF);
|
||||
int fw, tw, sw;
|
||||
|
||||
#define LABEL_HEIGHT 20
|
||||
|
||||
height -= WMGetSplitViewDividerThickness(panel->split);
|
||||
|
||||
height -= LABEL_HEIGHT + 8;
|
||||
|
||||
fw = (125 * width) / 235;
|
||||
tw = (110 * width) / 235;
|
||||
sw = 55;
|
||||
|
||||
WMMoveWidget(panel->famL, 10, 0);
|
||||
WMResizeWidget(panel->famL, fw, LABEL_HEIGHT);
|
||||
|
||||
WMMoveWidget(panel->famLs, 10, 23);
|
||||
WMResizeWidget(panel->famLs, fw, height);
|
||||
|
||||
WMMoveWidget(panel->typL, 10 + fw + 3, 0);
|
||||
WMResizeWidget(panel->typL, tw, LABEL_HEIGHT);
|
||||
|
||||
WMMoveWidget(panel->typLs, 10 + fw + 3, 23);
|
||||
WMResizeWidget(panel->typLs, tw, height);
|
||||
|
||||
WMMoveWidget(panel->sizL, 10 + fw + 3 + tw + 3, 0);
|
||||
WMResizeWidget(panel->sizL, sw + 4, LABEL_HEIGHT);
|
||||
|
||||
WMMoveWidget(panel->sizT, 10 + fw + 3 + tw + 3, 23);
|
||||
WMResizeWidget(panel->sizT, sw + 4, 20);
|
||||
|
||||
WMMoveWidget(panel->sizLs, 10 + fw + 3 + tw + 3, 46);
|
||||
WMResizeWidget(panel->sizLs, sw + 4, height - 23);
|
||||
}
|
||||
|
||||
#define NUM_FIELDS 14
|
||||
|
||||
static int isXLFD(const char *font, int *length_ret)
|
||||
{
|
||||
int c = 0;
|
||||
|
||||
*length_ret = 0;
|
||||
while (*font) {
|
||||
(*length_ret)++;
|
||||
if (*font++ == '-')
|
||||
c++;
|
||||
}
|
||||
|
||||
return c == NUM_FIELDS;
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
char *typeface;
|
||||
WMArray *sizes;
|
||||
} Typeface;
|
||||
|
||||
typedef struct {
|
||||
char *name; /* gotta love simplicity */
|
||||
WMArray *typefaces;
|
||||
} Family;
|
||||
|
||||
static int compare_int(const void *a, const void *b)
|
||||
{
|
||||
int i1 = *(int *)a;
|
||||
int i2 = *(int *)b;
|
||||
|
||||
if (i1 < i2)
|
||||
return -1;
|
||||
else if (i1 > i2)
|
||||
return 1;
|
||||
else
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void addSizeToTypeface(Typeface * face, int size)
|
||||
{
|
||||
if (size == 0) {
|
||||
int j;
|
||||
|
||||
for (j = 0; j < wlengthof(scalableFontSizes); j++) {
|
||||
size = scalableFontSizes[j];
|
||||
|
||||
if (!WMCountInArray(face->sizes, (void *)(uintptr_t) size)) {
|
||||
WMAddToArray(face->sizes, (void *)(uintptr_t) size);
|
||||
}
|
||||
}
|
||||
WMSortArray(face->sizes, compare_int);
|
||||
} else {
|
||||
if (!WMCountInArray(face->sizes, (void *)(uintptr_t) size)) {
|
||||
WMAddToArray(face->sizes, (void *)(uintptr_t) size);
|
||||
WMSortArray(face->sizes, compare_int);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void addTypefaceToXftFamily(Family * fam, const char *style)
|
||||
{
|
||||
Typeface *face;
|
||||
WMArrayIterator i;
|
||||
|
||||
if (fam->typefaces) {
|
||||
WM_ITERATE_ARRAY(fam->typefaces, face, i) {
|
||||
if (strcmp(face->typeface, style) != 0)
|
||||
continue; /* go to next interation */
|
||||
addSizeToTypeface(face, 0);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
fam->typefaces = WMCreateArray(4);
|
||||
}
|
||||
|
||||
face = wmalloc(sizeof(Typeface));
|
||||
|
||||
face->typeface = wstrdup(style);
|
||||
face->sizes = WMCreateArray(4);
|
||||
addSizeToTypeface(face, 0);
|
||||
|
||||
WMAddToArray(fam->typefaces, face);
|
||||
}
|
||||
|
||||
/*
|
||||
* families (same family name) (Hashtable of family -> array)
|
||||
* registries (same family but different registries)
|
||||
*
|
||||
*/
|
||||
static void addFontToXftFamily(WMHashTable * families, const char *name, const char *style)
|
||||
{
|
||||
WMArrayIterator i;
|
||||
WMArray *array;
|
||||
Family *fam;
|
||||
|
||||
array = WMHashGet(families, name);
|
||||
if (array) {
|
||||
WM_ITERATE_ARRAY(array, fam, i) {
|
||||
if (strcmp(fam->name, name) == 0)
|
||||
addTypefaceToXftFamily(fam, style);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
array = WMCreateArray(8);
|
||||
|
||||
fam = wmalloc(sizeof(Family));
|
||||
|
||||
fam->name = wstrdup(name);
|
||||
|
||||
addTypefaceToXftFamily(fam, style);
|
||||
|
||||
WMAddToArray(array, fam);
|
||||
|
||||
WMHashInsert(families, fam->name, array);
|
||||
}
|
||||
|
||||
static void listFamilies(WMScreen * scr, WMFontPanel * panel)
|
||||
{
|
||||
FcObjectSet *os = 0;
|
||||
FcFontSet *fs;
|
||||
FcPattern *pat;
|
||||
WMHashTable *families;
|
||||
WMHashEnumerator *enumer;
|
||||
WMArray *array;
|
||||
int i;
|
||||
|
||||
pat = FcPatternCreate();
|
||||
os = FcObjectSetBuild(FC_FAMILY, FC_STYLE, NULL);
|
||||
fs = FcFontList(0, pat, os);
|
||||
if (!fs) {
|
||||
WMRunAlertPanel(scr, panel->win, _("Error"),
|
||||
_("Could not init font config library\n"), _("OK"), NULL, NULL);
|
||||
return;
|
||||
}
|
||||
if (pat)
|
||||
FcPatternDestroy(pat);
|
||||
|
||||
families = WMCreateStringHashTable();
|
||||
|
||||
if (fs) {
|
||||
for (i = 0; i < fs->nfont; i++) {
|
||||
FcChar8 *family;
|
||||
FcChar8 *style;
|
||||
|
||||
if (FcPatternGetString(fs->fonts[i], FC_FAMILY, 0, &family) == FcResultMatch)
|
||||
if (FcPatternGetString(fs->fonts[i], FC_STYLE, 0, &style) == FcResultMatch)
|
||||
addFontToXftFamily(families, (char *)family, (char *)style);
|
||||
}
|
||||
FcFontSetDestroy(fs);
|
||||
}
|
||||
|
||||
enumer = WMEnumerateHashTable(families);
|
||||
|
||||
while ((array = WMNextHashEnumeratorItem(enumer))) {
|
||||
WMArrayIterator i;
|
||||
Family *fam;
|
||||
char buffer[256];
|
||||
WMListItem *item;
|
||||
|
||||
WM_ITERATE_ARRAY(array, fam, i) {
|
||||
strlcpy(buffer, fam->name, sizeof(buffer));
|
||||
item = WMAddListItem(panel->famLs, buffer);
|
||||
|
||||
item->clientData = fam;
|
||||
}
|
||||
|
||||
WMFreeArray(array);
|
||||
}
|
||||
|
||||
WMSortListItems(panel->famLs);
|
||||
|
||||
WMFreeHashTable(families);
|
||||
}
|
||||
|
||||
static void getSelectedFont(FontPanel * panel, char buffer[], int bufsize)
|
||||
{
|
||||
WMListItem *item;
|
||||
Family *family;
|
||||
Typeface *face;
|
||||
char *size;
|
||||
|
||||
item = WMGetListSelectedItem(panel->famLs);
|
||||
if (!item)
|
||||
return;
|
||||
family = (Family *) item->clientData;
|
||||
|
||||
item = WMGetListSelectedItem(panel->typLs);
|
||||
if (!item)
|
||||
return;
|
||||
face = (Typeface *) item->clientData;
|
||||
|
||||
size = WMGetTextFieldText(panel->sizT);
|
||||
|
||||
snprintf(buffer, bufsize, "%s:style=%s:pixelsize=%s", family->name, face->typeface, size);
|
||||
|
||||
wfree(size);
|
||||
}
|
||||
|
||||
static void preview(FontPanel * panel)
|
||||
{
|
||||
char buffer[512];
|
||||
WMFont *font;
|
||||
|
||||
getSelectedFont(panel, buffer, sizeof(buffer));
|
||||
font = WMCreateFont(WMWidgetScreen(panel->win), buffer);
|
||||
if (font) {
|
||||
WMSetTextFieldFont(panel->sampleT, font);
|
||||
WMReleaseFont(font);
|
||||
}
|
||||
}
|
||||
|
||||
static void familyClick(WMWidget * w, void *data)
|
||||
{
|
||||
WMList *lPtr = (WMList *) w;
|
||||
WMListItem *item;
|
||||
Family *family;
|
||||
Typeface *face;
|
||||
FontPanel *panel = (FontPanel *) data;
|
||||
WMArrayIterator i;
|
||||
/* current typeface and size */
|
||||
char *oface = NULL;
|
||||
char *osize = NULL;
|
||||
int facei = -1;
|
||||
int sizei = -1;
|
||||
|
||||
/* must try to keep the same typeface and size for the new family */
|
||||
item = WMGetListSelectedItem(panel->typLs);
|
||||
if (item)
|
||||
oface = wstrdup(item->text);
|
||||
|
||||
osize = WMGetTextFieldText(panel->sizT);
|
||||
|
||||
item = WMGetListSelectedItem(lPtr);
|
||||
family = (Family *) item->clientData;
|
||||
|
||||
WMClearList(panel->typLs);
|
||||
|
||||
WM_ITERATE_ARRAY(family->typefaces, face, i) {
|
||||
char buffer[256];
|
||||
int top = 0;
|
||||
WMListItem *fitem;
|
||||
|
||||
strlcpy(buffer, face->typeface, sizeof(buffer));
|
||||
if (strcasecmp(face->typeface, "Roman") == 0)
|
||||
top = 1;
|
||||
if (strcasecmp(face->typeface, "Regular") == 0)
|
||||
top = 1;
|
||||
if (top)
|
||||
fitem = WMInsertListItem(panel->typLs, 0, buffer);
|
||||
else
|
||||
fitem = WMAddListItem(panel->typLs, buffer);
|
||||
fitem->clientData = face;
|
||||
}
|
||||
|
||||
if (oface) {
|
||||
facei = WMFindRowOfListItemWithTitle(panel->typLs, oface);
|
||||
wfree(oface);
|
||||
}
|
||||
if (facei < 0) {
|
||||
facei = 0;
|
||||
}
|
||||
WMSelectListItem(panel->typLs, facei);
|
||||
typefaceClick(panel->typLs, panel);
|
||||
|
||||
if (osize) {
|
||||
sizei = WMFindRowOfListItemWithTitle(panel->sizLs, osize);
|
||||
}
|
||||
if (sizei >= 0) {
|
||||
WMSelectListItem(panel->sizLs, sizei);
|
||||
sizeClick(panel->sizLs, panel);
|
||||
}
|
||||
|
||||
if (osize)
|
||||
wfree(osize);
|
||||
|
||||
preview(panel);
|
||||
}
|
||||
|
||||
static void typefaceClick(WMWidget * w, void *data)
|
||||
{
|
||||
FontPanel *panel = (FontPanel *) data;
|
||||
WMListItem *item;
|
||||
Typeface *face;
|
||||
WMArrayIterator i;
|
||||
char buffer[32];
|
||||
|
||||
char *osize = NULL;
|
||||
int sizei = -1;
|
||||
void *size;
|
||||
|
||||
/* Parameter not used, but tell the compiler that it is ok */
|
||||
(void) w;
|
||||
|
||||
osize = WMGetTextFieldText(panel->sizT);
|
||||
|
||||
item = WMGetListSelectedItem(panel->typLs);
|
||||
face = (Typeface *) item->clientData;
|
||||
|
||||
WMClearList(panel->sizLs);
|
||||
|
||||
WM_ITERATE_ARRAY(face->sizes, size, i) {
|
||||
if (size != NULL) {
|
||||
int size_int = (intptr_t) size;
|
||||
|
||||
sprintf(buffer, "%i", size_int);
|
||||
|
||||
WMAddListItem(panel->sizLs, buffer);
|
||||
}
|
||||
}
|
||||
|
||||
if (osize) {
|
||||
sizei = WMFindRowOfListItemWithTitle(panel->sizLs, osize);
|
||||
}
|
||||
if (sizei < 0) {
|
||||
sizei = WMFindRowOfListItemWithTitle(panel->sizLs, "12");
|
||||
}
|
||||
if (sizei < 0) {
|
||||
sizei = 0;
|
||||
}
|
||||
WMSelectListItem(panel->sizLs, sizei);
|
||||
WMSetListPosition(panel->sizLs, sizei);
|
||||
sizeClick(panel->sizLs, panel);
|
||||
|
||||
if (osize)
|
||||
wfree(osize);
|
||||
|
||||
preview(panel);
|
||||
}
|
||||
|
||||
static void sizeClick(WMWidget * w, void *data)
|
||||
{
|
||||
FontPanel *panel = (FontPanel *) data;
|
||||
WMListItem *item;
|
||||
|
||||
/* Parameter not used, but tell the compiler that it is ok */
|
||||
(void) w;
|
||||
|
||||
item = WMGetListSelectedItem(panel->sizLs);
|
||||
WMSetTextFieldText(panel->sizT, item->text);
|
||||
|
||||
WMSelectTextFieldRange(panel->sizT, wmkrange(0, strlen(item->text)));
|
||||
|
||||
preview(panel);
|
||||
}
|
||||
|
||||
static void setFontPanelFontName(FontPanel * panel, const char *family, const char *style, double size)
|
||||
{
|
||||
int famrow;
|
||||
int stlrow;
|
||||
int sz;
|
||||
char asize[64];
|
||||
void *vsize;
|
||||
WMListItem *item;
|
||||
Family *fam;
|
||||
Typeface *face;
|
||||
WMArrayIterator i;
|
||||
|
||||
famrow = WMFindRowOfListItemWithTitle(panel->famLs, family);
|
||||
if (famrow < 0) {
|
||||
famrow = 0;
|
||||
return;
|
||||
}
|
||||
WMSelectListItem(panel->famLs, famrow);
|
||||
WMSetListPosition(panel->famLs, famrow);
|
||||
|
||||
WMClearList(panel->typLs);
|
||||
|
||||
item = WMGetListSelectedItem(panel->famLs);
|
||||
|
||||
fam = (Family *) item->clientData;
|
||||
WM_ITERATE_ARRAY(fam->typefaces, face, i) {
|
||||
char buffer[256];
|
||||
int top = 0;
|
||||
WMListItem *fitem;
|
||||
|
||||
strlcpy(buffer, face->typeface, sizeof(buffer));
|
||||
if (strcasecmp(face->typeface, "Roman") == 0)
|
||||
top = 1;
|
||||
if (top)
|
||||
fitem = WMInsertListItem(panel->typLs, 0, buffer);
|
||||
else
|
||||
fitem = WMAddListItem(panel->typLs, buffer);
|
||||
fitem->clientData = face;
|
||||
}
|
||||
|
||||
stlrow = WMFindRowOfListItemWithTitle(panel->typLs, style);
|
||||
|
||||
if (stlrow < 0) {
|
||||
stlrow = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
WMSelectListItem(panel->typLs, stlrow);
|
||||
|
||||
item = WMGetListSelectedItem(panel->typLs);
|
||||
|
||||
face = (Typeface *) item->clientData;
|
||||
|
||||
WMClearList(panel->sizLs);
|
||||
|
||||
WM_ITERATE_ARRAY(face->sizes, vsize, i) {
|
||||
char buffer[32];
|
||||
|
||||
if (vsize != NULL) {
|
||||
int size_int = (intptr_t) vsize;
|
||||
|
||||
sprintf(buffer, "%i", size_int);
|
||||
|
||||
WMAddListItem(panel->sizLs, buffer);
|
||||
}
|
||||
}
|
||||
|
||||
snprintf(asize, sizeof(asize) - 1, "%d", (int)(size + 0.5));
|
||||
|
||||
sz = WMFindRowOfListItemWithTitle(panel->sizLs, asize);
|
||||
|
||||
if (sz < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
WMSelectListItem(panel->sizLs, sz);
|
||||
sizeClick(panel->sizLs, panel);
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
use wings_rs::WINGsP::{WMGetFontPanel, WMScreenMainLoop, WMShowFontPanel};
|
||||
use wings_rs::{font_panel::FontPanel, WINGsP::WMScreenMainLoop};
|
||||
use wings_rs_tests::LiveApplication;
|
||||
|
||||
fn main() {
|
||||
let app = LiveApplication::new("WMFontPanel");
|
||||
let mut font_panel = FontPanel::new(app.screen.as_ptr()).expect("could not create font panel");
|
||||
font_panel.show();
|
||||
unsafe {
|
||||
let font_panel = WMGetFontPanel(app.screen.as_ptr());
|
||||
WMShowFontPanel(font_panel);
|
||||
WMScreenMainLoop(app.screen.as_ptr());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
use std::time::Instant;
|
||||
use insta_image::assert_png_snapshot;
|
||||
use wings_rs::WINGsP::{WMGetFontPanel, WMShowFontPanel};
|
||||
use std::time::Instant;
|
||||
use wings_rs::font_panel::FontPanel;
|
||||
use wings_rs_tests::HeadlessApplication;
|
||||
|
||||
#[test]
|
||||
fn show_font_panel() {
|
||||
let mut app = HeadlessApplication::new();
|
||||
unsafe {
|
||||
let font_panel = WMGetFontPanel(app.screen.as_ptr());
|
||||
WMShowFontPanel(font_panel);
|
||||
}
|
||||
let mut font_panel =
|
||||
FontPanel::new(app.screen.as_ptr()).expect("could not construct font panel");
|
||||
font_panel.show();
|
||||
while app.pump_event_queue(Instant::now()) {}
|
||||
assert_png_snapshot!("font_panel", app.xvfb.png_screenshot());
|
||||
}
|
||||
|
||||
@@ -2,11 +2,15 @@ AUTOMAKE_OPTIONS =
|
||||
|
||||
RUST_SOURCES = \
|
||||
src/WINGsP.rs \
|
||||
src/button.rs \
|
||||
src/configuration.rs \
|
||||
src/font.rs \
|
||||
src/font_panel.rs \
|
||||
src/lib.rs \
|
||||
src/list.rs \
|
||||
src/pango_extras.rs \
|
||||
src/screen.rs
|
||||
src/screen.rs \
|
||||
src/widget.rs
|
||||
|
||||
RUST_EXTRA = \
|
||||
Cargo.lock \
|
||||
@@ -18,11 +22,44 @@ src/WINGsP.rs: ../WINGs/WINGsP.h ../../wrlib/wraster.h ../WINGs/WINGs.h ../WINGs
|
||||
--allowlist-type "^W_.+|^WM(View|Array|DragOperationType|Point|Data|OpenPanel|SavePanel|HashTable|DraggingInfo|SelectionProcs|Rect|EventProc|Widget|Size|Color|Pixmap|FilePanel)|R(Context|ContextAttributes|Image|RenderingMode|ScalingFilter|StdColormapMode|ImageFormat|Color)|_WINGsConfiguration" \
|
||||
--allowlist-type "^WM(FontPanel|Screen|Button)" \
|
||||
--allowlist-function "^WMCreateScreen|^WM(Get|Show)FontPanel|^WMCreateCommandButton|^WM(Initialize|Release)Application|^WMScreenMainLoop|^WMHandleEvent" \
|
||||
--allowlist-function "^WMWidgetScreen|^WM(Initialize|Release)Application|^WM(ScreenMainLoop|HandleEvent)|^WM(((Get|Set)TextFieldFont)|GetTextFieldText|SetTextFieldText|SelectTextFieldRange)" \
|
||||
--allowlist-type "WMList" \
|
||||
--allowlist-function "^WM(CreateList|SetListAction|GetListSelectedItem|InsertListItem|ClearList|FindRowOfListItemWithTitle|SelectListItem|SetListPosition|SortListItems)" \
|
||||
--allowlist-type "^WM(Window|Action)" \
|
||||
--allowlist-function "^WM(CreateWindow|SetWindowTitle|SetWindowMinSize|SetWindowCloseAction)" \
|
||||
--allowlist-type "^WMWidget" \
|
||||
--allowlist-function "^WM(Map|Destroy|Unmap|Resize|Realize|Move)Widget" \
|
||||
--allowlist-function "^WMWidget(Width|Height)" \
|
||||
--allowlist-function "^WM(SetWidgetBackgroundColor|SetWidgetBackgroundColor|MapSubwidgets)" \
|
||||
--allowlist-type "^WM(SplitView|SplitViewConstrainProc)" \
|
||||
--allowlist-function "^WM(SetViewNotifySizeChanges|CreateSplitView|SetSplitViewConstrainProc|GetSplitViewDividerThickness|AddSplitViewSubview)" \
|
||||
--allowlist-type "^WMNotification" \
|
||||
--allowlist-function "^WM(Add|Remove)NotificationObserver|^WMGetNotification(Object|Name)" \
|
||||
--allowlist-item "^WMNotificationObserverAction" \
|
||||
--allowlist-item "^WMViewSizeDidChangeNotification" \
|
||||
--allowlist-type "^WMFrame" \
|
||||
--allowlist-function "^WM(CreateFrame|SetFrameRelief)" \
|
||||
--allowlist-function "^WM(White|DarkGray|Release)Color" \
|
||||
--allowlist-type "^WMTextField" \
|
||||
--allowlist-function "^WM(CreateTextField)" \
|
||||
--allowlist-type "^WMLabel" \
|
||||
--allowlist-function "^WM(CreateLabel|SetLabelText|SetLabelFont|SetLabelTextColor|SetLabelRelief|SetLabelTextAlignment)" \
|
||||
--allowlist-type "^WM(Button|ButtonBehaviorMask)" \
|
||||
--allowlist-function "^WM(CreateCustomButton|SetButtonText|SetButtonAction|SetButtonText)" \
|
||||
--allowlist-function "wmkrange" \
|
||||
--allowlist-type "^WM(View|Array|DragOperationType|Point|Data|OpenPanel|SavePanel|HashTable|DraggingInfo|SelectionProcs|Rect|EventProc|Widget|Size|Color|Pixmap|FilePanel|Screen|Range|List|ListItem)" \
|
||||
--allowlist-type "^R(Context|ContextAttributes|Image|RenderingMode|ScalingFilter|StdColormapMode|ImageFormat|Color)" \
|
||||
--allowlist-type "_WINGsConfiguration" \
|
||||
--allowlist-item "^WMAlignment" \
|
||||
--allowlist-item "^WMReliefType" \
|
||||
-o src/WINGsP.rs -- \
|
||||
@PANGO_CFLAGS@ \
|
||||
-I../../wrlib \
|
||||
-I.. && ./patch_WINGsP.sh src/WINGsP.rs
|
||||
|
||||
Cargo.lock:
|
||||
$(CARGO) build
|
||||
|
||||
target/debug/libwings_rs.a: $(RUST_SOURCES) $(RUST_EXTRA)
|
||||
$(CARGO) build
|
||||
|
||||
|
||||
22
WINGs/wings-rs/src/button.rs
Normal file
22
WINGs/wings-rs/src/button.rs
Normal file
@@ -0,0 +1,22 @@
|
||||
#[allow(non_snake_case)]
|
||||
pub mod ffi {
|
||||
use crate::WINGsP::*;
|
||||
|
||||
/// Creates a button like an "Ok" button to dismiss a dialog.
|
||||
///
|
||||
/// ## Rust rewrite notes
|
||||
///
|
||||
/// This was originally a macro.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn WMCreateCommandButton(parent: *mut WMWidget) -> *mut WMButton {
|
||||
unsafe {
|
||||
WMCreateCustomButton(
|
||||
parent,
|
||||
(WMButtonBehaviorMask_WBBSpringLoadedMask
|
||||
| WMButtonBehaviorMask_WBBPushInMask
|
||||
| WMButtonBehaviorMask_WBBPushLightMask
|
||||
| WMButtonBehaviorMask_WBBPushChangeMask) as i32,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
1019
WINGs/wings-rs/src/font_panel.rs
Normal file
1019
WINGs/wings-rs/src/font_panel.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,11 @@
|
||||
#[allow(non_snake_case)]
|
||||
#[allow(non_upper_case_globals)]
|
||||
pub mod WINGsP;
|
||||
pub mod button;
|
||||
pub mod configuration;
|
||||
pub mod font;
|
||||
pub mod font_panel;
|
||||
pub mod list;
|
||||
pub(crate) mod pango_extras;
|
||||
pub mod screen;
|
||||
pub mod widget;
|
||||
|
||||
15
WINGs/wings-rs/src/list.rs
Normal file
15
WINGs/wings-rs/src/list.rs
Normal file
@@ -0,0 +1,15 @@
|
||||
#[allow(non_snake_case)]
|
||||
pub mod ffi {
|
||||
use crate::WINGsP::*;
|
||||
|
||||
use std::ffi::c_char;
|
||||
|
||||
/// Appends an item with the label `text` to `list`.
|
||||
///
|
||||
/// ## Rust rewrite notes
|
||||
///
|
||||
/// This was originally a macro.
|
||||
pub unsafe fn WMAddListItem(list: *mut WMList, text: *const c_char) -> *mut WMListItem {
|
||||
unsafe { WMInsertListItem(list, -1, text) }
|
||||
}
|
||||
}
|
||||
57
WINGs/wings-rs/src/widget.rs
Normal file
57
WINGs/wings-rs/src/widget.rs
Normal file
@@ -0,0 +1,57 @@
|
||||
//! Common WINGs widget fields.
|
||||
//!
|
||||
//! WINGs widgets all have a [`WINGsP::WMView`] object that tracks common attributes
|
||||
//! like size and location and a [`WINGsP::W_Class`] field that identifies the widget
|
||||
//! type (button, list, etc.). This module provides access to these fields.
|
||||
//!
|
||||
//! ## Rust rewrite notes
|
||||
//!
|
||||
//! The original WINGs implementation uses casting and a common layout for
|
||||
//! "primitive" widget structs, such that the first two fields are always a
|
||||
//! `W_Class` and a `*mut WMView`. We rely on this convention when implementing
|
||||
//! [`Widget`] for widgets that are defined in C.
|
||||
use crate::WINGsP;
|
||||
use std::{
|
||||
ptr::NonNull,
|
||||
};
|
||||
|
||||
/// Base WINGs widget type.
|
||||
///
|
||||
/// All primitive widgets (like buttons and lists) should implement this.
|
||||
pub trait Widget {
|
||||
fn class(&self) -> WINGsP::W_Class;
|
||||
fn view(&self) -> NonNull<WINGsP::WMView>;
|
||||
}
|
||||
|
||||
/// Common widget header fields. It should be safe to cast opaque WINGs widget
|
||||
/// structs defined in C to this type to access these fields. Before writing
|
||||
/// code that casts anything to this, double-check the widget definition!
|
||||
#[repr(C)]
|
||||
struct WidgetLike {
|
||||
class: WINGsP::W_Class,
|
||||
view: *mut WINGsP::WMView,
|
||||
}
|
||||
|
||||
/// Casts `$widget` to [`WidgetLike`]. Provided for convenience.
|
||||
///
|
||||
/// ## Safety
|
||||
///
|
||||
/// The caller of this macro is responsible for ensuring that the layout of `$widget` is compatible
|
||||
/// with `WidgetLike`.
|
||||
macro_rules! impl_widget {
|
||||
($widget:ty) => {
|
||||
impl Widget for $widget {
|
||||
fn class(&self) -> crate::WINGsP::W_Class {
|
||||
unsafe { ::std::mem::transmute::<&$widget, &WidgetLike>(self).class }
|
||||
}
|
||||
|
||||
fn view(&self) -> ::std::ptr::NonNull<WINGsP::WMView> {
|
||||
let widget = unsafe { ::std::mem::transmute::<&$widget, &WidgetLike>(self) };
|
||||
NonNull::new(widget.view).expect("WMView field of widget struct cannot be null")
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl_widget!(WINGsP::WMWindow);
|
||||
impl_widget!(WINGsP::WMFrame);
|
||||
Reference in New Issue
Block a user