diff options
author | bors-servo <metajack+bors@gmail.com> | 2015-03-02 14:54:52 -0700 |
---|---|---|
committer | bors-servo <metajack+bors@gmail.com> | 2015-03-02 14:54:52 -0700 |
commit | 93d1f40a96df69eb9d38890df96c621e180d78cc (patch) | |
tree | dab39945bbadcbf717af71ce01a6f3b9eafa49a2 | |
parent | 9eaa48b793de78b713e6c3a3c79c4060084d5fbe (diff) | |
parent | 09358b908d937a3dfbb74a5bdcc083dbf2b1df1c (diff) | |
download | servo-93d1f40a96df69eb9d38890df96c621e180d78cc.tar.gz servo-93d1f40a96df69eb9d38890df96c621e180d78cc.zip |
auto merge of #4475 : pcwalton/servo/text-shadow, r=mbrubeck
r? @mbrubeck
Depends on servo/rust-geom#64.
-rw-r--r-- | components/gfx/display_list/mod.rs | 39 | ||||
-rw-r--r-- | components/gfx/paint_context.rs | 301 | ||||
-rw-r--r-- | components/gfx/paint_task.rs | 2 | ||||
-rw-r--r-- | components/layout/display_list_builder.rs | 117 | ||||
-rw-r--r-- | components/layout/fragment.rs | 6 | ||||
-rw-r--r-- | components/script/dom/webidls/CSSStyleDeclaration.webidl | 1 | ||||
-rw-r--r-- | components/servo/Cargo.lock | 2 | ||||
-rw-r--r-- | components/style/properties.mako.rs | 165 | ||||
-rw-r--r-- | ports/cef/Cargo.lock | 2 | ||||
-rw-r--r-- | ports/gonk/Cargo.lock | 2 | ||||
-rw-r--r-- | tests/ref/basic.list | 3 | ||||
-rw-r--r-- | tests/ref/text_shadow_blur_a.html | 15 | ||||
-rw-r--r-- | tests/ref/text_shadow_blur_ref.html | 18 | ||||
-rw-r--r-- | tests/ref/text_shadow_decorations_a.html | 22 | ||||
-rw-r--r-- | tests/ref/text_shadow_decorations_ref.html | 30 | ||||
-rw-r--r-- | tests/ref/text_shadow_multiple_shadows_a.html | 21 | ||||
-rw-r--r-- | tests/ref/text_shadow_multiple_shadows_ref.html | 34 | ||||
-rw-r--r-- | tests/ref/text_shadow_simple_a.html | 22 | ||||
-rw-r--r-- | tests/ref/text_shadow_simple_ref.html | 35 |
19 files changed, 684 insertions, 153 deletions
diff --git a/components/gfx/display_list/mod.rs b/components/gfx/display_list/mod.rs index 694fdeef125..7ad745a1ae9 100644 --- a/components/gfx/display_list/mod.rs +++ b/components/gfx/display_list/mod.rs @@ -50,10 +50,9 @@ pub use azure::azure_hl::GradientStop; pub mod optimizer; -/// The factor that we multiply the blur radius by in order to inflate the boundaries of box shadow -/// display items. This ensures that the box shadow display item boundaries include all the -/// shadow's ink. -pub static BOX_SHADOW_INFLATION_FACTOR: i32 = 3; +/// The factor that we multiply the blur radius by in order to inflate the boundaries of display +/// items that involve a blur. This ensures that the display item boundaries include all the ink. +pub static BLUR_INFLATION_FACTOR: i32 = 3; /// An opaque handle to a node. The only safe operation that can be performed on this node is to /// compare it to another opaque handle or to another node. @@ -248,8 +247,8 @@ impl StackingContext { { let mut paint_subcontext = PaintContext { draw_target: temporary_draw_target.clone(), - font_ctx: &mut *paint_context.font_ctx, - page_rect: paint_context.page_rect, + font_context: &mut *paint_context.font_context, + page_rect: *tile_bounds, screen_rect: paint_context.screen_rect, clip_rect: clip_rect.map(|clip_rect| *clip_rect), transient_clip: None, @@ -714,7 +713,10 @@ impl DisplayItemMetadata { /// Paints a solid color. #[derive(Clone)] pub struct SolidColorDisplayItem { + /// Fields common to all display items. pub base: BaseDisplayItem, + + /// The color. pub color: Color, } @@ -733,8 +735,14 @@ pub struct TextDisplayItem { /// The color of the text. pub text_color: Color, + /// The position of the start of the baseline of this text. pub baseline_origin: Point2D<Au>, + + /// The orientation of the text: upright or sideways left/right. pub orientation: TextOrientation, + + /// The blur radius for this text. If zero, this text is not blurred. + pub blur_radius: Au, } #[derive(Clone, Eq, PartialEq)] @@ -858,8 +866,21 @@ pub struct BoxShadowDisplayItem { /// The spread radius of this shadow. pub spread_radius: Au, - /// True if this shadow is inset; false if it's outset. - pub inset: bool, + /// How we should clip the result. + pub clip_mode: BoxShadowClipMode, +} + +/// How a box shadow should be clipped. +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum BoxShadowClipMode { + /// No special clipping should occur. This is used for (shadowed) text decorations. + None, + /// The area inside `box_bounds` should be clipped out. Corresponds to the normal CSS + /// `box-shadow`. + Outset, + /// The area outside `box_bounds` should be clipped out. Corresponds to the `inset` flag on CSS + /// `box-shadow`. + Inset, } pub enum DisplayItemIterator<'a> { @@ -947,7 +968,7 @@ impl DisplayItem { box_shadow.color, box_shadow.blur_radius, box_shadow.spread_radius, - box_shadow.inset) + box_shadow.clip_mode) } } } diff --git a/components/gfx/paint_context.rs b/components/gfx/paint_context.rs index d831a01d7ec..82184bf06de 100644 --- a/components/gfx/paint_context.rs +++ b/components/gfx/paint_context.rs @@ -4,45 +4,47 @@ //! Painting of display lists using Moz2D/Azure. +use color; +use display_list::TextOrientation::{SidewaysLeft, SidewaysRight, Upright}; +use display_list::{BLUR_INFLATION_FACTOR, BorderRadii, BoxShadowClipMode, ClippingRegion}; +use display_list::{TextDisplayItem}; +use filters; +use font_context::FontContext; +use text::TextRun; +use text::glyph::CharIndex; + use azure::azure::AzIntSize; use azure::azure_hl::{Color, ColorPattern}; use azure::azure_hl::{DrawOptions, DrawSurfaceOptions, DrawTarget, ExtendMode, FilterType}; -use azure::azure_hl::{GaussianBlurInput, GradientStop, Filter, LinearGradientPattern}; -use azure::azure_hl::{PatternRef, Path, PathBuilder, CompositionOp}; use azure::azure_hl::{GaussianBlurAttribute, StrokeOptions, SurfaceFormat}; +use azure::azure_hl::{GaussianBlurInput, GradientStop, Filter, FilterNode, LinearGradientPattern}; use azure::azure_hl::{JoinStyle, CapStyle}; +use azure::azure_hl::{PatternRef, Path, PathBuilder, CompositionOp}; use azure::scaled_font::ScaledFont; use azure::{AzFloat, struct__AzDrawOptions, struct__AzGlyph}; use azure::{struct__AzGlyphBuffer, struct__AzPoint, AzDrawTargetFillGlyphs}; -use color; -use display_list::TextOrientation::{SidewaysLeft, SidewaysRight, Upright}; -use display_list::{BOX_SHADOW_INFLATION_FACTOR, BorderRadii, ClippingRegion, TextDisplayItem}; -use filters; -use font_context::FontContext; use geom::matrix2d::Matrix2D; use geom::point::Point2D; use geom::rect::Rect; use geom::side_offsets::SideOffsets2D; use geom::size::Size2D; use libc::types::common::c99::{uint16_t, uint32_t}; -use png::PixelsByColorType; use net::image::base::Image; -use util::geometry::{Au, MAX_RECT}; -use util::opts; -use util::range::Range; +use png::PixelsByColorType; use std::default::Default; use std::f32; use std::mem; use std::num::Float; use std::ptr; -use style::computed_values::{border_style, filter, mix_blend_mode}; use std::sync::Arc; -use text::TextRun; -use text::glyph::CharIndex; +use style::computed_values::{border_style, filter, mix_blend_mode}; +use util::geometry::{self, Au, MAX_RECT, ZERO_RECT}; +use util::opts; +use util::range::Range; pub struct PaintContext<'a> { pub draw_target: DrawTarget, - pub font_ctx: &'a mut Box<FontContext>, + pub font_context: &'a mut Box<FontContext>, /// The rectangle that this context encompasses in page coordinates. pub page_rect: Rect<f32>, /// The rectangle that this context encompasses in screen coordinates (pixels). @@ -803,8 +805,9 @@ impl<'a> PaintContext<'a> { self.draw_border_path(&original_bounds, direction, border, radius, scaled_color); } + /// Draws the given text display item into the current context. pub fn draw_text(&mut self, text: &TextDisplayItem) { - let current_transform = self.draw_target.get_transform(); + let draw_target_transform = self.draw_target.get_transform(); // Optimization: Don’t set a transform matrix for upright text, and pass a start point to // `draw_text_into_context`. @@ -816,35 +819,41 @@ impl<'a> PaintContext<'a> { SidewaysLeft => { let x = text.baseline_origin.x.to_subpx() as AzFloat; let y = text.baseline_origin.y.to_subpx() as AzFloat; - self.draw_target.set_transform(¤t_transform.mul(&Matrix2D::new(0., -1., - 1., 0., - x, y))); + self.draw_target.set_transform(&draw_target_transform.mul(&Matrix2D::new(0., -1., + 1., 0., + x, y))); Point2D::zero() } SidewaysRight => { let x = text.baseline_origin.x.to_subpx() as AzFloat; let y = text.baseline_origin.y.to_subpx() as AzFloat; - self.draw_target.set_transform(¤t_transform.mul(&Matrix2D::new(0., 1., - -1., 0., - x, y))); + self.draw_target.set_transform(&draw_target_transform.mul(&Matrix2D::new(0., 1., + -1., 0., + x, y))); Point2D::zero() } }; - self.font_ctx + // Draw the text. + let temporary_draw_target = + self.create_draw_target_for_blur_if_necessary(&text.base.bounds, text.blur_radius); + self.font_context .get_paint_font_from_template(&text.text_run.font_template, text.text_run.actual_pt_size) .borrow() - .draw_text_into_context(self, - &*text.text_run, - &text.range, - baseline_origin, - text.text_color, - opts::get().enable_text_antialiasing); + .draw_text(&temporary_draw_target.draw_target, + &*text.text_run, + &text.range, + baseline_origin, + text.text_color, + opts::get().enable_text_antialiasing); + + // Blur, if necessary. + self.blur_if_necessary(temporary_draw_target, text.blur_radius); // Undo the transform, only when we did one. if text.orientation != Upright { - self.draw_target.set_transform(¤t_transform) + self.draw_target.set_transform(&draw_target_transform) } } @@ -930,80 +939,85 @@ impl<'a> PaintContext<'a> { color: Color, blur_radius: Au, spread_radius: Au, - inset: bool) { + clip_mode: BoxShadowClipMode) { // Remove both the transient clip and the stacking context clip, because we may need to // draw outside the stacking context's clip. self.remove_transient_clip_if_applicable(); self.pop_clip_if_applicable(); - // If we have blur, create a new draw target that's the same size as this tile, but with - // enough space around the edges to hold the entire blur. (If we don't do the latter, then - // there will be seams between tiles.) - // - // FIXME(pcwalton): This draw target might be larger than necessary and waste memory. - let side_inflation = (blur_radius * BOX_SHADOW_INFLATION_FACTOR).to_subpx().ceil() as i32; - let draw_target_transform = self.draw_target.get_transform(); - let temporary_draw_target; - if blur_radius > Au(0) { - let draw_target_size = self.draw_target.get_size(); - let draw_target_size = Size2D(draw_target_size.width, draw_target_size.height); - let inflated_draw_target_size = Size2D(draw_target_size.width + side_inflation * 2, - draw_target_size.height + side_inflation * 2); - temporary_draw_target = - self.draw_target.create_similar_draw_target(&inflated_draw_target_size, - self.draw_target.get_format()); - temporary_draw_target.set_transform( - &Matrix2D::identity().translate(side_inflation as AzFloat, - side_inflation as AzFloat) - .mul(&draw_target_transform)); - } else { - temporary_draw_target = self.draw_target.clone(); - } - + // If we have blur, create a new draw target. let shadow_bounds = box_bounds.translate(offset).inflate(spread_radius, spread_radius); + let side_inflation = blur_radius * BLUR_INFLATION_FACTOR; + let inflated_shadow_bounds = shadow_bounds.inflate(side_inflation, side_inflation); + let temporary_draw_target = + self.create_draw_target_for_blur_if_necessary(&inflated_shadow_bounds, blur_radius); + let path; - if inset { - path = temporary_draw_target.create_rectangular_border_path(&MAX_RECT, &shadow_bounds); - self.draw_target.push_clip(&self.draw_target.create_rectangular_path(box_bounds)) - } else { - path = temporary_draw_target.create_rectangular_path(&shadow_bounds); - self.draw_target.push_clip(&self.draw_target - .create_rectangular_border_path(&MAX_RECT, box_bounds)) + match clip_mode { + BoxShadowClipMode::Inset => { + path = temporary_draw_target.draw_target + .create_rectangular_border_path(&MAX_RECT, + &shadow_bounds); + self.draw_target.push_clip(&self.draw_target.create_rectangular_path(box_bounds)) + } + BoxShadowClipMode::Outset => { + path = temporary_draw_target.draw_target.create_rectangular_path(&shadow_bounds); + self.draw_target.push_clip(&self.draw_target + .create_rectangular_border_path(&MAX_RECT, + box_bounds)) + } + BoxShadowClipMode::None => { + path = temporary_draw_target.draw_target.create_rectangular_path(&shadow_bounds) + } } - temporary_draw_target.fill(&path, &ColorPattern::new(color), &DrawOptions::new(1.0, 0)); - - // Blur, if we need to. - if blur_radius > Au(0) { - // Go ahead and create the blur now. Despite the name, Azure's notion of `StdDeviation` - // describes the blur radius, not the sigma for the Gaussian blur. - let blur_filter = self.draw_target.create_filter(FilterType::GaussianBlur); - blur_filter.set_attribute(GaussianBlurAttribute::StdDeviation(blur_radius.to_subpx() as - AzFloat)); - blur_filter.set_input(GaussianBlurInput, &temporary_draw_target.snapshot()); - - // Blit the blur onto the tile. We undo the transforms here because we want to directly - // stack the temporary draw target onto the tile. - temporary_draw_target.set_transform(&Matrix2D::identity()); - self.draw_target.set_transform(&Matrix2D::identity()); - let temporary_draw_target_size = temporary_draw_target.get_size(); - self.draw_target - .draw_filter(&blur_filter, - &Rect(Point2D(0.0, 0.0), - Size2D(temporary_draw_target_size.width as AzFloat, - temporary_draw_target_size.height as AzFloat)), - &Point2D(-side_inflation as AzFloat, -side_inflation as AzFloat), - DrawOptions::new(1.0, 0)); - self.draw_target.set_transform(&draw_target_transform); - } + // Draw the shadow, and blur if we need to. + temporary_draw_target.draw_target.fill(&path, + &ColorPattern::new(color), + &DrawOptions::new(1.0, 0)); + self.blur_if_necessary(temporary_draw_target, blur_radius); - // Undo the draw target's clip. - self.draw_target.pop_clip(); + // Undo the draw target's clip if we need to, and push back the stacking context clip. + if clip_mode != BoxShadowClipMode::None { + self.draw_target.pop_clip() + } - // Push back the stacking context clip. self.push_clip_if_applicable(); } + /// If we have blur, create a new draw target that's the same size as this tile, but with + /// enough space around the edges to hold the entire blur. (If we don't do the latter, then + /// there will be seams between tiles.) + fn create_draw_target_for_blur_if_necessary(&self, box_bounds: &Rect<Au>, blur_radius: Au) + -> TemporaryDrawTarget { + if blur_radius == Au(0) { + return TemporaryDrawTarget::from_main_draw_target(&self.draw_target) + } + + // Intersect display item bounds with the tile bounds inflated by blur radius to get the + // smallest possible rectangle that encompasses all the paint. + let side_inflation = blur_radius * BLUR_INFLATION_FACTOR; + let tile_box_bounds = + geometry::f32_rect_to_au_rect(self.page_rect).intersection(box_bounds) + .unwrap_or(ZERO_RECT) + .inflate(side_inflation, side_inflation); + TemporaryDrawTarget::from_bounds(&self.draw_target, &tile_box_bounds) + } + + /// Performs a blur using the draw target created in + /// `create_draw_target_for_blur_if_necessary`. + fn blur_if_necessary(&self, temporary_draw_target: TemporaryDrawTarget, blur_radius: Au) { + if blur_radius == Au(0) { + return + } + + let blur_filter = self.draw_target.create_filter(FilterType::GaussianBlur); + blur_filter.set_attribute(GaussianBlurAttribute::StdDeviation(blur_radius.to_subpx() as + AzFloat)); + blur_filter.set_input(GaussianBlurInput, &temporary_draw_target.draw_target.snapshot()); + temporary_draw_target.draw_filter(&self.draw_target, blur_filter); + } + pub fn push_clip_if_applicable(&self) { if let Some(ref clip_rect) = self.clip_rect { self.draw_push_clip(clip_rect) @@ -1042,23 +1056,31 @@ impl<'a> PaintContext<'a> { pub trait ToAzurePoint { fn to_azure_point(&self) -> Point2D<AzFloat>; + fn to_subpx_azure_point(&self) -> Point2D<AzFloat>; } impl ToAzurePoint for Point2D<Au> { fn to_azure_point(&self) -> Point2D<AzFloat> { Point2D(self.x.to_nearest_px() as AzFloat, self.y.to_nearest_px() as AzFloat) } + fn to_subpx_azure_point(&self) -> Point2D<AzFloat> { + Point2D(self.x.to_subpx() as AzFloat, self.y.to_subpx() as AzFloat) + } } pub trait ToAzureRect { fn to_azure_rect(&self) -> Rect<AzFloat>; + fn to_subpx_azure_rect(&self) -> Rect<AzFloat>; } impl ToAzureRect for Rect<Au> { fn to_azure_rect(&self) -> Rect<AzFloat> { - Rect(self.origin.to_azure_point(), - Size2D(self.size.width.to_nearest_px() as AzFloat, - self.size.height.to_nearest_px() as AzFloat)) + Rect(self.origin.to_azure_point(), Size2D(self.size.width.to_nearest_px() as AzFloat, + self.size.height.to_nearest_px() as AzFloat)) + } + fn to_subpx_azure_rect(&self) -> Rect<AzFloat> { + Rect(self.origin.to_subpx_azure_point(), Size2D(self.size.width.to_subpx() as AzFloat, + self.size.height.to_subpx() as AzFloat)) } } @@ -1127,24 +1149,23 @@ impl ToRadiiPx for BorderRadii<Au> { } trait ScaledFontExtensionMethods { - fn draw_text_into_context(&self, - rctx: &PaintContext, - run: &Box<TextRun>, - range: &Range<CharIndex>, - baseline_origin: Point2D<Au>, - color: Color, - antialias: bool); + fn draw_text(&self, + draw_target: &DrawTarget, + run: &Box<TextRun>, + range: &Range<CharIndex>, + baseline_origin: Point2D<Au>, + color: Color, + antialias: bool); } impl ScaledFontExtensionMethods for ScaledFont { - fn draw_text_into_context(&self, - rctx: &PaintContext, - run: &Box<TextRun>, - range: &Range<CharIndex>, - baseline_origin: Point2D<Au>, - color: Color, - antialias: bool) { - let target = rctx.get_draw_target(); + fn draw_text(&self, + draw_target: &DrawTarget, + run: &Box<TextRun>, + range: &Range<CharIndex>, + baseline_origin: Point2D<Au>, + color: Color, + antialias: bool) { let pattern = ColorPattern::new(color); let azure_pattern = pattern.azure_color_pattern; assert!(!azure_pattern.is_null()); @@ -1190,7 +1211,7 @@ impl ScaledFontExtensionMethods for ScaledFont { unsafe { // TODO(Issue #64): this call needs to move into azure_hl.rs - AzDrawTargetFillGlyphs(target.azure_draw_target, + AzDrawTargetFillGlyphs(draw_target.azure_draw_target, self.get_ref(), &mut glyphbuf, azure_pattern, @@ -1290,3 +1311,65 @@ impl ToAzureCompositionOp for mix_blend_mode::T { } } +/// Represents a temporary drawing surface. Some operations that perform complex compositing +/// operations need this. +struct TemporaryDrawTarget { + /// The draw target. + draw_target: DrawTarget, + /// The distance from the top left of the main draw target to the top left of this temporary + /// draw target. + offset: Point2D<AzFloat>, +} + +impl TemporaryDrawTarget { + /// Creates a temporary draw target that simply draws to the main draw target. + fn from_main_draw_target(main_draw_target: &DrawTarget) -> TemporaryDrawTarget { + TemporaryDrawTarget { + draw_target: main_draw_target.clone(), + offset: Point2D(0.0, 0.0), + } + } + + /// Creates a temporary draw target large enough to encompass the given bounding rect in page + /// coordinates. The temporary draw target will have the same transform as the tile we're + /// drawing to. + fn from_bounds(main_draw_target: &DrawTarget, bounds: &Rect<Au>) -> TemporaryDrawTarget { + let draw_target_transform = main_draw_target.get_transform(); + let temporary_draw_target_bounds = + draw_target_transform.transform_rect(&bounds.to_subpx_azure_rect()); + let temporary_draw_target_size = + Size2D(temporary_draw_target_bounds.size.width.ceil() as i32, + temporary_draw_target_bounds.size.height.ceil() as i32); + let temporary_draw_target = + main_draw_target.create_similar_draw_target(&temporary_draw_target_size, + main_draw_target.get_format()); + let matrix = + Matrix2D::identity().translate(-temporary_draw_target_bounds.origin.x as AzFloat, + -temporary_draw_target_bounds.origin.y as AzFloat) + .mul(&draw_target_transform); + temporary_draw_target.set_transform(&matrix); + TemporaryDrawTarget { + draw_target: temporary_draw_target, + offset: temporary_draw_target_bounds.origin, + } + } + + /// Composites this temporary draw target onto the main surface, with the given Azure filter. + fn draw_filter(self, main_draw_target: &DrawTarget, filter: FilterNode) { + let main_draw_target_transform = main_draw_target.get_transform(); + let temporary_draw_target_size = self.draw_target.get_size(); + let temporary_draw_target_size = Size2D(temporary_draw_target_size.width as AzFloat, + temporary_draw_target_size.height as AzFloat); + + // Blit the blur onto the tile. We undo the transforms here because we want to directly + // stack the temporary draw target onto the tile. + main_draw_target.set_transform(&Matrix2D::identity()); + main_draw_target.draw_filter(&filter, + &Rect(Point2D(0.0, 0.0), temporary_draw_target_size), + &self.offset, + DrawOptions::new(1.0, 0)); + main_draw_target.set_transform(&main_draw_target_transform); + + } +} + diff --git a/components/gfx/paint_task.rs b/components/gfx/paint_task.rs index 5c9008bb009..731842d3f89 100644 --- a/components/gfx/paint_task.rs +++ b/components/gfx/paint_task.rs @@ -536,7 +536,7 @@ impl WorkerThread { // Build the paint context. let mut paint_context = PaintContext { draw_target: draw_target.clone(), - font_ctx: &mut self.font_context, + font_context: &mut self.font_context, page_rect: tile.page_rect, screen_rect: tile.screen_rect, clip_rect: None, diff --git a/components/layout/display_list_builder.rs b/components/layout/display_list_builder.rs index b41c407eb58..25a292e2780 100644 --- a/components/layout/display_list_builder.rs +++ b/components/layout/display_list_builder.rs @@ -24,8 +24,8 @@ use util::{OpaqueNodeMethods, ToGfxColor}; use geom::approxeq::ApproxEq; use geom::{Point2D, Rect, Size2D, SideOffsets2D}; use gfx::color; -use gfx::display_list::{BOX_SHADOW_INFLATION_FACTOR, BaseDisplayItem, BorderDisplayItem}; -use gfx::display_list::{BorderRadii, BoxShadowDisplayItem, ClippingRegion}; +use gfx::display_list::{BLUR_INFLATION_FACTOR, BaseDisplayItem, BorderDisplayItem}; +use gfx::display_list::{BorderRadii, BoxShadowClipMode, BoxShadowDisplayItem, ClippingRegion}; use gfx::display_list::{DisplayItem, DisplayList, DisplayItemMetadata}; use gfx::display_list::{GradientDisplayItem}; use gfx::display_list::{GradientStop, ImageDisplayItem, LineDisplayItem}; @@ -39,7 +39,7 @@ use msg::constellation_msg::Msg as ConstellationMsg; use msg::constellation_msg::ConstellationChan; use net::image::holder::ImageHolder; use servo_util::cursor::Cursor; -use servo_util::geometry::{self, Au, to_px, to_frac_px}; +use servo_util::geometry::{self, Au, ZERO_POINT, to_px, to_frac_px}; use servo_util::logical_geometry::{LogicalPoint, LogicalRect, LogicalSize}; use servo_util::opts; use std::default::Default; @@ -196,12 +196,17 @@ pub trait FragmentDisplayListBuilding { stacking_relative_border_box: &Rect<Au>) -> ClippingRegion; - /// Creates the text display item for one text fragment. + /// Creates the text display item for one text fragment. This can be called multiple times for + /// one fragment if there are text shadows. + /// + /// `shadow_blur_radius` will be `Some` if this is a shadow, even if the blur radius is zero. fn build_display_list_for_text_fragment(&self, display_list: &mut DisplayList, text_fragment: &ScannedTextFragmentInfo, text_color: RGBA, stacking_relative_content_box: &Rect<Au>, + shadow_blur_radius: Option<Au>, + offset: &Point2D<Au>, clip: &ClippingRegion); /// Creates the display item for a text decoration: underline, overline, or line-through. @@ -209,7 +214,8 @@ pub trait FragmentDisplayListBuilding { display_list: &mut DisplayList, color: &RGBA, stacking_relative_box: &LogicalRect<Au>, - clip: &ClippingRegion); + clip: &ClippingRegion, + blur_radius: Au); /// A helper method that `build_display_list` calls to create per-fragment-type display items. fn build_fragment_type_specific_display_items(&mut self, @@ -535,11 +541,10 @@ impl FragmentDisplayListBuilding for Fragment { clip: &ClippingRegion) { // NB: According to CSS-BACKGROUNDS, box shadows render in *reverse* order (front to back). for box_shadow in style.get_effects().box_shadow.iter().rev() { - let inflation = box_shadow.spread_radius + box_shadow.blur_radius * - BOX_SHADOW_INFLATION_FACTOR; - let bounds = - absolute_bounds.translate(&Point2D(box_shadow.offset_x, box_shadow.offset_y)) - .inflate(inflation, inflation); + let bounds = shadow_bounds(&absolute_bounds.translate(&Point2D(box_shadow.offset_x, + box_shadow.offset_y)), + box_shadow.blur_radius, + box_shadow.spread_radius); list.push(DisplayItem::BoxShadowClass(box BoxShadowDisplayItem { base: BaseDisplayItem::new(bounds, DisplayItemMetadata::new(self.node, @@ -551,7 +556,11 @@ impl FragmentDisplayListBuilding for Fragment { offset: Point2D(box_shadow.offset_x, box_shadow.offset_y), blur_radius: box_shadow.blur_radius, spread_radius: box_shadow.spread_radius, - inset: box_shadow.inset, + clip_mode: if box_shadow.inset { + BoxShadowClipMode::Inset + } else { + BoxShadowClipMode::Outset + }, }), level); } } @@ -841,19 +850,31 @@ impl FragmentDisplayListBuilding for Fragment { self.stacking_relative_content_box(stacking_relative_border_box); match self.specific { - SpecificFragmentInfo::UnscannedText(_) => { - panic!("Shouldn't see unscanned fragments here.") - } - SpecificFragmentInfo::TableColumn(_) => { - panic!("Shouldn't see table column fragments here.") - } SpecificFragmentInfo::ScannedText(ref text_fragment) => { - // Create the main text display item. + // Create items for shadows. + // + // NB: According to CSS-BACKGROUNDS, text shadows render in *reverse* order (front + // to back). let text_color = self.style().get_color().color; + for text_shadow in self.style.get_effects().text_shadow.0.iter().rev() { + let offset = &Point2D(text_shadow.offset_x, text_shadow.offset_y); + let color = self.style().resolve_color(text_shadow.color); + self.build_display_list_for_text_fragment(display_list, + &**text_fragment, + color, + &stacking_relative_content_box, + Some(text_shadow.blur_radius), + offset, + clip); + } + + // Create the main text display item. self.build_display_list_for_text_fragment(display_list, &**text_fragment, text_color, &stacking_relative_content_box, + None, + &Point2D(Au(0), Au(0)), clip); if opts::get().show_debug_fragment_borders { @@ -932,6 +953,12 @@ impl FragmentDisplayListBuilding for Fragment { display_list.content.push_back(DisplayItem::ImageClass(canvas_display_item)); } + SpecificFragmentInfo::UnscannedText(_) => { + panic!("Shouldn't see unscanned fragments here.") + } + SpecificFragmentInfo::TableColumn(_) => { + panic!("Shouldn't see table column fragments here.") + } } } @@ -984,6 +1011,8 @@ impl FragmentDisplayListBuilding for Fragment { text_fragment: &ScannedTextFragmentInfo, text_color: RGBA, stacking_relative_content_box: &Rect<Au>, + shadow_blur_radius: Option<Au>, + offset: &Point2D<Au>, clip: &ClippingRegion) { // Determine the orientation and cursor to use. let (orientation, cursor) = if self.style.writing_mode.is_vertical() { @@ -1001,6 +1030,7 @@ impl FragmentDisplayListBuilding for Fragment { // FIXME(pcwalton): Get the real container size. let container_size = Size2D::zero(); let metrics = &text_fragment.run.font_metrics; + let stacking_relative_content_box = stacking_relative_content_box.translate(offset); let baseline_origin = stacking_relative_content_box.origin + LogicalPoint::new(self.style.writing_mode, Au(0), @@ -1009,7 +1039,7 @@ impl FragmentDisplayListBuilding for Fragment { // Create the text display item. display_list.content.push_back(DisplayItem::TextClass(box TextDisplayItem { - base: BaseDisplayItem::new(*stacking_relative_content_box, + base: BaseDisplayItem::new(stacking_relative_content_box, DisplayItemMetadata::new(self.node, self.style(), cursor), (*clip).clone()), text_run: text_fragment.run.clone(), @@ -1017,13 +1047,23 @@ impl FragmentDisplayListBuilding for Fragment { text_color: text_color.to_gfx_color(), orientation: orientation, baseline_origin: baseline_origin, + blur_radius: shadow_blur_radius.unwrap_or(Au(0)), })); // Create display items for text decorations. - let text_decorations = self.style().get_inheritedtext()._servo_text_decorations_in_effect; + let mut text_decorations = self.style() + .get_inheritedtext() + ._servo_text_decorations_in_effect; + if shadow_blur_radius.is_some() { + // If we're painting a shadow, paint the decorations the same color as the shadow. + text_decorations.underline = text_decorations.underline.map(|_| text_color); + text_decorations.overline = text_decorations.overline.map(|_| text_color); + text_decorations.line_through = text_decorations.line_through.map(|_| text_color); + } + let stacking_relative_content_box = LogicalRect::from_physical(self.style.writing_mode, - *stacking_relative_content_box, + stacking_relative_content_box, container_size); if let Some(ref underline_color) = text_decorations.underline { let mut stacking_relative_box = stacking_relative_content_box; @@ -1033,7 +1073,8 @@ impl FragmentDisplayListBuilding for Fragment { self.build_display_list_for_text_decoration(display_list, underline_color, &stacking_relative_box, - clip) + clip, + shadow_blur_radius.unwrap_or(Au(0))) } if let Some(ref overline_color) = text_decorations.overline { @@ -1042,7 +1083,8 @@ impl FragmentDisplayListBuilding for Fragment { self.build_display_list_for_text_decoration(display_list, overline_color, &stacking_relative_box, - clip) + clip, + shadow_blur_radius.unwrap_or(Au(0))) } if let Some(ref line_through_color) = text_decorations.line_through { @@ -1053,7 +1095,8 @@ impl FragmentDisplayListBuilding for Fragment { self.build_display_list_for_text_decoration(display_list, line_through_color, &stacking_relative_box, - clip) + clip, + shadow_blur_radius.unwrap_or(Au(0))) } } @@ -1061,16 +1104,27 @@ impl FragmentDisplayListBuilding for Fragment { display_list: &mut DisplayList, color: &RGBA, stacking_relative_box: &LogicalRect<Au>, - clip: &ClippingRegion) { + clip: &ClippingRegion, + blur_radius: Au) { + // Perhaps surprisingly, text decorations are box shadows. This is because they may need + // to have blur in the case of `text-shadow`, and this doesn't hurt performance because box + // shadows are optimized into essentially solid colors if there is no need for the blur. + // // FIXME(pcwalton, #2795): Get the real container size. let container_size = Size2D::zero(); let stacking_relative_box = stacking_relative_box.to_physical(self.style.writing_mode, container_size); - let metadata = DisplayItemMetadata::new(self.node, &*self.style, Cursor::DefaultCursor); - display_list.content.push_back(DisplayItem::SolidColorClass(box SolidColorDisplayItem { - base: BaseDisplayItem::new(stacking_relative_box, metadata, (*clip).clone()), + display_list.content.push_back(DisplayItem::BoxShadowClass(box BoxShadowDisplayItem { + base: BaseDisplayItem::new(shadow_bounds(&stacking_relative_box, blur_radius, Au(0)), + metadata, + (*clip).clone()), + box_bounds: stacking_relative_box, color: color.to_gfx_color(), + offset: ZERO_POINT, + blur_radius: blur_radius, + spread_radius: Au(0), + clip_mode: BoxShadowClipMode::None, })) } } @@ -1423,3 +1477,10 @@ impl StackingContextConstruction for DisplayList { } } +/// Adjusts `content_rect` as necessary for the given spread, and blur so that the resulting +/// bounding rect contains all of a shadow's ink. +fn shadow_bounds(content_rect: &Rect<Au>, blur_radius: Au, spread_radius: Au) -> Rect<Au> { + let inflation = spread_radius + blur_radius * BLUR_INFLATION_FACTOR; + content_rect.inflate(inflation, inflation) +} + diff --git a/components/layout/fragment.rs b/components/layout/fragment.rs index 89b6d743fd1..26a91139b31 100644 --- a/components/layout/fragment.rs +++ b/components/layout/fragment.rs @@ -25,7 +25,7 @@ use wrapper::{TLayoutNode, ThreadSafeLayoutNode}; use geom::num::Zero; use geom::{Point2D, Rect, Size2D}; -use gfx::display_list::{BOX_SHADOW_INFLATION_FACTOR, OpaqueNode}; +use gfx::display_list::{BLUR_INFLATION_FACTOR, OpaqueNode}; use gfx::text::glyph::CharIndex; use gfx::text::text_run::{TextRun, TextRunSlice}; use script_traits::UntrustedNodeAddress; @@ -2043,8 +2043,8 @@ impl Fragment { // Box shadows cause us to draw outside our border box. for box_shadow in self.style().get_effects().box_shadow.iter() { let offset = Point2D(box_shadow.offset_x, box_shadow.offset_y); - let inflation = box_shadow.spread_radius + - box_shadow.blur_radius * BOX_SHADOW_INFLATION_FACTOR; + let inflation = box_shadow.spread_radius + box_shadow.blur_radius * + BLUR_INFLATION_FACTOR; overflow = overflow.union(&border_box.translate(&offset).inflate(inflation, inflation)) } diff --git a/components/script/dom/webidls/CSSStyleDeclaration.webidl b/components/script/dom/webidls/CSSStyleDeclaration.webidl index 0d716d4ab15..1d642acf17c 100644 --- a/components/script/dom/webidls/CSSStyleDeclaration.webidl +++ b/components/script/dom/webidls/CSSStyleDeclaration.webidl @@ -79,6 +79,7 @@ partial interface CSSStyleDeclaration { [TreatNullAs=EmptyString] attribute DOMString boxSizing; [TreatNullAs=EmptyString] attribute DOMString boxShadow; + [TreatNullAs=EmptyString] attribute DOMString textShadow; //[TreatNullAs=EmptyString] attribute DOMString float; //XXXjdm need BinaryName annotation diff --git a/components/servo/Cargo.lock b/components/servo/Cargo.lock index 941bd221322..a52a5ec3191 100644 --- a/components/servo/Cargo.lock +++ b/components/servo/Cargo.lock @@ -292,7 +292,7 @@ dependencies = [ [[package]] name = "geom" version = "0.1.0" -source = "git+https://github.com/servo/rust-geom#6b079ba2738ed15bac2b6ec66850494afb9f2b4c" +source = "git+https://github.com/servo/rust-geom#876c2fceee211130d1294eacdc1bd8742c52540e" dependencies = [ "rustc-serialize 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)", ] diff --git a/components/style/properties.mako.rs b/components/style/properties.mako.rs index b9049a0a30c..2ff84b2107c 100644 --- a/components/style/properties.mako.rs +++ b/components/style/properties.mako.rs @@ -2030,6 +2030,171 @@ pub mod longhands { } </%self:longhand> + <%self:longhand name="text-shadow"> + use cssparser::{self, ToCss}; + use std::fmt; + use text_writer::{self, TextWriter}; + + use values::computed::{Context, ToComputedValue}; + + #[derive(Clone, PartialEq)] + pub struct SpecifiedValue(Vec<SpecifiedTextShadow>); + + #[derive(Clone, PartialEq)] + pub struct SpecifiedTextShadow { + pub offset_x: specified::Length, + pub offset_y: specified::Length, + pub blur_radius: specified::Length, + pub color: Option<specified::CSSColor>, + } + + impl fmt::Debug for SpecifiedTextShadow { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let _ = write!(f, + "{:?} {:?} {:?}", + self.offset_x, + self.offset_y, + self.blur_radius); + if let Some(ref color) = self.color { + let _ = write!(f, "{:?}", color); + } + Ok(()) + } + } + + pub mod computed_value { + use cssparser::Color; + use util::geometry::Au; + + #[derive(Clone, PartialEq, Debug)] + pub struct T(pub Vec<TextShadow>); + + #[derive(Clone, PartialEq, Debug)] + pub struct TextShadow { + pub offset_x: Au, + pub offset_y: Au, + pub blur_radius: Au, + pub color: Color, + } + } + + impl ToCss for SpecifiedValue { + fn to_css<W>(&self, dest: &mut W) -> text_writer::Result where W: TextWriter { + let mut iter = self.0.iter(); + if let Some(shadow) = iter.next() { + try!(shadow.to_css(dest)); + } else { + try!(dest.write_str("none")); + return Ok(()) + } + for shadow in iter { + try!(dest.write_str(", ")); + try!(shadow.to_css(dest)); + } + Ok(()) + } + } + + impl ToCss for SpecifiedTextShadow { + fn to_css<W>(&self, dest: &mut W) -> text_writer::Result where W: TextWriter { + try!(self.offset_x.to_css(dest)); + try!(dest.write_str(" ")); + try!(self.offset_y.to_css(dest)); + try!(dest.write_str(" ")); + try!(self.blur_radius.to_css(dest)); + + if let Some(ref color) = self.color { + try!(dest.write_str(" ")); + try!(color.to_css(dest)); + } + Ok(()) + } + } + + #[inline] + pub fn get_initial_value() -> computed_value::T { + computed_value::T(Vec::new()) + } + + pub fn parse(_: &ParserContext, input: &mut Parser) -> Result<SpecifiedValue,()> { + if input.try(|input| input.expect_ident_matching("none")).is_ok() { + Ok(SpecifiedValue(Vec::new())) + } else { + input.parse_comma_separated(parse_one_text_shadow).map(|shadows| { + SpecifiedValue(shadows) + }) + } + } + + fn parse_one_text_shadow(input: &mut Parser) -> Result<SpecifiedTextShadow,()> { + use util::geometry::Au; + let mut lengths = [specified::Length::Au(Au(0)); 3]; + let mut lengths_parsed = false; + let mut color = None; + + loop { + if !lengths_parsed { + if let Ok(value) = input.try(specified::Length::parse) { + lengths[0] = value; + let mut length_parsed_count = 1; + while length_parsed_count < 3 { + if let Ok(value) = input.try(specified::Length::parse) { + lengths[length_parsed_count] = value + } else { + break + } + length_parsed_count += 1; + } + + // The first two lengths must be specified. + if length_parsed_count < 2 { + return Err(()) + } + + lengths_parsed = true; + continue + } + } + if color.is_none() { + if let Ok(value) = input.try(specified::CSSColor::parse) { + color = Some(value); + continue + } + } + break + } + + // Lengths must be specified. + if !lengths_parsed { + return Err(()) + } + + Ok(SpecifiedTextShadow { + offset_x: lengths[0], + offset_y: lengths[1], + blur_radius: lengths[2], + color: color, + }) + } + + impl ToComputedValue for SpecifiedValue { + type ComputedValue = computed_value::T; + + fn to_computed_value(&self, context: &computed::Context) -> computed_value::T { + computed_value::T(self.0.iter().map(|value| { + computed_value::TextShadow { + offset_x: value.offset_x.to_computed_value(context), + offset_y: value.offset_y.to_computed_value(context), + blur_radius: value.blur_radius.to_computed_value(context), + color: value.color + .as_ref() + .map(|color| color.parsed) + .unwrap_or(cssparser::Color::CurrentColor), + } + }).collect()) + } + } + </%self:longhand> <%self:longhand name="filter"> use values::specified::Angle; pub use self::computed_value::T as SpecifiedValue; diff --git a/ports/cef/Cargo.lock b/ports/cef/Cargo.lock index 926815dba89..c67a240d535 100644 --- a/ports/cef/Cargo.lock +++ b/ports/cef/Cargo.lock @@ -295,7 +295,7 @@ dependencies = [ [[package]] name = "geom" version = "0.1.0" -source = "git+https://github.com/servo/rust-geom#6b079ba2738ed15bac2b6ec66850494afb9f2b4c" +source = "git+https://github.com/servo/rust-geom#876c2fceee211130d1294eacdc1bd8742c52540e" dependencies = [ "rustc-serialize 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)", ] diff --git a/ports/gonk/Cargo.lock b/ports/gonk/Cargo.lock index 8a8723fc71c..e7c1f955094 100644 --- a/ports/gonk/Cargo.lock +++ b/ports/gonk/Cargo.lock @@ -257,7 +257,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "geom" version = "0.1.0" -source = "git+https://github.com/servo/rust-geom#6b079ba2738ed15bac2b6ec66850494afb9f2b4c" +source = "git+https://github.com/servo/rust-geom#876c2fceee211130d1294eacdc1bd8742c52540e" dependencies = [ "rustc-serialize 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)", ] diff --git a/tests/ref/basic.list b/tests/ref/basic.list index 9e9b8c139fa..306a66950a8 100644 --- a/tests/ref/basic.list +++ b/tests/ref/basic.list @@ -254,3 +254,6 @@ fragment=top != ../html/acid2.html acid2_ref.html == canvas_lineto_a.html canvas_lineto_ref.html != text_decoration_smoke_a.html text_decoration_smoke_ref.html == hide_after_create.html hide_after_create_ref.html +== text_shadow_simple_a.html text_shadow_simple_ref.html +== text_shadow_decorations_a.html text_shadow_decorations_ref.html +== text_shadow_blur_a.html text_shadow_blur_ref.html diff --git a/tests/ref/text_shadow_blur_a.html b/tests/ref/text_shadow_blur_a.html new file mode 100644 index 00000000000..5846bf0a221 --- /dev/null +++ b/tests/ref/text_shadow_blur_a.html @@ -0,0 +1,15 @@ +<!DOCTYPE html> +<html> +<head> +<!-- Tests that `text-shadow` mirrors `box-shadow` in the way that it blurs. --> +<link rel="stylesheet" type="text/css" href="css/ahem.css"> +<style> +html, body { + margin: 0; + color: red; + text-shadow: blue 10px 10px 5px; +} +</style> +</head> +<body>X</body> +</html> diff --git a/tests/ref/text_shadow_blur_ref.html b/tests/ref/text_shadow_blur_ref.html new file mode 100644 index 00000000000..b733b16ec18 --- /dev/null +++ b/tests/ref/text_shadow_blur_ref.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<html> +<head> +<!-- Tests that `text-shadow` mirrors `box-shadow` in the way that it blurs. --> +<style> +html, body { + margin: 0; +} +section { + background: red; + width: 100px; + height: 100px; + box-shadow: blue 10px 10px 5px; +} +</style> +</head> +<body><section></section></body> +</html> diff --git a/tests/ref/text_shadow_decorations_a.html b/tests/ref/text_shadow_decorations_a.html new file mode 100644 index 00000000000..e61b2d4725f --- /dev/null +++ b/tests/ref/text_shadow_decorations_a.html @@ -0,0 +1,22 @@ +<!DOCTYPE html> +<html> +<head> +<!-- Tests that `text-shadow` shadows decorations. --> +<style> +#a { + text-decoration: underline; + text-shadow: 6px 6px black; + color: red; + font-size: 96px; + position: absolute; + top: 0; + left: 0; + width: 300px; +} +</style> +</head> +<body> +<div id=a>Foo</div> +</body> +</html> + diff --git a/tests/ref/text_shadow_decorations_ref.html b/tests/ref/text_shadow_decorations_ref.html new file mode 100644 index 00000000000..00cd8dd3e35 --- /dev/null +++ b/tests/ref/text_shadow_decorations_ref.html @@ -0,0 +1,30 @@ +<!DOCTYPE html> +<html> +<head> +<!-- Tests that `text-shadow` shadows decorations. --> +<style> +section { + text-decoration: underline; + font-size: 96px; + position: absolute; + width: 300px; +} +#a { + color: red; + top: 0; + left: 0; +} +#b { + color: black; + top: 6px; + left: 6px; +} +</style> +</head> +<body> +<section id=b>Foo</section> +<section id=a>Foo</section> +</body> +</html> + + diff --git a/tests/ref/text_shadow_multiple_shadows_a.html b/tests/ref/text_shadow_multiple_shadows_a.html new file mode 100644 index 00000000000..40b1dfbe799 --- /dev/null +++ b/tests/ref/text_shadow_multiple_shadows_a.html @@ -0,0 +1,21 @@ +<!DOCTYPE html> +<html> +<head> +<!-- Tests that multiple `text-shadow`s paint in the right order. --> +<style> +#a { + text-shadow: 6px 6px black, 12px 12px blue; + color: red; + font-size: 96px; + position: absolute; + top: 100px; + left: 100px; + width: 300px; +} +</style> +</head> +<body> +<div id=a>Foo</div> +</body> +</html> + diff --git a/tests/ref/text_shadow_multiple_shadows_ref.html b/tests/ref/text_shadow_multiple_shadows_ref.html new file mode 100644 index 00000000000..6a33debae4b --- /dev/null +++ b/tests/ref/text_shadow_multiple_shadows_ref.html @@ -0,0 +1,34 @@ +<!DOCTYPE html> +<html> +<head> +<!-- Tests that multiple `text-shadow`s paint in the right order. --> +<style> +section { + position: absolute; + width: 300px; + font-size: 96px; +} +#a { + top: 100px; + left: 100px; + color: red; +} +#b { + top: 106px; + left: 106px; + color: black; +} +#c { + top: 112px; + left: 112px; + color: blue; +} +</style> +</head> +<body> +<section id=c>Foo</section> +<section id=b>Foo</section> +<section id=a>Foo</section> +</body> +</html> + diff --git a/tests/ref/text_shadow_simple_a.html b/tests/ref/text_shadow_simple_a.html new file mode 100644 index 00000000000..5e177238bda --- /dev/null +++ b/tests/ref/text_shadow_simple_a.html @@ -0,0 +1,22 @@ +<!DOCTYPE html> +<html> +<head> +<!-- + Tests that `text-shadow` works with multiple unblurred shadows in the right order with the + right offsets. +--> +<link rel="stylesheet" type="text/css" href="css/ahem.css"> +<style> +section { + position: absolute; + top: 0; + left: 0; + width: 500px; + height: 500px; + color: green; + text-shadow: 20px 10px blue, 30px 40px red; +} +</style> +</head> +<body><section>X</section></body> +</html> diff --git a/tests/ref/text_shadow_simple_ref.html b/tests/ref/text_shadow_simple_ref.html new file mode 100644 index 00000000000..34c9b897b98 --- /dev/null +++ b/tests/ref/text_shadow_simple_ref.html @@ -0,0 +1,35 @@ +<!DOCTYPE html> +<html> +<head> +<!-- + Tests that `text-shadow` works with multiple unblurred shadows in the right order with the + right offsets. +--> +<style> +html, body { + margin: 0; +} +div { + width: 100px; + height: 100px; + position: absolute; +} +#c { + background: green; + left: 0; + top: 0; +} +#b { + background: blue; + left: 20px; + top: 10px; +} +#a { + background: red; + left: 30px; + top: 40px; +} +</style> +</head> +<body><div id=a></div><div id=b></div><div id=c></div></body> +</html> |