mirror of
https://github.com/mozilla/gecko-dev.git
synced 2025-02-10 01:08:21 +00:00
Bug 1040668 part 10 - Implement emphasis mark rendering. r=jfkthame
--HG-- extra : source : 1c53ccbaece3931ffe1da5610977e92fcce5f3f6
This commit is contained in:
parent
72495f58ae
commit
6fdb9fbeaa
@ -48,6 +48,7 @@
|
||||
#include "graphite2/Font.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
using namespace mozilla;
|
||||
using namespace mozilla::gfx;
|
||||
@ -1849,6 +1850,46 @@ gfxFont::DrawGlyphs(gfxShapedText *aShapedText,
|
||||
return emittedGlyphs;
|
||||
}
|
||||
|
||||
// This method is mostly parallel to DrawGlyphs.
|
||||
void
|
||||
gfxFont::DrawEmphasisMarks(gfxTextRun* aShapedText, gfxPoint* aPt,
|
||||
uint32_t aOffset, uint32_t aCount,
|
||||
const EmphasisMarkDrawParams& aParams)
|
||||
{
|
||||
gfxFloat& inlineCoord = aParams.isVertical ? aPt->y : aPt->x;
|
||||
uint32_t markLength = aParams.mark->GetLength();
|
||||
|
||||
gfxFloat clusterStart = NAN;
|
||||
bool shouldDrawEmphasisMark = false;
|
||||
for (uint32_t i = 0, idx = aOffset; i < aCount; ++i, ++idx) {
|
||||
if (aParams.spacing) {
|
||||
inlineCoord += aParams.direction * aParams.spacing[i].mBefore;
|
||||
}
|
||||
if (aShapedText->IsClusterStart(idx)) {
|
||||
clusterStart = inlineCoord;
|
||||
}
|
||||
if (aShapedText->CharMayHaveEmphasisMark(idx)) {
|
||||
shouldDrawEmphasisMark = true;
|
||||
}
|
||||
inlineCoord += aParams.direction * aShapedText->GetAdvanceForGlyph(idx);
|
||||
if (shouldDrawEmphasisMark &&
|
||||
(i + 1 == aCount || aShapedText->IsClusterStart(idx + 1))) {
|
||||
MOZ_ASSERT(!std::isnan(clusterStart), "Should have cluster start");
|
||||
gfxFloat clusterAdvance = inlineCoord - clusterStart;
|
||||
// Move the coord backward to get the needed start point.
|
||||
gfxFloat delta = (clusterAdvance + aParams.advance) / 2;
|
||||
inlineCoord -= delta;
|
||||
aParams.mark->Draw(aParams.context, *aPt, DrawMode::GLYPH_FILL,
|
||||
0, markLength, nullptr, nullptr, nullptr);
|
||||
inlineCoord += delta;
|
||||
shouldDrawEmphasisMark = false;
|
||||
}
|
||||
if (aParams.spacing) {
|
||||
inlineCoord += aParams.direction * aParams.spacing[i].mAfter;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
gfxFont::Draw(gfxTextRun *aTextRun, uint32_t aStart, uint32_t aEnd,
|
||||
gfxPoint *aPt, const TextRunDrawParams& aRunParams,
|
||||
|
@ -1307,6 +1307,7 @@ private:
|
||||
class GlyphBufferAzure;
|
||||
struct TextRunDrawParams;
|
||||
struct FontDrawParams;
|
||||
struct EmphasisMarkDrawParams;
|
||||
|
||||
class gfxFont {
|
||||
|
||||
@ -1601,6 +1602,16 @@ public:
|
||||
gfxPoint *aPt, const TextRunDrawParams& aRunParams,
|
||||
uint16_t aOrientation);
|
||||
|
||||
/**
|
||||
* Draw the emphasis marks for the given text run. Its prerequisite
|
||||
* and output are similiar to the method Draw().
|
||||
* @param aPt the baseline origin of the emphasis marks.
|
||||
* @param aParams some drawing parameters, see EmphasisMarkDrawParams.
|
||||
*/
|
||||
void DrawEmphasisMarks(gfxTextRun* aShapedText, gfxPoint* aPt,
|
||||
uint32_t aOffset, uint32_t aCount,
|
||||
const EmphasisMarkDrawParams& aParams);
|
||||
|
||||
/**
|
||||
* Measure a run of characters. See gfxTextRun::Metrics.
|
||||
* @param aTight if false, then return the union of the glyph extents
|
||||
@ -2167,4 +2178,13 @@ struct FontDrawParams {
|
||||
bool haveColorGlyphs;
|
||||
};
|
||||
|
||||
struct EmphasisMarkDrawParams {
|
||||
gfxContext* context;
|
||||
gfxFont::Spacing* spacing;
|
||||
gfxTextRun* mark;
|
||||
gfxFloat advance;
|
||||
gfxFloat direction;
|
||||
bool isVertical;
|
||||
};
|
||||
|
||||
#endif
|
||||
|
@ -1,4 +1,5 @@
|
||||
/* -*- Mode: C++; tab-width: 20; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
|
||||
/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
|
||||
/* vim: set ts=4 et sw=4 tw=80: */
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
@ -665,6 +666,50 @@ gfxTextRun::Draw(gfxContext *aContext, gfxPoint aPt, DrawMode aDrawMode,
|
||||
}
|
||||
}
|
||||
|
||||
// This method is mostly parallel to Draw().
|
||||
void
|
||||
gfxTextRun::DrawEmphasisMarks(gfxContext *aContext, gfxTextRun* aMark,
|
||||
gfxFloat aMarkAdvance, gfxPoint aPt,
|
||||
uint32_t aStart, uint32_t aLength,
|
||||
PropertyProvider* aProvider)
|
||||
{
|
||||
MOZ_ASSERT(aStart + aLength <= GetLength());
|
||||
|
||||
EmphasisMarkDrawParams params;
|
||||
params.context = aContext;
|
||||
params.mark = aMark;
|
||||
params.advance = aMarkAdvance;
|
||||
params.direction = GetDirection();
|
||||
params.isVertical = IsVertical();
|
||||
|
||||
gfxFloat& inlineCoord = params.isVertical ? aPt.y : aPt.x;
|
||||
gfxFloat direction = params.direction;
|
||||
|
||||
GlyphRunIterator iter(this, aStart, aLength);
|
||||
while (iter.NextRun()) {
|
||||
gfxFont* font = iter.GetGlyphRun()->mFont;
|
||||
uint32_t start = iter.GetStringStart();
|
||||
uint32_t end = iter.GetStringEnd();
|
||||
uint32_t ligatureRunStart = start;
|
||||
uint32_t ligatureRunEnd = end;
|
||||
ShrinkToLigatureBoundaries(&ligatureRunStart, &ligatureRunEnd);
|
||||
|
||||
inlineCoord += direction *
|
||||
ComputePartialLigatureWidth(start, ligatureRunStart, aProvider);
|
||||
|
||||
nsAutoTArray<PropertyProvider::Spacing, 200> spacingBuffer;
|
||||
bool haveSpacing = GetAdjustedSpacingArray(
|
||||
ligatureRunStart, ligatureRunEnd, aProvider,
|
||||
ligatureRunStart, ligatureRunEnd, &spacingBuffer);
|
||||
params.spacing = haveSpacing ? spacingBuffer.Elements() : nullptr;
|
||||
font->DrawEmphasisMarks(this, &aPt, ligatureRunStart,
|
||||
ligatureRunEnd - ligatureRunStart, params);
|
||||
|
||||
inlineCoord += direction *
|
||||
ComputePartialLigatureWidth(ligatureRunEnd, end, aProvider);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
gfxTextRun::AccumulateMetricsForRun(gfxFont *aFont,
|
||||
uint32_t aStart, uint32_t aEnd,
|
||||
|
@ -251,6 +251,16 @@ public:
|
||||
gfxFloat *aAdvanceWidth, gfxTextContextPaint *aContextPaint,
|
||||
gfxTextRunDrawCallbacks *aCallbacks = nullptr);
|
||||
|
||||
/**
|
||||
* Draws the emphasis marks for this text run. Uses only GetSpacing
|
||||
* from aProvider. The provided point is the baseline origin of the
|
||||
* line of emphasis marks.
|
||||
*/
|
||||
void DrawEmphasisMarks(gfxContext* aContext, gfxTextRun* aMark,
|
||||
gfxFloat aMarkAdvance, gfxPoint aPt,
|
||||
uint32_t aStart, uint32_t aLength,
|
||||
PropertyProvider* aProvider);
|
||||
|
||||
/**
|
||||
* Computes the ReflowMetrics for a substring.
|
||||
* Uses GetSpacing from aBreakProvider.
|
||||
|
@ -3285,8 +3285,11 @@ nsLineLayout::RelativePositionFrames(PerSpanData* psd, nsOverflowAreas& aOverflo
|
||||
// (1) When PFD_RECOMPUTEOVERFLOW is set due to trimming
|
||||
// (2) When there are text decorations, since we can't recompute the
|
||||
// overflow area until Reflow and VerticalAlignLine have finished
|
||||
// (3) When there are text emphasis marks, since the marks may be
|
||||
// put further away if the text is inside ruby.
|
||||
if (pfd->mRecomputeOverflow ||
|
||||
frame->StyleContext()->HasTextDecorationLines()) {
|
||||
frame->StyleContext()->HasTextDecorationLines() ||
|
||||
frame->StyleText()->HasTextEmphasis()) {
|
||||
nsTextFrame* f = static_cast<nsTextFrame*>(frame);
|
||||
r = f->RecomputeOverflow(mBlockReflowState->frame);
|
||||
}
|
||||
|
@ -5116,6 +5116,92 @@ GetInflationForTextDecorations(nsIFrame* aFrame, nscoord aInflationMinFontSize)
|
||||
return nsLayoutUtils::FontSizeInflationInner(aFrame, aInflationMinFontSize);
|
||||
}
|
||||
|
||||
static already_AddRefed<nsFontMetrics>
|
||||
GetFontMetricsOfEmphasisMarks(nsStyleContext* aStyleContext, float aInflation)
|
||||
{
|
||||
nsPresContext* pc = aStyleContext->PresContext();
|
||||
WritingMode wm(aStyleContext);
|
||||
gfxFont::Orientation orientation = wm.IsVertical() && !wm.IsSideways() ?
|
||||
gfxFont::eVertical : gfxFont::eHorizontal;
|
||||
|
||||
const nsStyleFont* styleFont = aStyleContext->StyleFont();
|
||||
nsFont font = styleFont->mFont;
|
||||
font.size = NSToCoordRound(font.size * aInflation * 0.5f);
|
||||
|
||||
RefPtr<nsFontMetrics> fm;
|
||||
pc->DeviceContext()->GetMetricsFor(font, styleFont->mLanguage,
|
||||
styleFont->mExplicitLanguage,
|
||||
orientation, pc->GetUserFontSet(),
|
||||
pc->GetTextPerfMetrics(),
|
||||
*getter_AddRefs(fm));
|
||||
return fm.forget();
|
||||
}
|
||||
|
||||
static gfxTextRun*
|
||||
GenerateTextRunForEmphasisMarks(nsTextFrame* aFrame, nsFontMetrics* aFontMetrics,
|
||||
WritingMode aWM, const nsStyleText* aStyleText)
|
||||
{
|
||||
const nsString& emphasisString = aStyleText->mTextEmphasisStyleString;
|
||||
RefPtr<gfxContext> ctx = CreateReferenceThebesContext(aFrame);
|
||||
auto appUnitsPerDevUnit = aFrame->PresContext()->AppUnitsPerDevPixel();
|
||||
uint32_t flags = nsLayoutUtils::
|
||||
GetTextRunOrientFlagsForStyle(aFrame->StyleContext());
|
||||
if (flags == gfxTextRunFactory::TEXT_ORIENT_VERTICAL_MIXED) {
|
||||
// The emphasis marks should always be rendered upright per spec.
|
||||
flags = gfxTextRunFactory::TEXT_ORIENT_VERTICAL_UPRIGHT;
|
||||
}
|
||||
return aFontMetrics->GetThebesFontGroup()->
|
||||
MakeTextRun<char16_t>(emphasisString.get(), emphasisString.Length(),
|
||||
ctx, appUnitsPerDevUnit, flags, nullptr);
|
||||
}
|
||||
|
||||
nsRect
|
||||
nsTextFrame::UpdateTextEmphasis(WritingMode aWM, PropertyProvider& aProvider)
|
||||
{
|
||||
const nsStyleText* styleText = StyleText();
|
||||
if (!styleText->HasTextEmphasis()) {
|
||||
Properties().Delete(EmphasisMarkProperty());
|
||||
return nsRect();
|
||||
}
|
||||
|
||||
RefPtr<nsFontMetrics> fm =
|
||||
GetFontMetricsOfEmphasisMarks(StyleContext(), GetFontSizeInflation());
|
||||
EmphasisMarkInfo* info = new EmphasisMarkInfo;
|
||||
info->textRun =
|
||||
GenerateTextRunForEmphasisMarks(this, fm, aWM, styleText);
|
||||
info->advance =
|
||||
info->textRun->GetAdvanceWidth(0, info->textRun->GetLength(), nullptr);
|
||||
|
||||
// Calculate the baseline offset
|
||||
LogicalSide side = styleText->TextEmphasisSide(aWM);
|
||||
nsFontMetrics* baseFontMetrics = aProvider.GetFontMetrics();
|
||||
LogicalSize frameSize = GetLogicalSize();
|
||||
// The overflow rect is inflated in the inline direction by half
|
||||
// advance of the emphasis mark on each side, so that even if a mark
|
||||
// is drawn for a zero-width character, it won't be clipped.
|
||||
LogicalRect overflowRect(aWM, -info->advance / 2,
|
||||
/* BStart to be computed below */0,
|
||||
frameSize.ISize(aWM) + info->advance,
|
||||
fm->MaxAscent() + fm->MaxDescent());
|
||||
// When the writing mode is vertical-lr the line is inverted, and thus
|
||||
// the ascent and descent are swapped.
|
||||
nscoord absOffset = (side == eLogicalSideBStart) != aWM.IsLineInverted() ?
|
||||
baseFontMetrics->MaxAscent() + fm->MaxDescent() :
|
||||
baseFontMetrics->MaxDescent() + fm->MaxAscent();
|
||||
// XXX emphasis marks should be drawn outside ruby, see bug 1224013.
|
||||
if (side == eLogicalSideBStart) {
|
||||
info->baselineOffset = -absOffset;
|
||||
overflowRect.BStart(aWM) = -overflowRect.BSize(aWM);
|
||||
} else {
|
||||
MOZ_ASSERT(side == eLogicalSideBEnd);
|
||||
info->baselineOffset = absOffset;
|
||||
overflowRect.BStart(aWM) = frameSize.BSize(aWM);
|
||||
}
|
||||
|
||||
Properties().Set(EmphasisMarkProperty(), info);
|
||||
return overflowRect.GetPhysicalRect(aWM, frameSize.GetPhysicalSize(aWM));
|
||||
}
|
||||
|
||||
void
|
||||
nsTextFrame::UnionAdditionalOverflow(nsPresContext* aPresContext,
|
||||
nsIFrame* aBlock,
|
||||
@ -5123,9 +5209,10 @@ nsTextFrame::UnionAdditionalOverflow(nsPresContext* aPresContext,
|
||||
nsRect* aVisualOverflowRect,
|
||||
bool aIncludeTextDecorations)
|
||||
{
|
||||
const WritingMode wm = GetWritingMode();
|
||||
bool verticalRun = mTextRun->IsVertical();
|
||||
bool useVerticalMetrics = verticalRun && mTextRun->UseCenterBaseline();
|
||||
bool inverted = GetWritingMode().IsLineInverted();
|
||||
bool inverted = wm.IsLineInverted();
|
||||
|
||||
if (IsFloatingFirstLetterChild()) {
|
||||
// The underline/overline drawable area must be contained in the overflow
|
||||
@ -5185,7 +5272,6 @@ nsTextFrame::UnionAdditionalOverflow(nsPresContext* aPresContext,
|
||||
const gfxFloat appUnitsPerDevUnit = aPresContext->AppUnitsPerDevPixel(),
|
||||
gfxWidth = measure / appUnitsPerDevUnit;
|
||||
gfxFloat ascent = gfxFloat(mAscent) / appUnitsPerDevUnit;
|
||||
const WritingMode wm = GetWritingMode();
|
||||
if (wm.IsVerticalRL()) {
|
||||
ascent = -ascent;
|
||||
}
|
||||
@ -5295,6 +5381,9 @@ nsTextFrame::UnionAdditionalOverflow(nsPresContext* aPresContext,
|
||||
verticalRun ? nsRect(topOrLeft, 0, bottomOrRight - topOrLeft, measure)
|
||||
: nsRect(0, topOrLeft, measure, bottomOrRight - topOrLeft));
|
||||
}
|
||||
|
||||
aVisualOverflowRect->UnionRect(*aVisualOverflowRect,
|
||||
UpdateTextEmphasis(wm, aProvider));
|
||||
}
|
||||
|
||||
// Text-shadow overflows
|
||||
@ -6107,6 +6196,36 @@ nsTextFrame::PaintTextWithSelection(gfxContext* aCtx,
|
||||
return true;
|
||||
}
|
||||
|
||||
void
|
||||
nsTextFrame::DrawEmphasisMarks(gfxContext* aContext, WritingMode aWM,
|
||||
const gfxPoint& aTextBaselinePt,
|
||||
uint32_t aOffset, uint32_t aLength,
|
||||
PropertyProvider& aProvider)
|
||||
{
|
||||
auto info = static_cast<const EmphasisMarkInfo*>(
|
||||
Properties().Get(EmphasisMarkProperty()));
|
||||
if (!info) {
|
||||
MOZ_ASSERT(!StyleText()->HasTextEmphasis());
|
||||
return;
|
||||
}
|
||||
|
||||
nscolor color = nsLayoutUtils::
|
||||
GetColor(this, eCSSProperty_text_emphasis_color);
|
||||
aContext->SetColor(Color::FromABGR(color));
|
||||
gfxPoint pt(aTextBaselinePt);
|
||||
if (!aWM.IsVertical()) {
|
||||
pt.y += info->baselineOffset;
|
||||
} else {
|
||||
if (aWM.IsVerticalRL()) {
|
||||
pt.x -= info->baselineOffset;
|
||||
} else {
|
||||
pt.x += info->baselineOffset;
|
||||
}
|
||||
}
|
||||
mTextRun->DrawEmphasisMarks(aContext, info->textRun, info->advance,
|
||||
pt, aOffset, aLength, &aProvider);
|
||||
}
|
||||
|
||||
nscolor
|
||||
nsTextFrame::GetCaretColorAt(int32_t aOffset)
|
||||
{
|
||||
@ -6609,6 +6728,9 @@ nsTextFrame::DrawTextRunAndDecorations(
|
||||
DrawTextRun(aCtx, aTextBaselinePt, aOffset, aLength, aProvider, aTextColor,
|
||||
aAdvanceWidth, aDrawSoftHyphen, aContextPaint, aCallbacks);
|
||||
|
||||
// Emphasis marks
|
||||
DrawEmphasisMarks(aCtx, wm, aTextBaselinePt, aOffset, aLength, aProvider);
|
||||
|
||||
// Line-throughs
|
||||
for (uint32_t i = aDecorations.mStrikes.Length(); i-- > 0; ) {
|
||||
const LineDecoration& dec = aDecorations.mStrikes[i];
|
||||
@ -6655,7 +6777,8 @@ nsTextFrame::DrawText(
|
||||
|
||||
// Hide text decorations if we're currently hiding @font-face fallback text
|
||||
const bool drawDecorations = !aProvider.GetFontGroup()->ShouldSkipDrawing() &&
|
||||
decorations.HasDecorationLines();
|
||||
(decorations.HasDecorationLines() ||
|
||||
StyleText()->HasTextEmphasis());
|
||||
if (drawDecorations) {
|
||||
DrawTextRunAndDecorations(aCtx, aDirtyRect, aFramePt, aTextBaselinePt, aOffset, aLength,
|
||||
aProvider, aTextStyle, aTextColor, aClipEdges, aAdvanceWidth,
|
||||
|
@ -440,6 +440,12 @@ public:
|
||||
SelectionType aSelectionType,
|
||||
DrawPathCallbacks* aCallbacks);
|
||||
|
||||
void DrawEmphasisMarks(gfxContext* aContext,
|
||||
mozilla::WritingMode aWM,
|
||||
const gfxPoint& aTextBaselinePt,
|
||||
uint32_t aOffset, uint32_t aLength,
|
||||
PropertyProvider& aProvider);
|
||||
|
||||
virtual nscolor GetCaretColorAt(int32_t aOffset) override;
|
||||
|
||||
int16_t GetSelectionStatus(int16_t* aSelectionFlags);
|
||||
@ -585,6 +591,11 @@ protected:
|
||||
nsRect* aVisualOverflowRect,
|
||||
bool aIncludeTextDecorations);
|
||||
|
||||
// Update information of emphasis marks, and return the visial
|
||||
// overflow rect of the emphasis marks.
|
||||
nsRect UpdateTextEmphasis(mozilla::WritingMode aWM,
|
||||
PropertyProvider& aProvider);
|
||||
|
||||
void PaintOneShadow(uint32_t aOffset,
|
||||
uint32_t aLength,
|
||||
nsCSSShadowItem* aShadowDetails,
|
||||
@ -812,6 +823,14 @@ protected:
|
||||
void ClearMetrics(nsHTMLReflowMetrics& aMetrics);
|
||||
|
||||
NS_DECLARE_FRAME_PROPERTY(JustificationAssignment, nullptr)
|
||||
|
||||
struct EmphasisMarkInfo
|
||||
{
|
||||
nsAutoPtr<gfxTextRun> textRun;
|
||||
gfxFloat advance;
|
||||
gfxFloat baselineOffset;
|
||||
};
|
||||
NS_DECLARE_FRAME_PROPERTY(EmphasisMarkProperty, DeleteValue<EmphasisMarkInfo>)
|
||||
};
|
||||
|
||||
#endif
|
||||
|
@ -3726,6 +3726,24 @@ nsChangeHint nsStyleText::CalcDifference(const nsStyleText& aOther) const
|
||||
return NS_STYLE_HINT_NONE;
|
||||
}
|
||||
|
||||
LogicalSide
|
||||
nsStyleText::TextEmphasisSide(WritingMode aWM) const
|
||||
{
|
||||
MOZ_ASSERT(
|
||||
(!(mTextEmphasisPosition & NS_STYLE_TEXT_EMPHASIS_POSITION_LEFT) !=
|
||||
!(mTextEmphasisPosition & NS_STYLE_TEXT_EMPHASIS_POSITION_RIGHT)) &&
|
||||
(!(mTextEmphasisPosition & NS_STYLE_TEXT_EMPHASIS_POSITION_OVER) !=
|
||||
!(mTextEmphasisPosition & NS_STYLE_TEXT_EMPHASIS_POSITION_UNDER)));
|
||||
Side side = aWM.IsVertical() ?
|
||||
(mTextEmphasisPosition & NS_STYLE_TEXT_EMPHASIS_POSITION_LEFT
|
||||
? eSideLeft : eSideRight) :
|
||||
(mTextEmphasisPosition & NS_STYLE_TEXT_EMPHASIS_POSITION_OVER
|
||||
? eSideTop : eSideBottom);
|
||||
LogicalSide result = aWM.LogicalSideForPhysicalSide(side);
|
||||
MOZ_ASSERT(IsBlock(result));
|
||||
return result;
|
||||
}
|
||||
|
||||
//-----------------------
|
||||
// nsStyleUserInterface
|
||||
//
|
||||
|
@ -1811,6 +1811,8 @@ struct nsStyleText {
|
||||
inline bool NewlineIsSignificant(const nsTextFrame* aContextFrame) const;
|
||||
inline bool WhiteSpaceCanWrap(const nsIFrame* aContextFrame) const;
|
||||
inline bool WordCanWrap(const nsIFrame* aContextFrame) const;
|
||||
|
||||
mozilla::LogicalSide TextEmphasisSide(mozilla::WritingMode aWM) const;
|
||||
};
|
||||
|
||||
struct nsStyleImageOrientation {
|
||||
|
Loading…
x
Reference in New Issue
Block a user