OLD | NEW |
| (Empty) |
1 /** | |
2 * Copyright (C) 1999 Lars Knoll (knoll@kde.org) | |
3 * (C) 2000 Simon Hausmann <hausmann@kde.org> | |
4 * (C) 2000 Stefan Schimanski (1Stein@gmx.de) | |
5 * Copyright (C) 2004, 2005, 2006 Apple Computer, Inc. | |
6 * | |
7 * This library is free software; you can redistribute it and/or | |
8 * modify it under the terms of the GNU Library General Public | |
9 * License as published by the Free Software Foundation; either | |
10 * version 2 of the License, or (at your option) any later version. | |
11 * | |
12 * This library is distributed in the hope that it will be useful, | |
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of | |
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |
15 * Library General Public License for more details. | |
16 * | |
17 * You should have received a copy of the GNU Library General Public License | |
18 * along with this library; see the file COPYING.LIB. If not, write to | |
19 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, | |
20 * Boston, MA 02110-1301, USA. | |
21 * | |
22 */ | |
23 | |
24 #include "config.h" | |
25 #include "core/rendering/RenderFrameSet.h" | |
26 | |
27 #include "core/dom/Document.h" | |
28 #include "core/events/MouseEvent.h" | |
29 #include "core/frame/LocalFrame.h" | |
30 #include "core/html/HTMLDimension.h" | |
31 #include "core/html/HTMLFrameSetElement.h" | |
32 #include "core/layout/PaintInfo.h" | |
33 #include "core/page/EventHandler.h" | |
34 #include "core/paint/FrameSetPainter.h" | |
35 #include "core/rendering/RenderFrame.h" | |
36 #include "core/rendering/RenderView.h" | |
37 #include "platform/Cursor.h" | |
38 #include "platform/graphics/GraphicsContext.h" | |
39 | |
40 namespace blink { | |
41 | |
42 RenderFrameSet::RenderFrameSet(HTMLFrameSetElement* frameSet) | |
43 : RenderBox(frameSet) | |
44 , m_isResizing(false) | |
45 , m_isChildResizing(false) | |
46 { | |
47 setInline(false); | |
48 } | |
49 | |
50 RenderFrameSet::~RenderFrameSet() | |
51 { | |
52 } | |
53 | |
54 RenderFrameSet::GridAxis::GridAxis() | |
55 : m_splitBeingResized(noSplit) | |
56 { | |
57 } | |
58 | |
59 HTMLFrameSetElement* RenderFrameSet::frameSet() const | |
60 { | |
61 return toHTMLFrameSetElement(node()); | |
62 } | |
63 | |
64 void RenderFrameSet::paint(const PaintInfo& paintInfo, const LayoutPoint& paintO
ffset) | |
65 { | |
66 FrameSetPainter(*this).paint(paintInfo, paintOffset); | |
67 } | |
68 | |
69 void RenderFrameSet::computePreferredLogicalWidths() | |
70 { | |
71 m_minPreferredLogicalWidth = 0; | |
72 m_maxPreferredLogicalWidth = 0; | |
73 clearPreferredLogicalWidthsDirty(); | |
74 } | |
75 | |
76 void RenderFrameSet::GridAxis::resize(int size) | |
77 { | |
78 m_sizes.resize(size); | |
79 m_deltas.resize(size); | |
80 m_deltas.fill(0); | |
81 | |
82 // To track edges for resizability and borders, we need to be (size + 1). Th
is is because a parent frameset | |
83 // may ask us for information about our left/top/right/bottom edges in order
to make its own decisions about | |
84 // what to do. We are capable of tainting that parent frameset's borders, so
we have to cache this info. | |
85 m_preventResize.resize(size + 1); | |
86 m_allowBorder.resize(size + 1); | |
87 } | |
88 | |
89 void RenderFrameSet::layOutAxis(GridAxis& axis, const Vector<HTMLDimension>& gri
d, int availableLen) | |
90 { | |
91 availableLen = max(availableLen, 0); | |
92 | |
93 int* gridLayout = axis.m_sizes.data(); | |
94 | |
95 if (grid.isEmpty()) { | |
96 gridLayout[0] = availableLen; | |
97 return; | |
98 } | |
99 | |
100 int gridLen = axis.m_sizes.size(); | |
101 ASSERT(gridLen); | |
102 | |
103 int totalRelative = 0; | |
104 int totalFixed = 0; | |
105 int totalPercent = 0; | |
106 int countRelative = 0; | |
107 int countFixed = 0; | |
108 int countPercent = 0; | |
109 | |
110 // First we need to investigate how many columns of each type we have and | |
111 // how much space these columns are going to require. | |
112 for (int i = 0; i < gridLen; ++i) { | |
113 // Count the total length of all of the fixed columns/rows -> totalFixed | |
114 // Count the number of columns/rows which are fixed -> countFixed | |
115 if (grid[i].isAbsolute()) { | |
116 gridLayout[i] = max<int>(grid[i].value(), 0); | |
117 totalFixed += gridLayout[i]; | |
118 countFixed++; | |
119 } | |
120 | |
121 // Count the total percentage of all of the percentage columns/rows -> t
otalPercent | |
122 // Count the number of columns/rows which are percentages -> countPercen
t | |
123 if (grid[i].isPercentage()) { | |
124 gridLayout[i] = max<int>(grid[i].value() * availableLen / 100., 0); | |
125 totalPercent += gridLayout[i]; | |
126 countPercent++; | |
127 } | |
128 | |
129 // Count the total relative of all the relative columns/rows -> totalRel
ative | |
130 // Count the number of columns/rows which are relative -> countRelative | |
131 if (grid[i].isRelative()) { | |
132 totalRelative += max<int>(grid[i].value(), 1); | |
133 countRelative++; | |
134 } | |
135 } | |
136 | |
137 int remainingLen = availableLen; | |
138 | |
139 // Fixed columns/rows are our first priority. If there is not enough space t
o fit all fixed | |
140 // columns/rows we need to proportionally adjust their size. | |
141 if (totalFixed > remainingLen) { | |
142 int remainingFixed = remainingLen; | |
143 | |
144 for (int i = 0; i < gridLen; ++i) { | |
145 if (grid[i].isAbsolute()) { | |
146 gridLayout[i] = (gridLayout[i] * remainingFixed) / totalFixed; | |
147 remainingLen -= gridLayout[i]; | |
148 } | |
149 } | |
150 } else | |
151 remainingLen -= totalFixed; | |
152 | |
153 // Percentage columns/rows are our second priority. Divide the remaining spa
ce proportionally | |
154 // over all percentage columns/rows. IMPORTANT: the size of each column/row
is not relative | |
155 // to 100%, but to the total percentage. For example, if there are three col
umns, each of 75%, | |
156 // and the available space is 300px, each column will become 100px in width. | |
157 if (totalPercent > remainingLen) { | |
158 int remainingPercent = remainingLen; | |
159 | |
160 for (int i = 0; i < gridLen; ++i) { | |
161 if (grid[i].isPercentage()) { | |
162 gridLayout[i] = (gridLayout[i] * remainingPercent) / totalPercen
t; | |
163 remainingLen -= gridLayout[i]; | |
164 } | |
165 } | |
166 } else | |
167 remainingLen -= totalPercent; | |
168 | |
169 // Relative columns/rows are our last priority. Divide the remaining space p
roportionally | |
170 // over all relative columns/rows. IMPORTANT: the relative value of 0* is tr
eated as 1*. | |
171 if (countRelative) { | |
172 int lastRelative = 0; | |
173 int remainingRelative = remainingLen; | |
174 | |
175 for (int i = 0; i < gridLen; ++i) { | |
176 if (grid[i].isRelative()) { | |
177 gridLayout[i] = (max(grid[i].value(), 1.) * remainingRelative) /
totalRelative; | |
178 remainingLen -= gridLayout[i]; | |
179 lastRelative = i; | |
180 } | |
181 } | |
182 | |
183 // If we could not evenly distribute the available space of all of the r
elative | |
184 // columns/rows, the remainder will be added to the last column/row. | |
185 // For example: if we have a space of 100px and three columns (*,*,*), t
he remainder will | |
186 // be 1px and will be added to the last column: 33px, 33px, 34px. | |
187 if (remainingLen) { | |
188 gridLayout[lastRelative] += remainingLen; | |
189 remainingLen = 0; | |
190 } | |
191 } | |
192 | |
193 // If we still have some left over space we need to divide it over the alrea
dy existing | |
194 // columns/rows | |
195 if (remainingLen) { | |
196 // Our first priority is to spread if over the percentage columns. The r
emaining | |
197 // space is spread evenly, for example: if we have a space of 100px, the
columns | |
198 // definition of 25%,25% used to result in two columns of 25px. After th
is the | |
199 // columns will each be 50px in width. | |
200 if (countPercent && totalPercent) { | |
201 int remainingPercent = remainingLen; | |
202 int changePercent = 0; | |
203 | |
204 for (int i = 0; i < gridLen; ++i) { | |
205 if (grid[i].isPercentage()) { | |
206 changePercent = (remainingPercent * gridLayout[i]) / totalPe
rcent; | |
207 gridLayout[i] += changePercent; | |
208 remainingLen -= changePercent; | |
209 } | |
210 } | |
211 } else if (totalFixed) { | |
212 // Our last priority is to spread the remaining space over the fixed
columns. | |
213 // For example if we have 100px of space and two column of each 40px
, both | |
214 // columns will become exactly 50px. | |
215 int remainingFixed = remainingLen; | |
216 int changeFixed = 0; | |
217 | |
218 for (int i = 0; i < gridLen; ++i) { | |
219 if (grid[i].isAbsolute()) { | |
220 changeFixed = (remainingFixed * gridLayout[i]) / totalFixed; | |
221 gridLayout[i] += changeFixed; | |
222 remainingLen -= changeFixed; | |
223 } | |
224 } | |
225 } | |
226 } | |
227 | |
228 // If we still have some left over space we probably ended up with a remaind
er of | |
229 // a division. We cannot spread it evenly anymore. If we have any percentage | |
230 // columns/rows simply spread the remainder equally over all available perce
ntage columns, | |
231 // regardless of their size. | |
232 if (remainingLen && countPercent) { | |
233 int remainingPercent = remainingLen; | |
234 int changePercent = 0; | |
235 | |
236 for (int i = 0; i < gridLen; ++i) { | |
237 if (grid[i].isPercentage()) { | |
238 changePercent = remainingPercent / countPercent; | |
239 gridLayout[i] += changePercent; | |
240 remainingLen -= changePercent; | |
241 } | |
242 } | |
243 } else if (remainingLen && countFixed) { | |
244 // If we don't have any percentage columns/rows we only have | |
245 // fixed columns. Spread the remainder equally over all fixed | |
246 // columns/rows. | |
247 int remainingFixed = remainingLen; | |
248 int changeFixed = 0; | |
249 | |
250 for (int i = 0; i < gridLen; ++i) { | |
251 if (grid[i].isAbsolute()) { | |
252 changeFixed = remainingFixed / countFixed; | |
253 gridLayout[i] += changeFixed; | |
254 remainingLen -= changeFixed; | |
255 } | |
256 } | |
257 } | |
258 | |
259 // Still some left over. Add it to the last column, because it is impossible | |
260 // spread it evenly or equally. | |
261 if (remainingLen) | |
262 gridLayout[gridLen - 1] += remainingLen; | |
263 | |
264 // now we have the final layout, distribute the delta over it | |
265 bool worked = true; | |
266 int* gridDelta = axis.m_deltas.data(); | |
267 for (int i = 0; i < gridLen; ++i) { | |
268 if (gridLayout[i] && gridLayout[i] + gridDelta[i] <= 0) | |
269 worked = false; | |
270 gridLayout[i] += gridDelta[i]; | |
271 } | |
272 // if the deltas broke something, undo them | |
273 if (!worked) { | |
274 for (int i = 0; i < gridLen; ++i) | |
275 gridLayout[i] -= gridDelta[i]; | |
276 axis.m_deltas.fill(0); | |
277 } | |
278 } | |
279 | |
280 void RenderFrameSet::notifyFrameEdgeInfoChanged() | |
281 { | |
282 if (needsLayout()) | |
283 return; | |
284 // FIXME: We should only recompute the edge info with respect to the frame t
hat changed | |
285 // and its adjacent frame(s) instead of recomputing the edge info for the en
tire frameset. | |
286 computeEdgeInfo(); | |
287 } | |
288 | |
289 void RenderFrameSet::fillFromEdgeInfo(const FrameEdgeInfo& edgeInfo, int r, int
c) | |
290 { | |
291 if (edgeInfo.allowBorder(LeftFrameEdge)) | |
292 m_cols.m_allowBorder[c] = true; | |
293 if (edgeInfo.allowBorder(RightFrameEdge)) | |
294 m_cols.m_allowBorder[c + 1] = true; | |
295 if (edgeInfo.preventResize(LeftFrameEdge)) | |
296 m_cols.m_preventResize[c] = true; | |
297 if (edgeInfo.preventResize(RightFrameEdge)) | |
298 m_cols.m_preventResize[c + 1] = true; | |
299 | |
300 if (edgeInfo.allowBorder(TopFrameEdge)) | |
301 m_rows.m_allowBorder[r] = true; | |
302 if (edgeInfo.allowBorder(BottomFrameEdge)) | |
303 m_rows.m_allowBorder[r + 1] = true; | |
304 if (edgeInfo.preventResize(TopFrameEdge)) | |
305 m_rows.m_preventResize[r] = true; | |
306 if (edgeInfo.preventResize(BottomFrameEdge)) | |
307 m_rows.m_preventResize[r + 1] = true; | |
308 } | |
309 | |
310 void RenderFrameSet::computeEdgeInfo() | |
311 { | |
312 m_rows.m_preventResize.fill(frameSet()->noResize()); | |
313 m_rows.m_allowBorder.fill(false); | |
314 m_cols.m_preventResize.fill(frameSet()->noResize()); | |
315 m_cols.m_allowBorder.fill(false); | |
316 | |
317 LayoutObject* child = firstChild(); | |
318 if (!child) | |
319 return; | |
320 | |
321 size_t rows = m_rows.m_sizes.size(); | |
322 size_t cols = m_cols.m_sizes.size(); | |
323 for (size_t r = 0; r < rows; ++r) { | |
324 for (size_t c = 0; c < cols; ++c) { | |
325 FrameEdgeInfo edgeInfo; | |
326 if (child->isFrameSet()) | |
327 edgeInfo = toRenderFrameSet(child)->edgeInfo(); | |
328 else | |
329 edgeInfo = toRenderFrame(child)->edgeInfo(); | |
330 fillFromEdgeInfo(edgeInfo, r, c); | |
331 child = child->nextSibling(); | |
332 if (!child) | |
333 return; | |
334 } | |
335 } | |
336 } | |
337 | |
338 FrameEdgeInfo RenderFrameSet::edgeInfo() const | |
339 { | |
340 FrameEdgeInfo result(frameSet()->noResize(), true); | |
341 | |
342 int rows = frameSet()->totalRows(); | |
343 int cols = frameSet()->totalCols(); | |
344 if (rows && cols) { | |
345 result.setPreventResize(LeftFrameEdge, m_cols.m_preventResize[0]); | |
346 result.setAllowBorder(LeftFrameEdge, m_cols.m_allowBorder[0]); | |
347 result.setPreventResize(RightFrameEdge, m_cols.m_preventResize[cols]); | |
348 result.setAllowBorder(RightFrameEdge, m_cols.m_allowBorder[cols]); | |
349 result.setPreventResize(TopFrameEdge, m_rows.m_preventResize[0]); | |
350 result.setAllowBorder(TopFrameEdge, m_rows.m_allowBorder[0]); | |
351 result.setPreventResize(BottomFrameEdge, m_rows.m_preventResize[rows]); | |
352 result.setAllowBorder(BottomFrameEdge, m_rows.m_allowBorder[rows]); | |
353 } | |
354 | |
355 return result; | |
356 } | |
357 | |
358 void RenderFrameSet::layout() | |
359 { | |
360 ASSERT(needsLayout()); | |
361 | |
362 if (!parent()->isFrameSet() && !document().printing()) { | |
363 setWidth(view()->viewWidth()); | |
364 setHeight(view()->viewHeight()); | |
365 } | |
366 | |
367 unsigned cols = frameSet()->totalCols(); | |
368 unsigned rows = frameSet()->totalRows(); | |
369 | |
370 if (m_rows.m_sizes.size() != rows || m_cols.m_sizes.size() != cols) { | |
371 m_rows.resize(rows); | |
372 m_cols.resize(cols); | |
373 } | |
374 | |
375 LayoutUnit borderThickness = frameSet()->border(); | |
376 layOutAxis(m_rows, frameSet()->rowLengths(), size().height() - (rows - 1) *
borderThickness); | |
377 layOutAxis(m_cols, frameSet()->colLengths(), size().width() - (cols - 1) * b
orderThickness); | |
378 | |
379 positionFrames(); | |
380 | |
381 RenderBox::layout(); | |
382 | |
383 computeEdgeInfo(); | |
384 | |
385 updateLayerTransformAfterLayout(); | |
386 | |
387 clearNeedsLayout(); | |
388 } | |
389 | |
390 static void clearNeedsLayoutOnHiddenFrames(RenderBox* frame) | |
391 { | |
392 for (; frame; frame = frame->nextSiblingBox()) { | |
393 frame->setWidth(0); | |
394 frame->setHeight(0); | |
395 frame->clearNeedsLayout(); | |
396 clearNeedsLayoutOnHiddenFrames(frame->firstChildBox()); | |
397 } | |
398 } | |
399 | |
400 void RenderFrameSet::positionFrames() | |
401 { | |
402 RenderBox* child = firstChildBox(); | |
403 if (!child) | |
404 return; | |
405 | |
406 int rows = frameSet()->totalRows(); | |
407 int cols = frameSet()->totalCols(); | |
408 | |
409 int borderThickness = frameSet()->border(); | |
410 LayoutSize size; | |
411 LayoutPoint position; | |
412 for (int r = 0; r < rows; r++) { | |
413 position.setX(0); | |
414 size.setHeight(m_rows.m_sizes[r]); | |
415 for (int c = 0; c < cols; c++) { | |
416 child->setLocation(position); | |
417 size.setWidth(m_cols.m_sizes[c]); | |
418 | |
419 // has to be resized and itself resize its contents | |
420 if (size != child->size()) { | |
421 child->setSize(size); | |
422 child->setNeedsLayoutAndFullPaintInvalidation(); | |
423 child->layout(); | |
424 } | |
425 | |
426 position.setX(position.x() + size.width() + borderThickness); | |
427 | |
428 child = child->nextSiblingBox(); | |
429 if (!child) | |
430 return; | |
431 } | |
432 position.setY(position.y() + size.height() + borderThickness); | |
433 } | |
434 | |
435 // All the remaining frames are hidden to avoid ugly spurious unflowed frame
s. | |
436 clearNeedsLayoutOnHiddenFrames(child); | |
437 } | |
438 | |
439 void RenderFrameSet::startResizing(GridAxis& axis, int position) | |
440 { | |
441 int split = hitTestSplit(axis, position); | |
442 if (split == noSplit || axis.m_preventResize[split]) { | |
443 axis.m_splitBeingResized = noSplit; | |
444 return; | |
445 } | |
446 axis.m_splitBeingResized = split; | |
447 axis.m_splitResizeOffset = position - splitPosition(axis, split); | |
448 } | |
449 | |
450 void RenderFrameSet::continueResizing(GridAxis& axis, int position) | |
451 { | |
452 if (needsLayout()) | |
453 return; | |
454 if (axis.m_splitBeingResized == noSplit) | |
455 return; | |
456 int currentSplitPosition = splitPosition(axis, axis.m_splitBeingResized); | |
457 int delta = (position - currentSplitPosition) - axis.m_splitResizeOffset; | |
458 if (!delta) | |
459 return; | |
460 axis.m_deltas[axis.m_splitBeingResized - 1] += delta; | |
461 axis.m_deltas[axis.m_splitBeingResized] -= delta; | |
462 setNeedsLayoutAndFullPaintInvalidation(); | |
463 } | |
464 | |
465 bool RenderFrameSet::userResize(MouseEvent* evt) | |
466 { | |
467 if (!m_isResizing) { | |
468 if (needsLayout()) | |
469 return false; | |
470 if (evt->type() == EventTypeNames::mousedown && evt->button() == LeftBut
ton) { | |
471 FloatPoint localPos = absoluteToLocal(FloatPoint(evt->absoluteLocati
on()), UseTransforms); | |
472 startResizing(m_cols, localPos.x()); | |
473 startResizing(m_rows, localPos.y()); | |
474 if (m_cols.m_splitBeingResized != noSplit || m_rows.m_splitBeingResi
zed != noSplit) { | |
475 setIsResizing(true); | |
476 return true; | |
477 } | |
478 } | |
479 } else { | |
480 if (evt->type() == EventTypeNames::mousemove || (evt->type() == EventTyp
eNames::mouseup && evt->button() == LeftButton)) { | |
481 FloatPoint localPos = absoluteToLocal(FloatPoint(evt->absoluteLocati
on()), UseTransforms); | |
482 continueResizing(m_cols, localPos.x()); | |
483 continueResizing(m_rows, localPos.y()); | |
484 if (evt->type() == EventTypeNames::mouseup && evt->button() == LeftB
utton) { | |
485 setIsResizing(false); | |
486 return true; | |
487 } | |
488 } | |
489 } | |
490 | |
491 return false; | |
492 } | |
493 | |
494 void RenderFrameSet::setIsResizing(bool isResizing) | |
495 { | |
496 m_isResizing = isResizing; | |
497 for (LayoutObject* ancestor = parent(); ancestor; ancestor = ancestor->paren
t()) { | |
498 if (ancestor->isFrameSet()) | |
499 toRenderFrameSet(ancestor)->m_isChildResizing = isResizing; | |
500 } | |
501 if (LocalFrame* frame = this->frame()) | |
502 frame->eventHandler().setResizingFrameSet(isResizing ? frameSet() : 0); | |
503 } | |
504 | |
505 bool RenderFrameSet::canResizeRow(const IntPoint& p) const | |
506 { | |
507 int r = hitTestSplit(m_rows, p.y()); | |
508 return r != noSplit && !m_rows.m_preventResize[r]; | |
509 } | |
510 | |
511 bool RenderFrameSet::canResizeColumn(const IntPoint& p) const | |
512 { | |
513 int c = hitTestSplit(m_cols, p.x()); | |
514 return c != noSplit && !m_cols.m_preventResize[c]; | |
515 } | |
516 | |
517 int RenderFrameSet::splitPosition(const GridAxis& axis, int split) const | |
518 { | |
519 if (needsLayout()) | |
520 return 0; | |
521 | |
522 int borderThickness = frameSet()->border(); | |
523 | |
524 int size = axis.m_sizes.size(); | |
525 if (!size) | |
526 return 0; | |
527 | |
528 int position = 0; | |
529 for (int i = 0; i < split && i < size; ++i) | |
530 position += axis.m_sizes[i] + borderThickness; | |
531 return position - borderThickness; | |
532 } | |
533 | |
534 int RenderFrameSet::hitTestSplit(const GridAxis& axis, int position) const | |
535 { | |
536 if (needsLayout()) | |
537 return noSplit; | |
538 | |
539 int borderThickness = frameSet()->border(); | |
540 if (borderThickness <= 0) | |
541 return noSplit; | |
542 | |
543 size_t size = axis.m_sizes.size(); | |
544 if (!size) | |
545 return noSplit; | |
546 | |
547 int splitPosition = axis.m_sizes[0]; | |
548 for (size_t i = 1; i < size; ++i) { | |
549 if (position >= splitPosition && position < splitPosition + borderThickn
ess) | |
550 return i; | |
551 splitPosition += borderThickness + axis.m_sizes[i]; | |
552 } | |
553 return noSplit; | |
554 } | |
555 | |
556 bool RenderFrameSet::isChildAllowed(LayoutObject* child, const LayoutStyle&) con
st | |
557 { | |
558 return child->isFrame() || child->isFrameSet(); | |
559 } | |
560 | |
561 CursorDirective RenderFrameSet::getCursor(const LayoutPoint& point, Cursor& curs
or) const | |
562 { | |
563 IntPoint roundedPoint = roundedIntPoint(point); | |
564 if (canResizeRow(roundedPoint)) { | |
565 cursor = rowResizeCursor(); | |
566 return SetCursor; | |
567 } | |
568 if (canResizeColumn(roundedPoint)) { | |
569 cursor = columnResizeCursor(); | |
570 return SetCursor; | |
571 } | |
572 return RenderBox::getCursor(point, cursor); | |
573 } | |
574 | |
575 } // namespace blink | |
OLD | NEW |