1/*
2 * Copyright (C) 2006, 2007, 2012 Apple Inc. All rights reserved.
3 *
4 * This library is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU Library General Public
6 * License as published by the Free Software Foundation; either
7 * version 2 of the License, or (at your option) any later version.
8 *
9 * This library is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 * Library General Public License for more details.
13 *
14 * You should have received a copy of the GNU Library General Public License
15 * along with this library; see the file COPYING.LIB. If not, write to
16 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
17 * Boston, MA 02110-1301, USA.
18 *
19 */
20
21#include "config.h"
22#include "RenderFileUploadControl.h"
23
24#include "FileList.h"
25#include "FontCascade.h"
26#include "GraphicsContext.h"
27#include "HTMLInputElement.h"
28#include "HTMLNames.h"
29#include "Icon.h"
30#include "LocalizedStrings.h"
31#include "PaintInfo.h"
32#include "RenderButton.h"
33#include "RenderText.h"
34#include "RenderTheme.h"
35#include "ShadowRoot.h"
36#include "StringTruncator.h"
37#include "TextRun.h"
38#include "VisiblePosition.h"
39#include <math.h>
40#include <wtf/IsoMallocInlines.h>
41
42namespace WebCore {
43
44using namespace HTMLNames;
45
46WTF_MAKE_ISO_ALLOCATED_IMPL(RenderFileUploadControl);
47
48const int afterButtonSpacing = 4;
49#if !PLATFORM(IOS_FAMILY)
50const int iconHeight = 16;
51const int iconWidth = 16;
52const int iconFilenameSpacing = 2;
53const int defaultWidthNumChars = 34;
54#else
55// On iOS the icon height matches the button height, to maximize the icon size.
56const int iconFilenameSpacing = afterButtonSpacing;
57const int defaultWidthNumChars = 38;
58#endif
59const int buttonShadowHeight = 2;
60
61RenderFileUploadControl::RenderFileUploadControl(HTMLInputElement& input, RenderStyle&& style)
62 : RenderBlockFlow(input, WTFMove(style))
63 , m_canReceiveDroppedFiles(input.canReceiveDroppedFiles())
64{
65}
66
67RenderFileUploadControl::~RenderFileUploadControl() = default;
68
69HTMLInputElement& RenderFileUploadControl::inputElement() const
70{
71 return downcast<HTMLInputElement>(nodeForNonAnonymous());
72}
73
74void RenderFileUploadControl::updateFromElement()
75{
76 ASSERT(inputElement().isFileUpload());
77
78 if (HTMLInputElement* button = uploadButton()) {
79 bool newCanReceiveDroppedFilesState = inputElement().canReceiveDroppedFiles();
80 if (m_canReceiveDroppedFiles != newCanReceiveDroppedFilesState) {
81 m_canReceiveDroppedFiles = newCanReceiveDroppedFilesState;
82 button->setActive(newCanReceiveDroppedFilesState);
83 }
84 }
85
86 // This only supports clearing out the files, but that's OK because for
87 // security reasons that's the only change the DOM is allowed to make.
88 FileList* files = inputElement().files();
89 ASSERT(files);
90 if (files && files->isEmpty())
91 repaint();
92}
93
94static int nodeWidth(Node* node)
95{
96 return (node && node->renderBox()) ? roundToInt(node->renderBox()->size().width()) : 0;
97}
98
99#if PLATFORM(IOS_FAMILY)
100static int nodeHeight(Node* node)
101{
102 return (node && node->renderBox()) ? roundToInt(node->renderBox()->size().height()) : 0;
103}
104#endif
105
106int RenderFileUploadControl::maxFilenameWidth() const
107{
108#if PLATFORM(IOS_FAMILY)
109 int iconWidth = nodeHeight(uploadButton());
110#endif
111 return std::max(0, snappedIntRect(contentBoxRect()).width() - nodeWidth(uploadButton()) - afterButtonSpacing
112 - (inputElement().icon() ? iconWidth + iconFilenameSpacing : 0));
113}
114
115void RenderFileUploadControl::paintObject(PaintInfo& paintInfo, const LayoutPoint& paintOffset)
116{
117 if (style().visibility() != Visibility::Visible)
118 return;
119
120 if (paintInfo.context().paintingDisabled())
121 return;
122
123 // Push a clip.
124 GraphicsContextStateSaver stateSaver(paintInfo.context(), false);
125 if (paintInfo.phase == PaintPhase::Foreground || paintInfo.phase == PaintPhase::ChildBlockBackgrounds) {
126 IntRect clipRect = enclosingIntRect(LayoutRect(paintOffset.x() + borderLeft(), paintOffset.y() + borderTop(),
127 width() - borderLeft() - borderRight(), height() - borderBottom() - borderTop() + buttonShadowHeight));
128 if (clipRect.isEmpty())
129 return;
130 stateSaver.save();
131 paintInfo.context().clip(clipRect);
132 }
133
134 if (paintInfo.phase == PaintPhase::Foreground) {
135 const String& displayedFilename = fileTextValue();
136 const FontCascade& font = style().fontCascade();
137 TextRun textRun = constructTextRun(displayedFilename, style(), AllowTrailingExpansion, RespectDirection | RespectDirectionOverride);
138
139#if PLATFORM(IOS_FAMILY)
140 int iconHeight = nodeHeight(uploadButton());
141 int iconWidth = iconHeight;
142#endif
143 // Determine where the filename should be placed
144 LayoutUnit contentLeft = paintOffset.x() + borderLeft() + paddingLeft();
145 HTMLInputElement* button = uploadButton();
146 if (!button)
147 return;
148
149 LayoutUnit buttonWidth = nodeWidth(button);
150 LayoutUnit buttonAndIconWidth = buttonWidth + afterButtonSpacing
151 + (inputElement().icon() ? iconWidth + iconFilenameSpacing : 0);
152 LayoutUnit textX;
153 if (style().isLeftToRightDirection())
154 textX = contentLeft + buttonAndIconWidth;
155 else
156 textX = contentLeft + contentWidth() - buttonAndIconWidth - font.width(textRun);
157
158 LayoutUnit textY;
159 // We want to match the button's baseline
160 // FIXME: Make this work with transforms.
161 if (RenderButton* buttonRenderer = downcast<RenderButton>(button->renderer()))
162 textY = paintOffset.y() + borderTop() + paddingTop() + buttonRenderer->baselinePosition(AlphabeticBaseline, true, HorizontalLine, PositionOnContainingLine);
163 else
164 textY = baselinePosition(AlphabeticBaseline, true, HorizontalLine, PositionOnContainingLine);
165
166 paintInfo.context().setFillColor(style().visitedDependentColorWithColorFilter(CSSPropertyColor));
167
168 // Draw the filename
169 paintInfo.context().drawBidiText(font, textRun, IntPoint(roundToInt(textX), roundToInt(textY)));
170
171 if (inputElement().icon()) {
172 // Determine where the icon should be placed
173 LayoutUnit iconY = paintOffset.y() + borderTop() + paddingTop() + (contentHeight() - iconHeight) / 2;
174 LayoutUnit iconX;
175 if (style().isLeftToRightDirection())
176 iconX = contentLeft + buttonWidth + afterButtonSpacing;
177 else
178 iconX = contentLeft + contentWidth() - buttonWidth - afterButtonSpacing - iconWidth;
179
180#if PLATFORM(IOS_FAMILY)
181 if (RenderButton* buttonRenderer = downcast<RenderButton>(button->renderer())) {
182 // Draw the file icon and decorations.
183 IntRect iconRect(iconX, iconY, iconWidth, iconHeight);
184 RenderTheme::FileUploadDecorations decorationsType = inputElement().files()->length() == 1 ? RenderTheme::SingleFile : RenderTheme::MultipleFiles;
185 theme().paintFileUploadIconDecorations(*this, *buttonRenderer, paintInfo, iconRect, inputElement().icon(), decorationsType);
186 }
187#else
188 // Draw the file icon
189 inputElement().icon()->paint(paintInfo.context(), IntRect(roundToInt(iconX), roundToInt(iconY), iconWidth, iconHeight));
190#endif
191 }
192 }
193
194 // Paint the children.
195 RenderBlockFlow::paintObject(paintInfo, paintOffset);
196}
197
198void RenderFileUploadControl::computeIntrinsicLogicalWidths(LayoutUnit& minLogicalWidth, LayoutUnit& maxLogicalWidth) const
199{
200 // Figure out how big the filename space needs to be for a given number of characters
201 // (using "0" as the nominal character).
202 const UChar character = '0';
203 const String characterAsString = String(&character, 1);
204 const FontCascade& font = style().fontCascade();
205 // FIXME: Remove the need for this const_cast by making constructTextRun take a const RenderObject*.
206 float minDefaultLabelWidth = defaultWidthNumChars * font.width(constructTextRun(characterAsString, style(), AllowTrailingExpansion));
207
208 const String label = theme().fileListDefaultLabel(inputElement().multiple());
209 float defaultLabelWidth = font.width(constructTextRun(label, style(), AllowTrailingExpansion));
210 if (HTMLInputElement* button = uploadButton())
211 if (RenderObject* buttonRenderer = button->renderer())
212 defaultLabelWidth += buttonRenderer->maxPreferredLogicalWidth() + afterButtonSpacing;
213 maxLogicalWidth = static_cast<int>(ceilf(std::max(minDefaultLabelWidth, defaultLabelWidth)));
214
215 if (!style().width().isPercentOrCalculated())
216 minLogicalWidth = maxLogicalWidth;
217}
218
219void RenderFileUploadControl::computePreferredLogicalWidths()
220{
221 ASSERT(preferredLogicalWidthsDirty());
222
223 m_minPreferredLogicalWidth = 0;
224 m_maxPreferredLogicalWidth = 0;
225
226 if (style().width().isFixed() && style().width().value() > 0)
227 m_minPreferredLogicalWidth = m_maxPreferredLogicalWidth = adjustContentBoxLogicalWidthForBoxSizing(style().width().value());
228 else
229 computeIntrinsicLogicalWidths(m_minPreferredLogicalWidth, m_maxPreferredLogicalWidth);
230
231 if (style().minWidth().isFixed() && style().minWidth().value() > 0) {
232 m_maxPreferredLogicalWidth = std::max(m_maxPreferredLogicalWidth, adjustContentBoxLogicalWidthForBoxSizing(style().minWidth().value()));
233 m_minPreferredLogicalWidth = std::max(m_minPreferredLogicalWidth, adjustContentBoxLogicalWidthForBoxSizing(style().minWidth().value()));
234 }
235
236 if (style().maxWidth().isFixed()) {
237 m_maxPreferredLogicalWidth = std::min(m_maxPreferredLogicalWidth, adjustContentBoxLogicalWidthForBoxSizing(style().maxWidth().value()));
238 m_minPreferredLogicalWidth = std::min(m_minPreferredLogicalWidth, adjustContentBoxLogicalWidthForBoxSizing(style().maxWidth().value()));
239 }
240
241 int toAdd = horizontalBorderAndPaddingExtent();
242 m_minPreferredLogicalWidth += toAdd;
243 m_maxPreferredLogicalWidth += toAdd;
244
245 setPreferredLogicalWidthsDirty(false);
246}
247
248VisiblePosition RenderFileUploadControl::positionForPoint(const LayoutPoint&, const RenderFragmentContainer*)
249{
250 return VisiblePosition();
251}
252
253HTMLInputElement* RenderFileUploadControl::uploadButton() const
254{
255 ASSERT(inputElement().shadowRoot());
256 Node* buttonNode = inputElement().shadowRoot()->firstChild();
257 return is<HTMLInputElement>(buttonNode) ? downcast<HTMLInputElement>(buttonNode) : nullptr;
258}
259
260String RenderFileUploadControl::buttonValue()
261{
262 if (HTMLInputElement* button = uploadButton())
263 return button->value();
264
265 return String();
266}
267
268String RenderFileUploadControl::fileTextValue() const
269{
270 auto& input = inputElement();
271 ASSERT(inputElement().files());
272 if (input.files()->length() && !input.displayString().isEmpty())
273 return StringTruncator::rightTruncate(input.displayString(), maxFilenameWidth(), style().fontCascade());
274 return theme().fileListNameForWidth(input.files(), style().fontCascade(), maxFilenameWidth(), input.multiple());
275}
276
277} // namespace WebCore
278