From 463b80090ce20a8c3cb79e43281c8925c2e05f39 Mon Sep 17 00:00:00 2001 From: Patrick Walton Date: Mon, 12 Nov 2012 12:08:38 -0800 Subject: [PATCH] Remove the servo-gfx submodule --- .gitmodules | 3 - Makefile.in | 21 +- configure | 3 +- mk/sub.mk | 18 +- src/servo-gfx | 1 - src/servo-gfx-2/JosefinSans-SemiBold.ttf | Bin 0 -> 33004 bytes src/servo-gfx-2/OFL.txt | 93 ++ src/servo-gfx-2/color.rs | 18 + src/servo-gfx-2/compositor.rs | 28 + src/servo-gfx-2/display_list.rs | 117 ++ src/servo-gfx-2/font.rs | 499 ++++++++ src/servo-gfx-2/font_context.rs | 124 ++ src/servo-gfx-2/font_list.rs | 137 ++ src/servo-gfx-2/fontconfig/font_list.rs | 12 + src/servo-gfx-2/freetype/font.rs | 151 +++ src/servo-gfx-2/freetype/font_context.rs | 33 + src/servo-gfx-2/geometry.rs | 109 ++ src/servo-gfx-2/image/base.rs | 43 + src/servo-gfx-2/image/encode/tga.rs | 23 + src/servo-gfx-2/image/holder.rs | 102 ++ src/servo-gfx-2/image/test.jpeg | Bin 0 -> 4962 bytes src/servo-gfx-2/native.rs | 9 + src/servo-gfx-2/opts.rs | 77 ++ src/servo-gfx-2/quartz/font.rs | 161 +++ src/servo-gfx-2/quartz/font_context.rs | 12 + src/servo-gfx-2/quartz/font_list.rs | 56 + src/servo-gfx-2/render_context.rs | 105 ++ src/servo-gfx-2/render_layers.rs | 137 ++ src/servo-gfx-2/render_task.rs | 157 +++ src/servo-gfx-2/resource/file_loader.rs | 29 + src/servo-gfx-2/resource/http_loader.rs | 38 + src/servo-gfx-2/resource/image_cache_task.rs | 1101 +++++++++++++++++ src/servo-gfx-2/resource/local_image_cache.rs | 150 +++ src/servo-gfx-2/resource/resource_task.rs | 157 +++ src/servo-gfx-2/servo_gfx.rc | 93 ++ src/servo-gfx-2/servo_gfx.rs | 5 + src/servo-gfx-2/surface.rs | 40 + src/servo-gfx-2/text.rs | 9 + src/servo-gfx-2/text/glyph.rs | 624 ++++++++++ src/servo-gfx-2/text/harfbuzz/shaper.rs | 177 +++ src/servo-gfx-2/text/shaper.rs | 17 + src/servo-gfx-2/text/text_run.rs | 203 +++ src/servo-gfx-2/text/util.rs | 223 ++++ src/servo-gfx-2/util/cache.rs | 59 + src/servo-gfx-2/util/range.rs | 164 +++ src/servo-gfx-2/util/time.rs | 15 + src/servo-gfx-2/util/url.rs | 107 ++ src/servo-gfx-2/util/vec.rs | 102 ++ 48 files changed, 5536 insertions(+), 26 deletions(-) delete mode 160000 src/servo-gfx create mode 100644 src/servo-gfx-2/JosefinSans-SemiBold.ttf create mode 100644 src/servo-gfx-2/OFL.txt create mode 100644 src/servo-gfx-2/color.rs create mode 100644 src/servo-gfx-2/compositor.rs create mode 100644 src/servo-gfx-2/display_list.rs create mode 100644 src/servo-gfx-2/font.rs create mode 100644 src/servo-gfx-2/font_context.rs create mode 100644 src/servo-gfx-2/font_list.rs create mode 100644 src/servo-gfx-2/fontconfig/font_list.rs create mode 100644 src/servo-gfx-2/freetype/font.rs create mode 100644 src/servo-gfx-2/freetype/font_context.rs create mode 100644 src/servo-gfx-2/geometry.rs create mode 100644 src/servo-gfx-2/image/base.rs create mode 100644 src/servo-gfx-2/image/encode/tga.rs create mode 100644 src/servo-gfx-2/image/holder.rs create mode 100644 src/servo-gfx-2/image/test.jpeg create mode 100644 src/servo-gfx-2/native.rs create mode 100644 src/servo-gfx-2/opts.rs create mode 100644 src/servo-gfx-2/quartz/font.rs create mode 100644 src/servo-gfx-2/quartz/font_context.rs create mode 100644 src/servo-gfx-2/quartz/font_list.rs create mode 100644 src/servo-gfx-2/render_context.rs create mode 100644 src/servo-gfx-2/render_layers.rs create mode 100644 src/servo-gfx-2/render_task.rs create mode 100644 src/servo-gfx-2/resource/file_loader.rs create mode 100644 src/servo-gfx-2/resource/http_loader.rs create mode 100644 src/servo-gfx-2/resource/image_cache_task.rs create mode 100644 src/servo-gfx-2/resource/local_image_cache.rs create mode 100644 src/servo-gfx-2/resource/resource_task.rs create mode 100644 src/servo-gfx-2/servo_gfx.rc create mode 100644 src/servo-gfx-2/servo_gfx.rs create mode 100644 src/servo-gfx-2/surface.rs create mode 100644 src/servo-gfx-2/text.rs create mode 100644 src/servo-gfx-2/text/glyph.rs create mode 100644 src/servo-gfx-2/text/harfbuzz/shaper.rs create mode 100644 src/servo-gfx-2/text/shaper.rs create mode 100644 src/servo-gfx-2/text/text_run.rs create mode 100644 src/servo-gfx-2/text/util.rs create mode 100644 src/servo-gfx-2/util/cache.rs create mode 100644 src/servo-gfx-2/util/range.rs create mode 100644 src/servo-gfx-2/util/time.rs create mode 100644 src/servo-gfx-2/util/url.rs create mode 100644 src/servo-gfx-2/util/vec.rs diff --git a/.gitmodules b/.gitmodules index 9bb7b40de34..ffce2ddf38e 100644 --- a/.gitmodules +++ b/.gitmodules @@ -85,6 +85,3 @@ [submodule "src/skia"] path = src/skia url = git://github.com/mozilla-servo/skia.git -[submodule "src/servo-gfx"] - path = src/servo-gfx - url = git://github.com/mozilla-servo/servo-gfx.git diff --git a/Makefile.in b/Makefile.in index 1914ce340ac..f7d5fa4c16d 100644 --- a/Makefile.in +++ b/Makefile.in @@ -130,12 +130,20 @@ endef $(foreach submodule,$(CFG_SUBMODULES),\ $(eval $(call DEF_SUBMODULE_RULES,$(submodule)))) -RFLAGS_servo = $(strip $(CFG_RUSTC_FLAGS)) $(addprefix -L $(B)src/,$(DEPS_SUBMODULES)) -SRC_servo = $(call rwildcard,$(S)src/servo/,*.rs) -CRATE_servo = $(S)src/servo/servo.rc DONE_SUBMODULES = $(foreach dep,$(DEPS_SUBMODULES),$(DONE_$(dep))) -DEPS_servo = $(CRATE_servo) $(SRC_servo) $(DONE_SUBMODULES) +RFLAGS_servo_gfx = $(strip $(CFG_RUSTC_FLAGS)) $(addprefix -L $(B)src/,$(DEPS_SUBMODULES)) +SRC_servo_gfx = $(call rwildcard,$(S)src/servo-gfx-2/,*.rs) +CRATE_servo_gfx = $(S)src/servo-gfx-2/servo_gfx.rc +DONE_servo_gfx = $(B)src/servo-gfx-2/libservogfx.dummy + +DEPS_servo_gfx = $(CRATE_servo_gfx) $(SRC_servo_gfx) $(DONE_SUBMODULES) + +RFLAGS_servo = $(strip $(CFG_RUSTC_FLAGS)) $(addprefix -L $(B)src/,$(DEPS_SUBMODULES)) -L $(B)src/servo-gfx-2 +SRC_servo = $(call rwildcard,$(S)src/servo/,*.rs) +CRATE_servo = $(S)src/servo/servo.rc + +DEPS_servo = $(CRATE_servo) $(SRC_servo) $(DONE_SUBMODULES) $(DONE_servo_gfx) # rules that depend on having correct meta-target vars (DEPS_CLEAN, DEPS_servo, etc) include $(S)mk/check.mk @@ -145,6 +153,11 @@ include $(S)mk/clean.mk .PHONY: all all: servo package +# Servo helper libraries + +$(DONE_servo_gfx): $(DEPS_servo_gfx) + $(RUSTC) $(RFLAGS_servo_gfx) -o $@ $< && touch $@ + # Servo binaries servo: $(DEPS_servo) diff --git a/configure b/configure index d5c478f8788..c11398e0f4d 100755 --- a/configure +++ b/configure @@ -344,7 +344,7 @@ step_msg "running submodule autoconf scripts" (cd ${CFG_SRC_DIR}src/mozjs/js/src && "${CFG_AUTOCONF213}") || exit $? -CFG_SUBMODULES="libwapcaplet rust-wapcaplet rust-harfbuzz rust-opengles skia rust-azure rust-cairo rust-stb-image rust-geom rust-glut rust-layers rust-http-client libparserutils libhubbub libcss rust-netsurfcss rust-css rust-hubbub sharegl rust-mozjs mozjs servo-gfx" +CFG_SUBMODULES="libwapcaplet rust-wapcaplet rust-harfbuzz rust-opengles skia rust-azure rust-cairo rust-stb-image rust-geom rust-glut rust-layers rust-http-client libparserutils libhubbub libcss rust-netsurfcss rust-css rust-hubbub sharegl rust-mozjs mozjs" if [ $CFG_OSTYPE = "darwin" ] then @@ -365,6 +365,7 @@ do make_dir ${CFG_BUILD_DIR}src/${i} done +make_dir ${CFG_BUILD_DIR}src/servo-gfx-2 make_dir src/test/ref # TODO: don't run configure on submodules unless necessary. For an example, diff --git a/mk/sub.mk b/mk/sub.mk index a47519a7dfb..b2ff78d9bf7 100644 --- a/mk/sub.mk +++ b/mk/sub.mk @@ -81,16 +81,6 @@ DEPS_libcss += \ libparserutils \ $(NULL) -DEPS_servo-gfx += \ - rust-azure \ - rust-cairo \ - rust-freetype \ - rust-geom \ - rust-harfbuzz \ - rust-http-client \ - rust-stb-image \ - $(NULL) - # Platform-specific dependencies ifeq ($(CFG_OSTYPE),darwin) DEPS_rust-azure += \ @@ -105,7 +95,7 @@ DEPS_rust-cairo += \ rust-core-graphics \ $(NULL) -DEPS_rust-io-surface += \ +dEPS_rust-io-surface += \ rust-core-foundation \ $(NULL) @@ -129,12 +119,6 @@ DEPS_rust-layers += \ rust-core-text \ $(NULL) -DEPS_servo-gfx += \ - rust-core-foundation \ - rust-core-graphics \ - rust-core-text \ - $(NULL) - endif ifeq ($(CFG_OSTYPE),linux) diff --git a/src/servo-gfx b/src/servo-gfx deleted file mode 160000 index 08d11a2db06..00000000000 --- a/src/servo-gfx +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 08d11a2db06e733ce9c84524819d37486a26c160 diff --git a/src/servo-gfx-2/JosefinSans-SemiBold.ttf b/src/servo-gfx-2/JosefinSans-SemiBold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..f58d378543eca14f50144728ea8eb183c1dca1e0 GIT binary patch literal 33004 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 literal 0 HcmV?d00001 diff --git a/src/servo-gfx-2/OFL.txt b/src/servo-gfx-2/OFL.txt new file mode 100644 index 00000000000..d0a273c523b --- /dev/null +++ b/src/servo-gfx-2/OFL.txt @@ -0,0 +1,93 @@ +Copyright (c) 2009, 2010, 2011 Daniel Johnson (). + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/src/servo-gfx-2/color.rs b/src/servo-gfx-2/color.rs new file mode 100644 index 00000000000..8763b9ee7a7 --- /dev/null +++ b/src/servo-gfx-2/color.rs @@ -0,0 +1,18 @@ +use azure::AzFloat; +use AzColor = azure::azure_hl::Color; + +pub type Color = AzColor; + +pub fn rgb(r: u8, g: u8, b: u8) -> AzColor { + rgba(r, g, b, 1.0) +} + +pub fn rgba(r: u8, g: u8, b: u8, a: float) -> AzColor { + AzColor { + r: (r as AzFloat) / (255.0 as AzFloat), + g: (g as AzFloat) / (255.0 as AzFloat), + b: (b as AzFloat) / (255.0 as AzFloat), + a: a as AzFloat + } +} + diff --git a/src/servo-gfx-2/compositor.rs b/src/servo-gfx-2/compositor.rs new file mode 100644 index 00000000000..b419bebd0d7 --- /dev/null +++ b/src/servo-gfx-2/compositor.rs @@ -0,0 +1,28 @@ +use azure::azure_hl::{DrawTarget}; +use geom::rect::Rect; + +struct LayerBuffer { + draw_target: DrawTarget, + + // The rect in the containing RenderLayer that this represents. + rect: Rect, + + // NB: stride is in pixels, like OpenGL GL_UNPACK_ROW_LENGTH. + stride: uint +} + +/// A set of layer buffers. This is an atomic unit used to switch between the front and back +/// buffers. +struct LayerBufferSet { + buffers: ~[LayerBuffer] +} + +/** +The interface used to by the renderer to aquire draw targets for +each rendered frame and submit them to be drawn to the display +*/ +trait Compositor { + fn begin_drawing(next_dt: pipes::Chan); + fn draw(next_dt: pipes::Chan, +draw_me: LayerBufferSet); +} + diff --git a/src/servo-gfx-2/display_list.rs b/src/servo-gfx-2/display_list.rs new file mode 100644 index 00000000000..bb0d17685e0 --- /dev/null +++ b/src/servo-gfx-2/display_list.rs @@ -0,0 +1,117 @@ +use color::{Color, rgb}; +use geometry::Au; +use image::base::Image; +use render_context::RenderContext; +use text::SendableTextRun; +use util::range::Range; + +use azure::azure_hl::DrawTarget; +use core::dvec::DVec; +use clone_arc = std::arc::clone; +use geom::Rect; +use geom::Point2D; +use std::arc::ARC; + +struct DisplayItemData { + bounds : Rect, // TODO: whose coordinate system should this use? +} + +impl DisplayItemData { + static pure fn new(bounds: &Rect) -> DisplayItemData { + DisplayItemData { bounds: copy *bounds } + } +} + +pub enum DisplayItem { + SolidColor(DisplayItemData, Color), + // TODO: need to provide spacing data for text run. + // (i.e, to support rendering of CSS 'word-spacing' and 'letter-spacing') + // TODO: don't copy text runs, ever. + Text(DisplayItemData, ~SendableTextRun, Range, Color), + Image(DisplayItemData, ARC<~image::base::Image>), + Border(DisplayItemData, Au, Color) +} + +impl DisplayItem { + pure fn d(&self) -> &self/DisplayItemData { + match *self { + SolidColor(ref d, _) => d, + Text(ref d, _, _, _) => d, + Image(ref d, _) => d, + Border(ref d, _, _) => d + } + } + + fn draw_into_context(&self, ctx: &RenderContext) { + match *self { + SolidColor(_, color) => ctx.draw_solid_color(&self.d().bounds, color), + Text(_, run, range, color) => { + let new_run = @run.deserialize(ctx.font_ctx); + let font = new_run.font; + let origin = self.d().bounds.origin; + let baseline_origin = Point2D(origin.x, origin.y + font.metrics.ascent); + font.draw_text_into_context(ctx, new_run, range, baseline_origin, color); + }, + Image(_, ref img) => ctx.draw_image(self.d().bounds, clone_arc(img)), + Border(_, width, color) => ctx.draw_border(&self.d().bounds, width, color), + } + + debug!("%?", { + ctx.draw_border(&self.d().bounds, Au::from_px(1), rgb(150, 150, 150)); + () }); + } + + static pure fn new_SolidColor(bounds: &Rect, color: Color) -> DisplayItem { + SolidColor(DisplayItemData::new(bounds), color) + } + + static pure fn new_Border(bounds: &Rect, width: Au, color: Color) -> DisplayItem { + Border(DisplayItemData::new(bounds), width, color) + } + + static pure fn new_Text(bounds: &Rect, + run: ~SendableTextRun, + range: Range, + color: Color) -> DisplayItem { + Text(DisplayItemData::new(bounds), move run, range, color) + } + + // ARC should be cloned into ImageData, but Images are not sendable + static pure fn new_Image(bounds: &Rect, image: ARC<~image::base::Image>) -> DisplayItem { + Image(DisplayItemData::new(bounds), move image) + } +} + +// Dual-mode/freezable. +pub struct DisplayList { + list: ~[~DisplayItem] +} + +trait DisplayListMethods { + fn append_item(&mut self, item: ~DisplayItem); + fn draw_into_context(ctx: &RenderContext); +} + +impl DisplayList { + static fn new() -> DisplayList { + DisplayList { list: ~[] } + } +} + +impl DisplayList : DisplayListMethods { + fn append_item(&mut self, item: ~DisplayItem) { + // FIXME(Issue #150): crashes + //debug!("Adding display item %u: %?", self.len(), item); + self.list.push(move item); + } + + fn draw_into_context(ctx: &RenderContext) { + debug!("beginning display list"); + for self.list.each |item| { + // FIXME(Issue #150): crashes + //debug!("drawing %?", *item); + item.draw_into_context(ctx); + } + debug!("ending display list"); + } +} diff --git a/src/servo-gfx-2/font.rs b/src/servo-gfx-2/font.rs new file mode 100644 index 00000000000..29abc435d66 --- /dev/null +++ b/src/servo-gfx-2/font.rs @@ -0,0 +1,499 @@ +use color::Color; +use geometry::Au; +use render_context::RenderContext; +use util::range::Range; +use text::glyph::{GlyphStore, GlyphIndex}; +use text::{Shaper, TextRun}; + +use azure::{AzFloat, AzScaledFontRef}; +use azure::azure_hl::{BackendType, ColorPattern}; +use core::dvec::DVec; +use geom::{Point2D, Rect, Size2D}; + +// FontHandle encapsulates access to the platform's font API, +// e.g. quartz, FreeType. It provides access to metrics and tables +// needed by the text shaper as well as access to the underlying font +// resources needed by the graphics layer to draw glyphs. + +#[cfg(target_os = "macos")] +pub type FontHandle/& = quartz::font::QuartzFontHandle; + +#[cfg(target_os = "linux")] +pub type FontHandle/& = freetype::font::FreeTypeFontHandle; + +pub trait FontHandleMethods { + pure fn face_name() -> ~str; + pure fn is_italic() -> bool; + pure fn boldness() -> CSSFontWeight; + + fn glyph_index(codepoint: char) -> Option; + fn glyph_h_advance(GlyphIndex) -> Option; + fn get_metrics() -> FontMetrics; +} + +// TODO: `new` should be part of trait FontHandleMethods + +// TODO(Issue #163): this is a workaround for static methods and +// typedefs not working well together. It should be removed. + +impl FontHandle { + #[cfg(target_os = "macos")] + static pub fn new(fctx: &native::FontContextHandle, buf: @~[u8], pt_size: float) -> Result { + quartz::font::QuartzFontHandle::new_from_buffer(fctx, buf, pt_size) + } + + #[cfg(target_os = "linux")] + static pub fn new(fctx: &native::FontContextHandle, buf: @~[u8], pt_size: float) -> Result { + freetype::font::FreeTypeFontHandle::new(fctx, buf, pt_size) + } +} + +// Used to abstract over the shaper's choice of fixed int representation. +type FractionalPixel = float; + +struct FontMetrics { + underline_size: Au, + underline_offset: Au, + leading: Au, + x_height: Au, + em_size: Au, + ascent: Au, + descent: Au, + max_advance: Au +} + +// TODO: use enum from CSS bindings +enum CSSFontWeight { + FontWeight100, + FontWeight200, + FontWeight300, + FontWeight400, + FontWeight500, + FontWeight600, + FontWeight700, + FontWeight800, + FontWeight900, +} +pub impl CSSFontWeight : cmp::Eq; + +pub impl CSSFontWeight { + pub pure fn is_bold() -> bool { + match self { + FontWeight900 | FontWeight800 | FontWeight700 | FontWeight600 => true, + _ => false + } + } +} + +// TODO: eventually this will be split into the specified and used +// font styles. specified contains uninterpreted CSS font property +// values, while 'used' is attached to gfx::Font to descript the +// instance's properties. +// +// For now, the cases are differentiated with a typedef +pub struct FontStyle { + pt_size: float, + weight: CSSFontWeight, + italic: bool, + oblique: bool, + families: ~str, + // TODO: font-stretch, text-decoration, font-variant, size-adjust +} + +// TODO(Issue #181): use deriving for trivial cmp::Eq implementations +pub impl FontStyle : cmp::Eq { + pure fn eq(other: &FontStyle) -> bool { + use std::cmp::FuzzyEq; + + self.pt_size.fuzzy_eq(&other.pt_size) && + self.weight == other.weight && + self.italic == other.italic && + self.oblique == other.oblique && + self.families == other.families + } + pure fn ne(other: &FontStyle) -> bool { !self.eq(other) } +} + +pub type SpecifiedFontStyle = FontStyle; +pub type UsedFontStyle = FontStyle; + +// TODO: move me to layout +struct ResolvedFont { + group: @FontGroup, + style: SpecifiedFontStyle, +} + +// FontDescriptor serializes a specific font and used font style +// options, such as point size. + +// It's used to swizzle/unswizzle gfx::Font instances when +// communicating across tasks, such as the display list between layout +// and render tasks. +pub struct FontDescriptor { + style: UsedFontStyle, + selector: FontSelector, +} + + +// TODO(Issue #181): use deriving for trivial cmp::Eq implementations +pub impl FontDescriptor : cmp::Eq { + pure fn eq(other: &FontDescriptor) -> bool { + self.style == other.style && + self.selector == other.selector + } + pure fn ne(other: &FontDescriptor) -> bool { !self.eq(other) } +} + +pub impl FontDescriptor { + static pure fn new(style: &UsedFontStyle, selector: &FontSelector) -> FontDescriptor { + FontDescriptor { + style: copy *style, + selector: copy *selector, + } + } +} + +// A FontSelector is a platform-specific strategy for serializing face names. +pub enum FontSelector { + SelectorPlatformName(~str), + SelectorStubDummy, // aka, use Josephin Sans +} + +// TODO(Issue #181): use deriving for trivial cmp::Eq implementations +pub impl FontSelector : cmp::Eq { + pure fn eq(other: &FontSelector) -> bool { + match (&self, other) { + (&SelectorStubDummy, &SelectorStubDummy) => true, + (&SelectorPlatformName(a), &SelectorPlatformName(b)) => a == b, + _ => false + } + } + pure fn ne(other: &FontSelector) -> bool { !self.eq(other) } +} + +// This struct is the result of mapping a specified FontStyle into the +// available fonts on the system. It contains an ordered list of font +// instances to be used in case the prior font cannot be used for +// rendering the specified language. + +// The ordering of font instances is mainly decided by the CSS +// 'font-family' property. The last font is a system fallback font. +pub struct FontGroup { + families: @str, + // style of the first western font in group, which is + // used for purposes of calculating text run metrics. + style: UsedFontStyle, + fonts: ~[@Font], +} + +pub impl FontGroup { + static fn new(families: @str, style: &UsedFontStyle, fonts: ~[@Font]) -> FontGroup { + FontGroup { + families: families, + style: copy *style, + fonts: move fonts, + } + } +} + +struct RunMetrics { + // may be negative due to negative width (i.e., kerning of '.' in 'P.T.') + advance_width: Au, + ascent: Au, // nonzero + descent: Au, // nonzero + // this bounding box is relative to the left origin baseline. + // so, bounding_box.position.y = -ascent + bounding_box: Rect +} + +/** +A font instance. Layout can use this to calculate glyph metrics +and the renderer can use it to render text. +*/ +pub struct Font { + priv fontbuf: @~[u8], + priv handle: FontHandle, + priv mut azure_font: Option, + priv mut shaper: Option<@Shaper>, + style: UsedFontStyle, + metrics: FontMetrics, + backend: BackendType, + + drop { + use azure::bindgen::AzReleaseScaledFont; + do (copy self.azure_font).iter |fontref| { AzReleaseScaledFont(*fontref); } + } +} + +impl Font { + // TODO: who should own fontbuf? + static fn new(fontbuf: @~[u8], + handle: FontHandle, + style: UsedFontStyle, + backend: BackendType) -> Font { + let metrics = handle.get_metrics(); + + Font { + fontbuf : fontbuf, + handle : move handle, + azure_font: None, + shaper: None, + style: move style, + metrics: move metrics, + backend: backend + } + } + + priv fn get_shaper(@self) -> @Shaper { + // fast path: already created a shaper + match self.shaper { + Some(shaper) => { return shaper; }, + None => {} + } + + let shaper = @Shaper::new(self); + self.shaper = Some(shaper); + shaper + } + + priv fn get_azure_font() -> AzScaledFontRef { + // fast path: we've already created the azure font resource + match self.azure_font { + Some(azfont) => return azfont, + None => {} + } + + let ct_font = &self.handle.ctfont; + let size = self.style.pt_size as AzFloat; + let scaled_font = azure::scaled_font::ScaledFont::new(self.backend, ct_font, size); + + let azure_scaled_font; + unsafe { + azure_scaled_font = scaled_font.azure_scaled_font; + cast::forget(move scaled_font); + } + + self.azure_font = Some(azure_scaled_font); + azure_scaled_font + /* + // TODO: these cairo-related things should be in rust-cairo. + // creating a cairo font/face from a native font resource + // should be part of the NativeFont API, not exposed here. + #[cfg(target_os = "linux")] + fn get_cairo_face(font: &Font) -> *cairo_font_face_t { + use cairo::cairo_ft::bindgen::{cairo_ft_font_face_create_for_ft_face}; + + let ftface = font.handle.face; + let cface = cairo_ft_font_face_create_for_ft_face(ftface, 0 as c_int); + // FIXME: error handling + return cface; + } + */ + } +} + +// Public API +pub trait FontMethods { + fn draw_text_into_context(rctx: &RenderContext, + run: &TextRun, + range: Range, + baseline_origin: Point2D, + color: Color); + fn measure_text(&TextRun, Range) -> RunMetrics; + fn shape_text(@self, &str) -> GlyphStore; + fn get_descriptor() -> FontDescriptor; + + fn buf(&self) -> @~[u8]; + // these are used to get glyphs and advances in the case that the + // shaper can't figure it out. + fn glyph_index(char) -> Option; + fn glyph_h_advance(GlyphIndex) -> FractionalPixel; +} + +pub impl Font : FontMethods { + fn draw_text_into_context(rctx: &RenderContext, + run: &TextRun, + range: Range, + baseline_origin: Point2D, + color: Color) { + use libc::types::common::c99::{uint16_t, uint32_t}; + use azure::{AzDrawOptions, + AzGlyph, + AzGlyphBuffer}; + use azure::bindgen::{AzCreateColorPattern, + AzDrawTargetFillGlyphs, + AzReleaseColorPattern}; + + let target = rctx.get_draw_target(); + let azfont = self.get_azure_font(); + let pattern = ColorPattern(color); + let azure_pattern = pattern.azure_color_pattern; + assert azure_pattern.is_not_null(); + + let options: AzDrawOptions = { + mAlpha: 1f as AzFloat, + fields: 0x0200 as uint16_t + }; + + let mut origin = copy baseline_origin; + let azglyphs = DVec(); + azglyphs.reserve(range.length()); + + for run.glyphs.iter_glyphs_for_range(range) |_i, glyph| { + let glyph_advance = glyph.advance(); + let glyph_offset = glyph.offset().get_default(Au::zero_point()); + + let azglyph: AzGlyph = { + mIndex: glyph.index() as uint32_t, + mPosition: { + x: (origin.x + glyph_offset.x).to_px() as AzFloat, + y: (origin.y + glyph_offset.y).to_px() as AzFloat + } + }; + origin = Point2D(origin.x + glyph_advance, origin.y); + azglyphs.push(move azglyph) + }; + + let azglyph_buf_len = azglyphs.len(); + let azglyph_buf = dvec::unwrap(move azglyphs); + let glyphbuf: AzGlyphBuffer = unsafe {{ + mGlyphs: vec::raw::to_ptr(azglyph_buf), + mNumGlyphs: azglyph_buf_len as uint32_t + }}; + + // TODO: this call needs to move into azure_hl.rs + AzDrawTargetFillGlyphs(target.azure_draw_target, + azfont, + ptr::to_unsafe_ptr(&glyphbuf), + azure_pattern, + ptr::to_unsafe_ptr(&options), + ptr::null()); + } + + fn measure_text(run: &TextRun, range: Range) -> RunMetrics { + assert range.is_valid_for_string(run.text); + + debug!("measuring text range '%s'", run.text.substr(range.begin(), range.length())); + + // TODO: alter advance direction for RTL + // TODO(Issue #98): using inter-char and inter-word spacing settings when measuring text + let mut advance = Au(0); + for run.glyphs.iter_glyphs_for_range(range) |_i, glyph| { + advance += glyph.advance(); + } + let mut bounds = Rect(Point2D(Au(0), -self.metrics.ascent), + Size2D(advance, self.metrics.ascent + self.metrics.descent)); + + // TODO(Issue #125): support loose and tight bounding boxes; using the + // ascent+descent and advance is sometimes too generous and + // looking at actual glyph extents can yield a tighter box. + + let metrics = RunMetrics { advance_width: advance, + bounding_box: bounds, + ascent: self.metrics.ascent, + descent: self.metrics.descent, + }; + debug!("Measured text range '%s' with metrics:", run.text.substr(range.begin(), range.length())); + debug!("%?", metrics); + + return metrics; + } + + fn shape_text(@self, text: &str) -> GlyphStore { + let store = GlyphStore(text.len()); + let shaper = self.get_shaper(); + shaper.shape_text(text, &store); + return move store; + } + + fn get_descriptor() -> FontDescriptor { + // TODO(Issue #174): implement by-platform-name FontSelectors, + // probably by adding such an API to FontHandle. + FontDescriptor::new(&font_context::dummy_style(), &SelectorStubDummy) + } + + fn buf(&self) -> @~[u8] { + self.fontbuf + } + + fn glyph_index(codepoint: char) -> Option { + self.handle.glyph_index(codepoint) + } + + fn glyph_h_advance(glyph: GlyphIndex) -> FractionalPixel { + match self.handle.glyph_h_advance(glyph) { + Some(adv) => adv, + None => /* FIXME: Need fallback strategy */ 10f as FractionalPixel + } + } +} + +/*fn should_destruct_on_fail_without_leaking() { + #[test]; + #[should_fail]; + + let fctx = @FontContext(); + let matcher = @FontMatcher(fctx); + let _font = matcher.get_test_font(); + fail; +} + +fn should_get_glyph_indexes() { + #[test]; + + let fctx = @FontContext(); + let matcher = @FontMatcher(fctx); + let font = matcher.get_test_font(); + let glyph_idx = font.glyph_index('w'); + assert glyph_idx == Some(40u as GlyphIndex); +} + +fn should_get_glyph_advance() { + #[test]; + #[ignore]; + + let fctx = @FontContext(); + let matcher = @FontMatcher(fctx); + let font = matcher.get_test_font(); + let x = font.glyph_h_advance(40u as GlyphIndex); + assert x == 15f || x == 16f; +} + +// Testing thread safety +fn should_get_glyph_advance_stress() { + #[test]; + #[ignore]; + + let mut ports = ~[]; + + for iter::repeat(100) { + let (chan, port) = pipes::stream(); + ports += [@move port]; + do task::spawn |move chan| { + let fctx = @FontContext(); + let matcher = @FontMatcher(fctx); + let _font = matcher.get_test_font(); + let x = font.glyph_h_advance(40u as GlyphIndex); + assert x == 15f || x == 16f; + chan.send(()); + } + } + + for ports.each |port| { + port.recv(); + } +} + +fn should_be_able_to_create_instances_in_multiple_threads() { + #[test]; + + for iter::repeat(10u) { + do task::spawn { + let fctx = @FontContext(); + let matcher = @FontMatcher(fctx); + let _font = matcher.get_test_font(); + } + } +} + +*/ diff --git a/src/servo-gfx-2/font_context.rs b/src/servo-gfx-2/font_context.rs new file mode 100644 index 00000000000..c6093d03871 --- /dev/null +++ b/src/servo-gfx-2/font_context.rs @@ -0,0 +1,124 @@ +use font::{Font, FontDescriptor, FontGroup, FontStyle, SelectorPlatformName, SelectorStubDummy}; +use font::{SpecifiedFontStyle}; +use font_list::FontList; +use native::FontHandle; +use util::cache; + +use azure::azure_hl::BackendType; +use core::dvec::DVec; + +// TODO(Issue #164): delete, and get default font from font list +const TEST_FONT: [u8 * 33004] = #include_bin("JosefinSans-SemiBold.ttf"); + +fn test_font_bin() -> ~[u8] { + return vec::from_fn(33004, |i| TEST_FONT[i]); +} + +// TODO(Rust #3934): creating lots of new dummy styles is a workaround +// for not being able to store symbolic enums in top-level constants. +pub fn dummy_style() -> FontStyle { + use font::FontWeight300; + return FontStyle { + pt_size: 20f, + weight: FontWeight300, + italic: false, + oblique: false, + families: ~"Helvetica, serif", + } +} + +// TODO(Issue #163): this is a workaround for static methods and +// typedefs not working well together. It should be removed. +#[cfg(target_os = "macos")] +type FontContextHandle/& = quartz::font_context::QuartzFontContextHandle; + +#[cfg(target_os = "linux")] +type FontContextHandle/& = freetype::font_context::FreeTypeFontContextHandle; + +pub impl FontContextHandle { + #[cfg(target_os = "macos")] + static pub fn new() -> FontContextHandle { + quartz::font_context::QuartzFontContextHandle::new() + } + + #[cfg(target_os = "linux")] + static pub fn new() -> FontContextHandle { + freetype::font_context::FreeTypeFontContextHandle::new() + } +} + +pub struct FontContext { + instance_cache: cache::MonoCache, + font_list: Option, // only needed by layout + handle: FontContextHandle, + backend: BackendType, +} + +pub impl FontContext { + static fn new(backend: BackendType, needs_font_list: bool) -> FontContext { + let handle = FontContextHandle::new(); + let font_list = if needs_font_list { Some(FontList::new(&handle)) } else { None }; + FontContext { + // TODO(Rust #3902): remove extraneous type parameters once they are inferred correctly. + instance_cache: cache::new::>(10), + font_list: move font_list, + handle: move handle, + backend: backend + } + } + + fn get_resolved_font_for_style(style: &SpecifiedFontStyle) -> @FontGroup { + // TODO(Issue #178, E): implement a cache of FontGroup instances. + self.create_font_group(style) + } + + fn get_font_by_descriptor(desc: &FontDescriptor) -> Result<@Font, ()> { + match self.instance_cache.find(desc) { + Some(f) => Ok(f), + None => { + let result = self.create_font_instance(desc); + match result { + Ok(font) => { + self.instance_cache.insert(desc, font); + }, _ => {} + }; + result + } + } + } + + priv fn create_font_group(style: &SpecifiedFontStyle) -> @FontGroup { + // TODO(Issue #178, D): implement private font matching + // TODO(Issue #174): implement by-platform-name FontSelectors + let desc = FontDescriptor::new(&font_context::dummy_style(), &SelectorStubDummy); + let fonts = DVec(); + match self.get_font_by_descriptor(&desc) { + Ok(instance) => fonts.push(instance), + Err(()) => {} + } + + assert fonts.len() > 0; + // TODO(Issue #179): Split FontStyle into specified and used styles + let used_style = copy *style; + + @FontGroup::new(style.families.to_managed(), &used_style, dvec::unwrap(move fonts)) + } + + priv fn create_font_instance(desc: &FontDescriptor) -> Result<@Font, ()> { + match desc.selector { + SelectorStubDummy => { + let font_bin = @test_font_bin(); + let handle = FontHandle::new(&self.handle, font_bin, desc.style.pt_size); + let handle = if handle.is_ok() { + result::unwrap(move handle) + } else { + return Err(handle.get_err()); + }; + + return Ok(@Font::new(font_bin, move handle, copy desc.style, self.backend)); + }, + // TODO(Issue #174): implement by-platform-name font selectors. + SelectorPlatformName(_) => { fail ~"FontContext::create_font_instance() can't yet handle SelectorPlatformName." } + } + } +} diff --git a/src/servo-gfx-2/font_list.rs b/src/servo-gfx-2/font_list.rs new file mode 100644 index 00000000000..1a8bc6147b3 --- /dev/null +++ b/src/servo-gfx-2/font_list.rs @@ -0,0 +1,137 @@ +use font::{CSSFontWeight, SpecifiedFontStyle}; +use native::FontHandle; + +use dvec::DVec; +use send_map::{linear, SendMap}; + +#[cfg(target_os = "macos")] +type FontListHandle/& = quartz::font_list::QuartzFontListHandle; + +#[cfg(target_os = "linux")] +type FontListHandle/& = freetype::font_list::FreeTypeFontListHandle; + +pub impl FontListHandle { + #[cfg(target_os = "macos")] + static pub fn new(fctx: &native::FontContextHandle) -> Result { + Ok(quartz::font_list::QuartzFontListHandle::new(fctx)) + } + + #[cfg(target_os = "linux")] + static pub fn new(fctx: &native::FontContextHandle) -> Result { + Ok(freetype::font_list::FreeTypeFontListHandle::new(fctx)) + } +} + +type FontFamilyMap = linear::LinearMap<~str, @FontFamily>; + +pub struct FontList { + mut family_map: FontFamilyMap, + mut handle: FontListHandle, +} + +pub impl FontList { + static fn new(fctx: &native::FontContextHandle) -> FontList { + let handle = result::unwrap(FontListHandle::new(fctx)); + let list = FontList { + handle: move handle, + family_map: linear::LinearMap(), + }; + list.refresh(fctx); + return move list; + } + + priv fn refresh(fctx: &native::FontContextHandle) { + // TODO(Issue #186): don't refresh unless something actually + // changed. Does OSX have a notification for this event? + do util::time::time("gfx::font_list: regenerating available font families and faces") { + self.family_map = self.handle.get_available_families(fctx); + } + } + + fn find_font_in_family(family_name: &str, + style: &SpecifiedFontStyle) -> Option<@FontEntry> { + let family = self.find_family(family_name); + let mut result : Option<@FontEntry> = None; + + // if such family exists, try to match style to a font + do family.iter |fam| { + result = fam.find_font_for_style(style); + } + return result; + } + + priv fn find_family(family_name: &str) -> Option<@FontFamily> { + // look up canonical name + let family = self.family_map.find(&str::from_slice(family_name)); + + // TODO(Issue #188): look up localized font family names if canonical name not found + return family; + } +} + +// Holds a specific font family, and the various +pub struct FontFamily { + family_name: @str, + entries: DVec<@FontEntry>, +} + +pub impl FontFamily { + static fn new(family_name: &str) -> FontFamily { + FontFamily { + family_name: str::from_slice(family_name).to_managed(), + entries: DVec(), + } + } + + pure fn find_font_for_style(style: &SpecifiedFontStyle) -> Option<@FontEntry> { + assert self.entries.len() > 0; + + // TODO(Issue #189): optimize lookup for + // regular/bold/italic/bolditalic with fixed offsets and a + // static decision table for fallback between these values. + + // TODO(Issue #190): if not in the fast path above, do + // expensive matching of weights, etc. + for self.entries.each |entry| { + if (style.weight.is_bold() == entry.is_bold()) && + (style.italic == entry.is_italic()) { + + return Some(*entry); + } + } + + return None; + } +} + +// This struct summarizes an available font's features. In the future, +// this will include fiddly settings such as special font table handling. + +// In the common case, each FontFamily will have a singleton FontEntry, or +// it will have the standard four faces: Normal, Bold, Italic, BoldItalic. +struct FontEntry { + family: @FontFamily, + face_name: ~str, + priv weight: CSSFontWeight, + priv italic: bool, + handle: FontHandle, + // TODO: array of OpenType features, etc. +} + +impl FontEntry { + static fn new(family: @FontFamily, handle: FontHandle) -> FontEntry { + FontEntry { + family: family, + face_name: handle.face_name(), + weight: handle.boldness(), + italic: handle.is_italic(), + handle: move handle + } + } + + pure fn is_bold() -> bool { + self.weight.is_bold() + } + + pure fn is_italic() -> bool { self.italic } +} diff --git a/src/servo-gfx-2/fontconfig/font_list.rs b/src/servo-gfx-2/fontconfig/font_list.rs new file mode 100644 index 00000000000..109e8b65620 --- /dev/null +++ b/src/servo-gfx-2/fontconfig/font_list.rs @@ -0,0 +1,12 @@ +pub struct FontconfigFontContextHandle { + ctx: (), + + drop { } +} + +pub impl FontconfigFontContextHandle { + // this is a placeholder. + static pub fn new() -> FontconfigFontContextHandle { + FontconfigFontContextHandle { ctx: () } + } +} \ No newline at end of file diff --git a/src/servo-gfx-2/freetype/font.rs b/src/servo-gfx-2/freetype/font.rs new file mode 100644 index 00000000000..f7ea0d34e40 --- /dev/null +++ b/src/servo-gfx-2/freetype/font.rs @@ -0,0 +1,151 @@ +extern mod freetype; + +use font::{FontMetrics, FractionalPixel}; +use font_context::FreeTypeFontContext; +use geometry::Au; +use text::glyph::GlyphIndex; +use text::util::{float_to_fixed, fixed_to_float}; + +use cast::reinterpret_cast; +use ptr::{addr_of, null}; +use vec_as_buf = vec::as_imm_buf; + +use freetype::{ FT_Error, FT_Library, FT_Face, FT_Long, FT_ULong, FT_Size, FT_SizeRec, + FT_UInt, FT_GlyphSlot, FT_Size_Metrics, FT_FaceRec, FT_F26Dot6 }; +use freetype::bindgen::{ + FT_Init_FreeType, + FT_Done_FreeType, + FT_New_Memory_Face, + FT_Done_Face, + FT_Get_Char_Index, + FT_Load_Glyph, + FT_Set_Char_Size +}; + +fn float_to_fixed_ft(f: float) -> i32 { + float_to_fixed(6, f) +} + +fn fixed_to_float_ft(f: i32) -> float { + fixed_to_float(6, f) +} + +pub struct FreeTypeFontHandle { + /// The font binary. This must stay valid for the lifetime of the font + buf: @~[u8], + face: FT_Face, + + drop { + assert self.face.is_not_null(); + if !FT_Done_Face(self.face).succeeded() { + fail ~"FT_Done_Face failed"; + } + } +} + +pub impl FreeTypeFontHandle { + static pub fn new(fctx: &FreeTypeFontContext, + buf: @~[u8], pt_size: float) -> Result { + let ft_ctx = fctx.ctx; + assert ft_ctx.is_not_null(); + let face: FT_Face = null(); + return vec_as_buf(*buf, |cbuf, _len| { + if FT_New_Memory_Face(ft_ctx, cbuf, (*buf).len() as FT_Long, + 0 as FT_Long, addr_of(&face)).succeeded() { + let res = FT_Set_Char_Size(face, // the face + float_to_fixed_ft(pt_size) as FT_F26Dot6, // char width + float_to_fixed_ft(pt_size) as FT_F26Dot6, // char height + 72, // horiz. DPI + 72); // vert. DPI + if !res.succeeded() { fail ~"unable to set font char size" } + Ok(FreeTypeFontHandle { face: face, buf: buf }) + } else { + Err(()) + } + }) + } + + pub fn glyph_index(codepoint: char) -> Option { + assert self.face.is_not_null(); + let idx = FT_Get_Char_Index(self.face, codepoint as FT_ULong); + return if idx != 0 as FT_UInt { + Some(idx as GlyphIndex) + } else { + debug!("Invalid codepoint: %?", codepoint); + None + }; + } + + pub fn glyph_h_advance(glyph: GlyphIndex) -> Option { + assert self.face.is_not_null(); + let res = FT_Load_Glyph(self.face, glyph as FT_UInt, 0); + if res.succeeded() { + unsafe { + let void_glyph = (*self.face).glyph; + let slot: FT_GlyphSlot = reinterpret_cast(&void_glyph); + assert slot.is_not_null(); + debug!("metrics: %?", (*slot).metrics); + let advance = (*slot).metrics.horiAdvance; + debug!("h_advance for %? is %?", glyph, advance); + let advance = advance as i32; + return Some(fixed_to_float_ft(advance) as FractionalPixel); + } + } else { + debug!("Unable to load glyph %?. reason: %?", glyph, res); + return None; + } + } + + pub fn get_metrics() -> FontMetrics { + /* TODO(Issue #76): complete me */ + let face = self.get_face_rec(); + + let underline_size = self.font_units_to_au(face.underline_thickness as float); + let underline_offset = self.font_units_to_au(face.underline_position as float); + let em_size = self.font_units_to_au(face.units_per_EM as float); + let ascent = self.font_units_to_au(face.ascender as float); + let descent = self.font_units_to_au(face.descender as float); + let max_advance = self.font_units_to_au(face.max_advance_width as float); + + return FontMetrics { + underline_size: underline_size, + underline_offset: underline_offset, + leading: geometry::from_pt(0.0), //FIXME + x_height: geometry::from_pt(0.0), //FIXME + em_size: em_size, + ascent: ascent, + descent: descent, + max_advance: max_advance + } + } + + priv fn get_face_rec() -> &self/FT_FaceRec unsafe { + &(*self.face) + } + + priv fn font_units_to_au(value: float) -> Au { + + let face = self.get_face_rec(); + + // face.size is a *c_void in the bindings, presumably to avoid + // recursive structural types + let size: &FT_SizeRec = unsafe { cast::transmute(&(*face.size)) }; + let metrics: &FT_Size_Metrics = unsafe { &((*size).metrics) }; + + let em_size = face.units_per_EM as float; + let x_scale = (metrics.x_ppem as float) / em_size as float; + + // If this isn't true then we're scaling one of the axes wrong + assert metrics.x_ppem == metrics.y_ppem; + + return geometry::from_frac_px(value * x_scale); + } +} + +trait FTErrorMethods { + fn succeeded() -> bool; +} + +impl FT_Error : FTErrorMethods { + fn succeeded() -> bool { self == 0 as FT_Error } +} diff --git a/src/servo-gfx-2/freetype/font_context.rs b/src/servo-gfx-2/freetype/font_context.rs new file mode 100644 index 00000000000..18a168fd77d --- /dev/null +++ b/src/servo-gfx-2/freetype/font_context.rs @@ -0,0 +1,33 @@ +extern mod freetype; + +use freetype::{ + FT_Error, + FT_Library, +}; +use freetype::bindgen::{ + FT_Init_FreeType, + FT_Done_FreeType +}; + + +pub struct FreeTypeFontContextHandle { + ctx: FT_Library, + + drop { + assert self.ctx.is_not_null(); + FT_Done_FreeType(self.ctx); + } +} + +pub impl FreeTypeFontContextHandle { + static pub fn new() -> FreeTypeFontContextHandle { + let lib: FT_Library = ptr::null(); + let res = FT_Init_FreeType(ptr::addr_of(&lib)); + // FIXME: error handling + assert res == 0 as FT_Error; + + FreeTypeFontContext { + ctx: lib, + } + } +} \ No newline at end of file diff --git a/src/servo-gfx-2/geometry.rs b/src/servo-gfx-2/geometry.rs new file mode 100644 index 00000000000..3efcbd05239 --- /dev/null +++ b/src/servo-gfx-2/geometry.rs @@ -0,0 +1,109 @@ +use geom::point::Point2D; +use geom::rect::Rect; +use geom::size::Size2D; +use num::{Num, from_int}; + +pub enum Au = i32; + +impl Au : Num { + pure fn add(other: &Au) -> Au { Au(*self + **other) } + pure fn sub(other: &Au) -> Au { Au(*self - **other) } + pure fn mul(other: &Au) -> Au { Au(*self * **other) } + pure fn div(other: &Au) -> Au { Au(*self / **other) } + pure fn modulo(other: &Au) -> Au { Au(*self % **other) } + pure fn neg() -> Au { Au(-*self) } + + pure fn to_int() -> int { *self as int } + + static pure fn from_int(n: int) -> Au { + Au((n & (i32::max_value as int)) as i32) + } +} + +impl Au : cmp::Ord { + pure fn lt(other: &Au) -> bool { *self < **other } + pure fn le(other: &Au) -> bool { *self <= **other } + pure fn ge(other: &Au) -> bool { *self >= **other } + pure fn gt(other: &Au) -> bool { *self > **other } +} + +impl Au : cmp::Eq { + pure fn eq(other: &Au) -> bool { *self == **other } + pure fn ne(other: &Au) -> bool { *self != **other } +} + +pub pure fn min(x: Au, y: Au) -> Au { if x < y { x } else { y } } +pub pure fn max(x: Au, y: Au) -> Au { if x > y { x } else { y } } + +pub fn box(x: A, y: A, w: A, h: A) -> Rect { + Rect(Point2D(x, y), Size2D(w, h)) +} + +impl Au { + pub pure fn scale_by(factor: float) -> Au { + Au(((*self as float) * factor) as i32) + } + + static pub pure fn from_px(i: int) -> Au { + from_int(i * 60) + } + + pub pure fn to_px(&const self) -> int { + (**self / 60) as int + } + + static pub pure fn zero_point() -> Point2D { + Point2D(Au(0), Au(0)) + } + + static pub pure fn zero_rect() -> Rect { + let z = Au(0); + Rect(Point2D(z, z), Size2D(z, z)) + } + + // assumes 72 points per inch, and 96 px per inch + static pub pure fn from_pt(f: float) -> Au { + from_px((f / 72f * 96f) as int) + } + + static pub pure fn from_frac_px(f: float) -> Au { + Au((f * 60f) as i32) + } + + static pub pure fn min(x: Au, y: Au) -> Au { if *x < *y { x } else { y } } + static pub pure fn max(x: Au, y: Au) -> Au { if *x > *y { x } else { y } } +} + +pub pure fn zero_rect() -> Rect { + let z = Au(0); + Rect(Point2D(z, z), Size2D(z, z)) +} + +pub pure fn zero_point() -> Point2D { + Point2D(Au(0), Au(0)) +} + +pub pure fn zero_size() -> Size2D { + Size2D(Au(0), Au(0)) +} + +pub pure fn from_frac_px(f: float) -> Au { + Au((f * 60f) as i32) +} + +pub pure fn from_px(i: int) -> Au { + from_int(i * 60) +} + +pub pure fn to_px(au: Au) -> int { + (*au / 60) as int +} + +pub pure fn to_frac_px(au: Au) -> float { + (*au as float) / 60f +} + +// assumes 72 points per inch, and 96 px per inch +pub pure fn from_pt(f: float) -> Au { + from_px((f / 72f * 96f) as int) +} diff --git a/src/servo-gfx-2/image/base.rs b/src/servo-gfx-2/image/base.rs new file mode 100644 index 00000000000..5444ca7949b --- /dev/null +++ b/src/servo-gfx-2/image/base.rs @@ -0,0 +1,43 @@ +use stb_image = stb_image::image; + +// FIXME: Images must not be copied every frame. Instead we should atomically +// reference count them. + +pub type Image = stb_image::Image; + +pub fn Image(width: uint, height: uint, depth: uint, data: ~[u8]) -> Image { + stb_image::new_image(width, height, depth, move data) +} + +const TEST_IMAGE: [u8 * 4962] = #include_bin("test.jpeg"); + +pub fn test_image_bin() -> ~[u8] { + return vec::from_fn(4962, |i| TEST_IMAGE[i]); +} + +pub fn load_from_memory(buffer: &[u8]) -> Option { + + // Can't remember why we do this. Maybe it's what cairo wants + const FORCE_DEPTH: uint = 4; + + do stb_image::load_from_memory_with_depth(buffer, FORCE_DEPTH).map |image| { + + assert image.depth == 4; + // Do color space conversion :( + let data = do vec::from_fn(image.width * image.height * 4) |i| { + let color = i % 4; + let pixel = i / 4; + match color { + 0 => image.data[pixel * 4 + 2], + 1 => image.data[pixel * 4 + 1], + 2 => image.data[pixel * 4 + 0], + 3 => 0xffu8, + _ => fail + } + }; + + assert image.data.len() == data.len(); + + Image(image.width, image.height, image.depth, move data) + } +} diff --git a/src/servo-gfx-2/image/encode/tga.rs b/src/servo-gfx-2/image/encode/tga.rs new file mode 100644 index 00000000000..4432243de1e --- /dev/null +++ b/src/servo-gfx-2/image/encode/tga.rs @@ -0,0 +1,23 @@ +use io::WriterUtil; + +fn encode(writer: io::Writer, surface: &surface::image_surface) { + assert surface.format == surface::fo_rgba_8888; + + writer.write_u8(0u8); // identsize + writer.write_u8(0u8); // colourmaptype + writer.write_u8(2u8); // imagetype + + writer.write_le_u16(0u16); // colourmapstart + writer.write_le_u16(0u16); // colourmaplength + writer.write_u8(16u8); // colourmapbits + + writer.write_le_u16(0u16); // xstart + writer.write_le_u16(0u16); // ystart + writer.write_le_u16(surface.size.width as u16); // width + writer.write_le_u16(surface.size.height as u16); // height + writer.write_u8(32u8); // bits + writer.write_u8(0x30u8); // descriptor + + writer.write(surface.buffer); +} + diff --git a/src/servo-gfx-2/image/holder.rs b/src/servo-gfx-2/image/holder.rs new file mode 100644 index 00000000000..d6f336e6ca5 --- /dev/null +++ b/src/servo-gfx-2/image/holder.rs @@ -0,0 +1,102 @@ +use image::base::Image; +use resource::image_cache_task::{ImageCacheTask, ImageReady, ImageNotReady, ImageFailed}; +use resource::image_cache_task; +use resource::local_image_cache::LocalImageCache; + +use core::util::replace; +use geom::size::Size2D; +use std::net::url::Url; +use std::arc::{ARC, clone, get}; + +// FIXME: Nasty coupling to resource here. This should probably be factored out into an interface +// and use dependency injection. + +/** A struct to store image data. The image will be loaded once, the + first time it is requested, and an arc will be stored. Clones of + this arc are given out on demand. + */ +pub struct ImageHolder { + url: Url, + mut image: Option>, + mut cached_size: Size2D, + local_image_cache: @LocalImageCache, +} + +impl ImageHolder { + static pub fn new(url: Url, local_image_cache: @LocalImageCache) -> ImageHolder { + debug!("ImageHolder::new() %?", url.to_str()); + let holder = ImageHolder { + url: move url, + image: None, + cached_size: Size2D(0,0), + local_image_cache: local_image_cache, + }; + + // Tell the image cache we're going to be interested in this url + // FIXME: These two messages must be sent to prep an image for use + // but they are intended to be spread out in time. Ideally prefetch + // should be done as early as possible and decode only once we + // are sure that the image will be used. + local_image_cache.prefetch(&holder.url); + local_image_cache.decode(&holder.url); + + move holder + } + + /** + This version doesn't perform any computation, but may be stale w.r.t. + newly-available image data that determines size. + + The intent is that the impure version is used during layout when + dimensions are used for computing layout. + */ + pure fn size() -> Size2D { + self.cached_size + } + + /** Query and update current image size */ + fn get_size() -> Option> { + debug!("get_size() %?", self.url); + match self.get_image() { + Some(img) => { + let img_ref = get(&img); + self.cached_size = Size2D(img_ref.width as int, + img_ref.height as int); + Some(copy self.cached_size) + }, + None => None + } + } + + fn get_image() -> Option> { + debug!("get_image() %?", self.url); + + // If this is the first time we've called this function, load + // the image and store it for the future + if self.image.is_none() { + match self.local_image_cache.get_image(&self.url).recv() { + ImageReady(move image) => { + self.image = Some(move image); + } + ImageNotReady => { + debug!("image not ready for %s", self.url.to_str()); + } + ImageFailed => { + debug!("image decoding failed for %s", self.url.to_str()); + } + } + } + + // Clone isn't pure so we have to swap out the mutable image option + let image = replace(&mut self.image, None); + + let result = match image { + Some(ref image) => Some(clone(image)), + None => None + }; + + replace(&mut self.image, move image); + + return move result; + } +} diff --git a/src/servo-gfx-2/image/test.jpeg b/src/servo-gfx-2/image/test.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..1a0bdb7acd1d231e015270ccc1727b31b2002003 GIT binary patch literal 4962 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 literal 0 HcmV?d00001 diff --git a/src/servo-gfx-2/native.rs b/src/servo-gfx-2/native.rs new file mode 100644 index 00000000000..9ae03bdc06b --- /dev/null +++ b/src/servo-gfx-2/native.rs @@ -0,0 +1,9 @@ +/* This file exists just to make it easier to import platform-specific + implementations. + +Note that you still must define each of the files as a module in +servo_gfx.rc. This is not ideal and may be changed in the future. */ + +pub use font::FontHandle; +pub use font_context::FontContextHandle; +pub use font_list::FontListHandle; diff --git a/src/servo-gfx-2/opts.rs b/src/servo-gfx-2/opts.rs new file mode 100644 index 00000000000..f6b2567c6e7 --- /dev/null +++ b/src/servo-gfx-2/opts.rs @@ -0,0 +1,77 @@ +//! Configuration options for a single run of the servo application. Created +//! from command line arguments. + +use azure::azure_hl::{BackendType, CairoBackend, CoreGraphicsBackend}; +use azure::azure_hl::{CoreGraphicsAcceleratedBackend, Direct2DBackend, SkiaBackend}; + +pub struct Opts { + urls: ~[~str], + render_mode: RenderMode, + render_backend: BackendType, + n_render_threads: uint, +} + +pub enum RenderMode { + Screen, + Png(~str) +} + +#[allow(non_implicitly_copyable_typarams)] +pub fn from_cmdline_args(args: &[~str]) -> Opts { + use std::getopts; + + let args = args.tail(); + + let opts = ~[ + getopts::optopt(~"o"), + getopts::optopt(~"r"), + getopts::optopt(~"t"), + ]; + + let opt_match = match getopts::getopts(args, opts) { + result::Ok(m) => { copy m } + result::Err(f) => { fail getopts::fail_str(copy f) } + }; + + let urls = if opt_match.free.is_empty() { + fail ~"servo asks that you provide 1 or more URLs" + } else { + copy opt_match.free + }; + + let render_mode = match getopts::opt_maybe_str(copy opt_match, ~"o") { + Some(move output_file) => { Png(move output_file) } + None => { Screen } + }; + + let render_backend = match getopts::opt_maybe_str(copy opt_match, ~"r") { + Some(move backend_str) => { + if backend_str == ~"direct2d" { + Direct2DBackend + } else if backend_str == ~"core-graphics" { + CoreGraphicsBackend + } else if backend_str == ~"core-graphics-accelerated" { + CoreGraphicsAcceleratedBackend + } else if backend_str == ~"cairo" { + CairoBackend + } else if backend_str == ~"skia" { + SkiaBackend + } else { + fail ~"unknown backend type" + } + } + None => CairoBackend + }; + + let n_render_threads: uint = match getopts::opt_maybe_str(move opt_match, ~"t") { + Some(move n_render_threads_str) => from_str::from_str(n_render_threads_str).get(), + None => 1, // FIXME: Number of cores. + }; + + Opts { + urls: move urls, + render_mode: move render_mode, + render_backend: move render_backend, + n_render_threads: n_render_threads, + } +} diff --git a/src/servo-gfx-2/quartz/font.rs b/src/servo-gfx-2/quartz/font.rs new file mode 100644 index 00000000000..83aa41e8b98 --- /dev/null +++ b/src/servo-gfx-2/quartz/font.rs @@ -0,0 +1,161 @@ +extern mod core_foundation; +extern mod core_graphics; +extern mod core_text; + +use font_context::QuartzFontContextHandle; +use geometry::Au; +use servo_gfx_font::{CSSFontWeight, FontHandleMethods, FontMetrics, FontWeight100, FontWeight200}; +use servo_gfx_font::{FontWeight300, FontWeight400, FontWeight500, FontWeight600, FontWeight700}; +use servo_gfx_font::{FontWeight800, FontWeight900, FractionalPixel}; +use text::glyph::GlyphIndex; + +use cf = core_foundation; +use cf::base::{CFIndex, CFRelease, CFTypeRef}; +use cf::string::UniChar; +use cg = core_graphics; +use cg::base::{CGFloat, CGAffineTransform}; +use cg::data_provider::{CGDataProviderRef, CGDataProvider}; +use cg::font::{CGFontCreateWithDataProvider, CGFontRef, CGFontRelease, CGGlyph}; +use cg::geometry::CGRect; +use core::libc::size_t; +use ct = core_text; +use ct::font::CTFont; +use ct::font_descriptor::{kCTFontDefaultOrientation, CTFontSymbolicTraits}; +use ct::font_descriptor::{SymbolicTraitAccessors}; + +pub struct QuartzFontHandle { + priv mut cgfont: Option, + ctfont: CTFont, + + drop { + // TODO(Issue #152): use a wrapped CGFont. + do (copy self.cgfont).iter |cgfont| { + assert cgfont.is_not_null(); + CGFontRelease(*cgfont); + } + } +} + +pub impl QuartzFontHandle { + static fn new_from_buffer(_fctx: &QuartzFontContextHandle, buf: @~[u8], pt_size: float) -> Result { + let fontprov = vec::as_imm_buf(*buf, |cbuf, len| { + CGDataProvider::new_from_buffer(cbuf, len) + }); + + let cgfont = CGFontCreateWithDataProvider(fontprov.get_ref()); + if cgfont.is_null() { return Err(()); } + + let ctfont = CTFont::new_from_CGFont(cgfont, pt_size); + + let result = Ok(QuartzFontHandle { + cgfont : Some(cgfont), + ctfont : move ctfont, + }); + + return move result; + } + + static fn new_from_CTFont(_fctx: &QuartzFontContextHandle, ctfont: CTFont) -> Result { + let result = Ok(QuartzFontHandle { + mut cgfont: None, + ctfont: move ctfont, + }); + + return move result; + } + + fn get_CGFont() -> CGFontRef { + match self.cgfont { + Some(cg) => cg, + None => { + let cgfont = self.ctfont.copy_to_CGFont(); + self.cgfont = Some(cgfont); + cgfont + } + } + } +} + +pub impl QuartzFontHandle : FontHandleMethods { + pure fn family_name() -> ~str { + self.ctfont.family_name() + } + + pure fn face_name() -> ~str { + self.ctfont.face_name() + } + + pure fn is_italic() -> bool { + self.ctfont.symbolic_traits().is_italic() + } + + pure fn boldness() -> CSSFontWeight { + // -1.0 to 1.0 + let normalized = unsafe { self.ctfont.all_traits().normalized_weight() }; + // 0.0 to 9.0 + let normalized = (normalized + 1.0) / 2.0 * 9.0; + if normalized < 1.0 { return FontWeight100; } + if normalized < 2.0 { return FontWeight200; } + if normalized < 3.0 { return FontWeight300; } + if normalized < 4.0 { return FontWeight400; } + if normalized < 5.0 { return FontWeight500; } + if normalized < 6.0 { return FontWeight600; } + if normalized < 7.0 { return FontWeight700; } + if normalized < 8.0 { return FontWeight800; } + else { return FontWeight900; } + } + + + fn glyph_index(codepoint: char) -> Option { + let characters: ~[UniChar] = ~[codepoint as UniChar]; + let glyphs: ~[mut CGGlyph] = ~[mut 0 as CGGlyph]; + let count: CFIndex = 1; + + let result = do vec::as_imm_buf(characters) |character_buf, _l| { + do vec::as_imm_buf(glyphs) |glyph_buf, _l| { + self.ctfont.get_glyphs_for_characters(character_buf, glyph_buf, count) + } + }; + + if !result { + // No glyph for this character + return None; + } + + assert glyphs[0] != 0; // FIXME: error handling + return Some(glyphs[0] as GlyphIndex); + } + + fn glyph_h_advance(glyph: GlyphIndex) -> Option { + let glyphs = ~[glyph as CGGlyph]; + let advance = do vec::as_imm_buf(glyphs) |glyph_buf, _l| { + self.ctfont.get_advances_for_glyphs(kCTFontDefaultOrientation, glyph_buf, ptr::null(), 1) + }; + + return Some(advance as FractionalPixel); + } + + fn get_metrics() -> FontMetrics { + let bounding_rect: CGRect = self.ctfont.bounding_box(); + let ascent = Au::from_pt(self.ctfont.ascent() as float); + let descent = Au::from_pt(self.ctfont.descent() as float); + + let metrics = FontMetrics { + underline_size: Au::from_pt(self.ctfont.underline_thickness() as float), + // TODO: underline metrics are not reliable. Have to pull out of font table directly. + // see also: https://bugs.webkit.org/show_bug.cgi?id=16768 + // see also: https://bugreports.qt-project.org/browse/QTBUG-13364 + underline_offset: Au::from_pt(self.ctfont.underline_position() as float), + leading: Au::from_pt(self.ctfont.leading() as float), + x_height: Au::from_pt(self.ctfont.x_height() as float), + em_size: ascent + descent, + ascent: ascent, + descent: descent, + max_advance: Au::from_pt(bounding_rect.size.width as float) + }; + + debug!("Font metrics (@%f pt): %?", self.ctfont.pt_size() as float, metrics); + return metrics; + } +} + diff --git a/src/servo-gfx-2/quartz/font_context.rs b/src/servo-gfx-2/quartz/font_context.rs new file mode 100644 index 00000000000..2e227923a7b --- /dev/null +++ b/src/servo-gfx-2/quartz/font_context.rs @@ -0,0 +1,12 @@ +pub struct QuartzFontContextHandle { + ctx: (), + + drop { } +} + +pub impl QuartzFontContextHandle { + // this is a placeholder until NSFontManager or whatever is bound in here. + static pub fn new() -> QuartzFontContextHandle { + QuartzFontContextHandle { ctx: () } + } +} \ No newline at end of file diff --git a/src/servo-gfx-2/quartz/font_list.rs b/src/servo-gfx-2/quartz/font_list.rs new file mode 100644 index 00000000000..39357884403 --- /dev/null +++ b/src/servo-gfx-2/quartz/font_list.rs @@ -0,0 +1,56 @@ +extern mod core_foundation; +extern mod core_text; + +use quartz::font::{QuartzFontHandle}; +use servo_gfx_font::FontHandle; +use servo_gfx_font_list::{FontEntry, FontFamily}; + +use cf = core_foundation; +use cf::array::CFArray; +use core::dvec::DVec; +use core::send_map::{linear, SendMap}; +use ct = core_text; +use ct::font::{CTFont, debug_font_names, debug_font_traits}; +use ct::font_collection::CTFontCollection; +use ct::font_descriptor::{CTFontDescriptor, CTFontDescriptorRef, debug_descriptor}; + +pub struct QuartzFontListHandle { + collection: CTFontCollection, +} + +pub impl QuartzFontListHandle { + static pub fn new(_fctx: &native::FontContextHandle) -> QuartzFontListHandle { + QuartzFontListHandle { collection: CTFontCollection::new() } + } + + fn get_available_families(&const self, + fctx: &native::FontContextHandle) + -> linear::LinearMap<~str, @FontFamily> { + // since we mutate it repeatedly, must be mut variable. + let mut family_map : linear::LinearMap<~str, @FontFamily> = linear::LinearMap(); + let descriptors : CFArray; + descriptors = self.collection.get_descriptors(); + for descriptors.each |desc: &CTFontDescriptor| { + // TODO: for each descriptor, make a FontEntry. + let font = CTFont::new_from_descriptor(desc, 0.0); + let handle = result::unwrap(QuartzFontHandle::new_from_CTFont(fctx, move font)); + let family_name = handle.family_name(); + debug!("Looking for family name: %s", family_name); + let family = match family_map.find(&family_name) { + Some(fam) => fam, + None => { + debug!("Creating new FontFamily for family: %s", family_name); + let new_family = @FontFamily::new(family_name); + family_map.insert(move family_name, new_family); + new_family + } + }; + + debug!("Creating new FontEntry for face: %s", handle.face_name()); + let entry = @FontEntry::new(family, move handle); + family.entries.push(entry); + // TODO: append FontEntry to hashtable value + } + return move family_map; + } +} diff --git a/src/servo-gfx-2/render_context.rs b/src/servo-gfx-2/render_context.rs new file mode 100644 index 00000000000..fe51ece25ea --- /dev/null +++ b/src/servo-gfx-2/render_context.rs @@ -0,0 +1,105 @@ +use compositor::LayerBuffer; +use font_context::FontContext; +use geometry::Au; +use image::base::Image; +use opts::Opts; +use text::TextRun; +use util::range::Range; + +use azure::azure_hl::{AsAzureRect, B8G8R8A8, Color, ColorPattern, DrawOptions}; +use azure::azure_hl::{DrawSurfaceOptions, DrawTarget, Linear, StrokeOptions}; +use azure::{AzDrawOptions, AzFloat}; +use core::dvec::DVec; +use core::libc::types::common::c99::uint16_t; +use core::ptr::to_unsafe_ptr; +use geom::point::Point2D; +use geom::rect::Rect; +use geom::size::Size2D; +use std::arc::ARC; + +struct RenderContext { + canvas: &LayerBuffer, + font_ctx: @FontContext, + opts: &Opts +} + +impl RenderContext { + pub fn get_draw_target(&self) -> &self/DrawTarget { + &self.canvas.draw_target + } + + pub fn draw_solid_color(&self, bounds: &Rect, color: Color) { + self.canvas.draw_target.fill_rect(&bounds.to_azure_rect(), &ColorPattern(color)); + } + + pub fn draw_border(&self, bounds: &Rect, width: Au, color: Color) { + let pattern = ColorPattern(color); + let stroke_fields = 2; // CAP_SQUARE + let width_px = width.to_px(); + let rect = if width_px % 2 == 0 { + bounds.to_azure_rect() + } else { + bounds.to_azure_snapped_rect() + }; + + let stroke_opts = StrokeOptions(width_px as AzFloat, 10 as AzFloat, stroke_fields); + let draw_opts = DrawOptions(1 as AzFloat, 0 as uint16_t); + + self.canvas.draw_target.stroke_rect(&rect, &pattern, &stroke_opts, &draw_opts); + } + + pub fn draw_image(&self, bounds: Rect, image: ARC<~Image>) { + let image = std::arc::get(&image); + let size = Size2D(image.width as i32, image.height as i32); + let stride = image.width * 4; + + let draw_target_ref = &self.canvas.draw_target; + let azure_surface = draw_target_ref.create_source_surface_from_data(image.data, size, + stride as i32, B8G8R8A8); + let source_rect = Rect(Point2D(0 as AzFloat, 0 as AzFloat), + Size2D(image.width as AzFloat, image.height as AzFloat)); + let dest_rect = bounds.to_azure_rect(); + let draw_surface_options = DrawSurfaceOptions(Linear, true); + let draw_options = DrawOptions(1.0f as AzFloat, 0); + draw_target_ref.draw_surface(move azure_surface, dest_rect, source_rect, + draw_surface_options, draw_options); + } + + fn clear(&self) { + let pattern = ColorPattern(Color(1f as AzFloat, 1f as AzFloat, 1f as AzFloat, 1f as AzFloat)); + let rect = Rect(Point2D(self.canvas.rect.origin.x as AzFloat, + self.canvas.rect.origin.y as AzFloat), + Size2D(self.canvas.rect.size.width as AzFloat, + self.canvas.rect.size.height as AzFloat)); + self.canvas.draw_target.fill_rect(&rect, &pattern); + } +} + +trait to_float { + fn to_float() -> float; +} + +impl u8 : to_float { + fn to_float() -> float { + (self as float) / 255f + } +} + +trait ToAzureRect { + fn to_azure_rect() -> Rect; + fn to_azure_snapped_rect() -> Rect; +} + +impl Rect : ToAzureRect { + fn to_azure_rect() -> Rect { + Rect(Point2D(self.origin.x.to_px() as AzFloat, self.origin.y.to_px() as AzFloat), + Size2D(self.size.width.to_px() as AzFloat, self.size.height.to_px() as AzFloat)) + } + + fn to_azure_snapped_rect() -> Rect { + Rect(Point2D(self.origin.x.to_px() as AzFloat + 0.5f as AzFloat, + self.origin.y.to_px() as AzFloat + 0.5f as AzFloat), + Size2D(self.size.width.to_px() as AzFloat, + self.size.height.to_px() as AzFloat)) + } +} diff --git a/src/servo-gfx-2/render_layers.rs b/src/servo-gfx-2/render_layers.rs new file mode 100644 index 00000000000..82cf327c1d0 --- /dev/null +++ b/src/servo-gfx-2/render_layers.rs @@ -0,0 +1,137 @@ +use compositor::{LayerBuffer, LayerBufferSet}; +use display_list::DisplayList; +use opts::Opts; +use util::time; + +use azure::AzFloat; +use azure::azure_hl::{B8G8R8A8, DrawTarget}; +use core::libc::c_int; +use core::pipes::Chan; +use geom::matrix2d::Matrix2D; +use geom::point::Point2D; +use geom::rect::Rect; +use geom::size::Size2D; +use std::arc::ARC; +use std::arc; + +const TILE_SIZE: uint = 512; + +pub struct RenderLayer { + display_list: DisplayList, + size: Size2D +} + +type RenderFn = &fn(layer: *RenderLayer, + buffer: LayerBuffer, + return_buffer: Chan); + +/// Given a layer and a buffer, either reuses the buffer (if it's of the right size and format) +/// or creates a new buffer (if it's not of the appropriate size and format) and invokes the +/// given callback with the render layer and the buffer. Returns the resulting layer buffer (which +/// might be the old layer buffer if it had the appropriate size and format). +pub fn render_layers(layer_ref: *RenderLayer, + buffer_set: LayerBufferSet, + opts: &Opts, + f: RenderFn) -> LayerBufferSet { + let mut buffers = match move buffer_set { LayerBufferSet { buffers: move b } => move b }; + + // FIXME: Try not to create a new array here. + let new_buffer_ports = dvec::DVec(); + + // Divide up the layer into tiles. + do time::time("rendering: preparing buffers") { + let layer: &RenderLayer = unsafe { cast::transmute(layer_ref) }; + let mut y = 0; + while y < layer.size.height { + let mut x = 0; + while x < layer.size.width { + // Figure out the dimension of this tile. + let right = uint::min(x + TILE_SIZE, layer.size.width); + let bottom = uint::min(y + TILE_SIZE, layer.size.height); + let width = right - x; + let height = bottom - y; + + // Round the width up the nearest 32 pixels for DMA on the Mac. + let mut stride = width; + if stride % 32 != 0 { + stride = (stride & !(32 - 1)) + 32; + } + assert stride % 32 == 0; + assert stride >= width; + + debug!("tile stride %u", stride); + + let tile_rect = Rect(Point2D(x, y), Size2D(width, height)); + + let buffer; + // FIXME: Try harder to search for a matching tile. + // FIXME: Don't use shift; it's bad for perf. Maybe reverse and pop. + /*if buffers.len() != 0 && buffers[0].rect == tile_rect { + debug!("reusing tile, (%u, %u)", x, y); + buffer = buffers.shift(); + } else {*/ + // Create a new buffer. + debug!("creating tile, (%u, %u)", x, y); + + let size = Size2D(stride as i32, height as i32); + + let mut data: ~[u8] = ~[0]; + let offset; + unsafe { + // FIXME: Evil black magic to ensure that we don't perform a slow memzero + // of this buffer. This should be made safe. + + let align = 256; + + let len = ((size.width * size.height * 4) as uint) + align; + vec::reserve(&mut data, len); + vec::raw::set_len(&mut data, len); + + // Round up to the nearest 32-byte-aligned address for DMA on the Mac. + let addr: uint = cast::transmute(ptr::to_unsafe_ptr(&data[0])); + if addr % align == 0 { + offset = 0; + } else { + offset = align - addr % align; + } + + debug!("tile offset is %u, expected addr is %x", offset, addr + offset); + } + + buffer = LayerBuffer { + draw_target: DrawTarget::new_with_data(opts.render_backend, + move data, + offset, + size, + size.width * 4, + B8G8R8A8), + rect: tile_rect, + stride: stride + }; + //} + + // Create a port and channel pair to receive the new buffer. + let (new_buffer_chan, new_buffer_port) = pipes::stream(); + + // Send the buffer to the child. + f(layer_ref, move buffer, move new_buffer_chan); + + // Enqueue the port. + new_buffer_ports.push(move new_buffer_port); + + x += TILE_SIZE; + } + y += TILE_SIZE; + } + } + + let new_buffers = dvec::DVec(); + do time::time("rendering: waiting on subtasks") { + for new_buffer_ports.each |new_buffer_port| { + new_buffers.push(new_buffer_port.recv()); + } + } + + return LayerBufferSet { buffers: move dvec::unwrap(move new_buffers) }; +} + diff --git a/src/servo-gfx-2/render_task.rs b/src/servo-gfx-2/render_task.rs new file mode 100644 index 00000000000..926a40f7ae1 --- /dev/null +++ b/src/servo-gfx-2/render_task.rs @@ -0,0 +1,157 @@ +use compositor::{Compositor, LayerBufferSet}; +use font_context::FontContext; +use opts::Opts; +use render_context::RenderContext; +use render_layers::{RenderLayer, render_layers}; + +use azure::AzFloat; +use core::comm::*; +use core::libc::size_t; +use core::libc::types::common::c99::uint16_t; +use core::pipes::{Port, Chan}; +use core::task::SingleThreaded; +use geom::matrix2d::Matrix2D; +use std::arc::ARC; +use std::arc; +use std::cell::Cell; +use std::thread_pool::ThreadPool; + +pub enum Msg { + RenderMsg(RenderLayer), + ExitMsg(pipes::Chan<()>) +} + +pub type RenderTask = comm::Chan; + +pub fn RenderTask(compositor: C, opts: Opts) -> RenderTask { + let compositor_cell = Cell(move compositor); + let opts_cell = Cell(move opts); + do task::spawn_listener |po: comm::Port, move compositor_cell, move opts_cell| { + let (layer_buffer_channel, layer_buffer_set_port) = pipes::stream(); + + let compositor = compositor_cell.take(); + compositor.begin_drawing(move layer_buffer_channel); + + // FIXME: Annoying three-cell dance here. We need one-shot closures. + let opts = opts_cell.with_ref(|o| copy *o); + let n_threads = opts.n_render_threads; + let new_opts_cell = Cell(move opts); + + let thread_pool = do ThreadPool::new(n_threads, Some(SingleThreaded)) + |move new_opts_cell| { + let opts_cell = Cell(new_opts_cell.with_ref(|o| copy *o)); + let f: ~fn(uint) -> ThreadRenderContext = |thread_index, move opts_cell| { + ThreadRenderContext { + thread_index: thread_index, + font_ctx: @FontContext::new(opts_cell.with_ref(|o| o.render_backend), false), + opts: opts_cell.with_ref(|o| copy *o), + } + }; + move f + }; + + Renderer { + port: po, + compositor: move compositor, + mut layer_buffer_set_port: Cell(move layer_buffer_set_port), + thread_pool: move thread_pool, + opts: opts_cell.take() + }.start(); + } +} + +/// Data that needs to be kept around for each render thread. +priv struct ThreadRenderContext { + thread_index: uint, + font_ctx: @FontContext, + opts: Opts, +} + +priv struct Renderer { + port: comm::Port, + compositor: C, + layer_buffer_set_port: Cell>, + thread_pool: ThreadPool, + opts: Opts, +} + +impl Renderer { + fn start() { + debug!("renderer: beginning rendering loop"); + + loop { + match self.port.recv() { + RenderMsg(move render_layer) => self.render(move render_layer), + ExitMsg(response_ch) => { + response_ch.send(()); + break; + } + } + } + } + + fn render(render_layer: RenderLayer) { + debug!("renderer: got render request"); + + let layer_buffer_set_port = self.layer_buffer_set_port.take(); + + if !layer_buffer_set_port.peek() { + warn!("renderer: waiting on layer buffer"); + } + + let layer_buffer_set = layer_buffer_set_port.recv(); + let (layer_buffer_set_channel, new_layer_buffer_set_port) = pipes::stream(); + self.layer_buffer_set_port.put_back(move new_layer_buffer_set_port); + + let layer_buffer_set_cell = Cell(move layer_buffer_set); + let layer_buffer_set_channel_cell = Cell(move layer_buffer_set_channel); + + #debug("renderer: rendering"); + + do util::time::time(~"rendering") { + let layer_buffer_set = layer_buffer_set_cell.take(); + let layer_buffer_set_channel = layer_buffer_set_channel_cell.take(); + + let layer_buffer_set = do render_layers(ptr::to_unsafe_ptr(&render_layer), + move layer_buffer_set, + &self.opts) + |render_layer_ref, layer_buffer, buffer_chan| { + let layer_buffer_cell = Cell(move layer_buffer); + do self.thread_pool.execute |thread_render_context, + move render_layer_ref, + move buffer_chan, + move layer_buffer_cell| { + do layer_buffer_cell.with_ref |layer_buffer| { + // Build the render context. + let ctx = RenderContext { + canvas: layer_buffer, + font_ctx: thread_render_context.font_ctx, + opts: &thread_render_context.opts + }; + + // Apply the translation to render the tile we want. + let matrix: Matrix2D = Matrix2D::identity(); + let matrix = matrix.translate(&-(layer_buffer.rect.origin.x as AzFloat), + &-(layer_buffer.rect.origin.y as AzFloat)); + layer_buffer.draw_target.set_transform(&matrix); + + // Clear the buffer. + ctx.clear(); + + // Draw the display list. + let render_layer: &RenderLayer = unsafe { + cast::transmute(render_layer_ref) + }; + render_layer.display_list.draw_into_context(&ctx); + } + + // Send back the buffer. + buffer_chan.send(layer_buffer_cell.take()); + } + }; + + #debug("renderer: returning surface"); + self.compositor.draw(move layer_buffer_set_channel, move layer_buffer_set); + } + } +} diff --git a/src/servo-gfx-2/resource/file_loader.rs b/src/servo-gfx-2/resource/file_loader.rs new file mode 100644 index 00000000000..ab92f26c4a4 --- /dev/null +++ b/src/servo-gfx-2/resource/file_loader.rs @@ -0,0 +1,29 @@ +export factory; + +use comm::Chan; +use task::spawn; +use resource_task::{ProgressMsg, Payload, Done}; +use std::net::url::Url; +use io::{file_reader, ReaderUtil}; + +const READ_SIZE: uint = 1024; + +pub fn factory(url: Url, progress_chan: Chan) { + assert url.scheme == ~"file"; + + do spawn |move url| { + // FIXME: Resolve bug prevents us from moving the path out of the URL. + match file_reader(&Path(url.path)) { + Ok(reader) => { + while !reader.eof() { + let data = reader.read_bytes(READ_SIZE); + progress_chan.send(Payload(move data)); + } + progress_chan.send(Done(Ok(()))); + } + Err(*) => { + progress_chan.send(Done(Err(()))); + } + }; + } +} diff --git a/src/servo-gfx-2/resource/http_loader.rs b/src/servo-gfx-2/resource/http_loader.rs new file mode 100644 index 00000000000..8c20c7eb559 --- /dev/null +++ b/src/servo-gfx-2/resource/http_loader.rs @@ -0,0 +1,38 @@ +export factory; + +use comm::Chan; +use task::spawn; +use resource_task::{ProgressMsg, Payload, Done}; +use std::net::url::Url; +use http_client::{uv_http_request}; + +pub fn factory(url: Url, progress_chan: Chan) { + assert url.scheme == ~"http"; + + do spawn |move url| { + #debug("http_loader: requesting via http: %?", copy url); + let request = uv_http_request(copy url); + let errored = @mut false; + do request.begin |event, copy url| { + let url = copy url; + match event { + http_client::Status(*) => { } + http_client::Payload(data) => { + #debug("http_loader: got data from %?", url); + let mut junk = None; + *data <-> junk; + progress_chan.send(Payload(option::unwrap(move junk))); + } + http_client::Error(*) => { + #debug("http_loader: error loading %?", url); + *errored = true; + progress_chan.send(Done(Err(()))); + } + } + } + + if !*errored { + progress_chan.send(Done(Ok(()))); + } + } +} diff --git a/src/servo-gfx-2/resource/image_cache_task.rs b/src/servo-gfx-2/resource/image_cache_task.rs new file mode 100644 index 00000000000..0e6399da05a --- /dev/null +++ b/src/servo-gfx-2/resource/image_cache_task.rs @@ -0,0 +1,1101 @@ +use image::base::{Image, load_from_memory, test_image_bin}; +use resource::resource_task; +use resource_task::ResourceTask; +use util::url::{make_url, UrlMap, url_map}; + +use clone_arc = std::arc::clone; +use core::pipes::{Chan, Port, SharedChan, stream}; +use core::task::{spawn, spawn_listener}; +use core::to_str::ToStr; +use core::util::replace; +use std::arc::ARC; +use std::net::url::Url; +use std::cell::Cell; + +pub enum Msg { + /// Tell the cache that we may need a particular image soon. Must be posted + /// before Decode + pub Prefetch(Url), + + // FIXME: We can probably get rid of this Cell now + /// Used be the prefetch tasks to post back image binaries + priv StorePrefetchedImageData(Url, Result, ()>), + + /// Tell the cache to decode an image. Must be posted before GetImage/WaitForImage + pub Decode(Url), + + /// Used by the decoder tasks to post decoded images back to the cache + priv StoreImage(Url, Option>), + + /// Request an Image object for a URL. If the image is not is not immediately + /// available then ImageNotReady is returned. + pub GetImage(Url, Chan), + + /// Wait for an image to become available (or fail to load). + pub WaitForImage(Url, Chan), + + /// For testing + priv OnMsg(fn~(msg: &Msg)), + + /// Clients must wait for a response before shutting down the ResourceTask + pub Exit(Chan<()>) +} + +pub enum ImageResponseMsg { + ImageReady(ARC<~Image>), + ImageNotReady, + ImageFailed +} + +impl ImageResponseMsg { + pure fn clone() -> ImageResponseMsg { + match self { + ImageReady(img) => ImageReady(unsafe { clone_arc(&img) }), + ImageNotReady => ImageNotReady, + ImageFailed => ImageFailed + } + } +} + +impl ImageResponseMsg: cmp::Eq { + pure fn eq(other: &ImageResponseMsg) -> bool { + // FIXME: Bad copies + match (self.clone(), other.clone()) { + (ImageReady(*), ImageReady(*)) => fail ~"unimplemented comparison", + (ImageNotReady, ImageNotReady) => true, + (ImageFailed, ImageFailed) => true, + + (ImageReady(*), _) + | (ImageNotReady, _) + | (ImageFailed, _) => false + } + } + pure fn ne(other: &ImageResponseMsg) -> bool { + return !self.eq(other); + } +} + +pub type ImageCacheTask = SharedChan; + +type DecoderFactory = ~fn() -> ~fn(&[u8]) -> Option; + +pub fn ImageCacheTask(resource_task: ResourceTask) -> ImageCacheTask { + ImageCacheTask_(resource_task, default_decoder_factory) +} + +pub fn ImageCacheTask_(resource_task: ResourceTask, decoder_factory: DecoderFactory) -> ImageCacheTask { + // FIXME: Doing some dancing to avoid copying decoder_factory, our test + // version of which contains an uncopyable type which rust will currently + // copy unsoundly + let decoder_factory_cell = Cell(move decoder_factory); + + let (chan, port) = stream(); + let chan = SharedChan(move chan); + let port_cell = Cell(move port); + let chan_cell = Cell(chan.clone()); + + do spawn |move port_cell, move chan_cell, move decoder_factory_cell| { + ImageCache { + resource_task: resource_task, + decoder_factory: decoder_factory_cell.take(), + port: port_cell.take(), + chan: chan_cell.take(), + state_map: url_map(), + wait_map: url_map(), + need_exit: None + }.run(); + } + + move chan +} + +fn SyncImageCacheTask(resource_task: ResourceTask) -> ImageCacheTask { + let (chan, port) = stream(); + let port_cell = Cell(move port); + + do spawn |move port_cell, move resource_task| { + let port = port_cell.take(); + let inner_cache = ImageCacheTask(resource_task); + + loop { + let msg: Msg = port.recv(); + + match move msg { + GetImage(move url, move response) => { + inner_cache.send(WaitForImage(move url, move response)); + } + Exit(move response) => { + inner_cache.send(Exit(move response)); + break; + } + move msg => inner_cache.send(move msg) + } + } + } + + return move SharedChan(move chan); +} + +struct ImageCache { + /// A handle to the resource task for fetching the image binaries + resource_task: ResourceTask, + /// Creates image decoders + decoder_factory: DecoderFactory, + /// The port on which we'll receive client requests + port: Port, + /// A copy of the shared chan to give to child tasks + chan: SharedChan, + /// The state of processsing an image for a URL + state_map: UrlMap, + /// List of clients waiting on a WaitForImage response + wait_map: UrlMap<@mut ~[Chan]>, + mut need_exit: Option>, +} + +enum ImageState { + Init, + Prefetching(AfterPrefetch), + Prefetched(@Cell<~[u8]>), + Decoding, + Decoded(@ARC<~Image>), + Failed +} + +enum AfterPrefetch { + DoDecode, + DoNotDecode +} + +#[allow(non_implicitly_copyable_typarams)] +impl ImageCache { + + pub fn run() { + + let mut msg_handlers: ~[fn~(msg: &Msg)] = ~[]; + + loop { + let msg = self.port.recv(); + + for msg_handlers.each |handler| { (*handler)(&msg) } + + #debug("image_cache_task: received: %?", msg); + + match move msg { + Prefetch(move url) => self.prefetch(move url), + StorePrefetchedImageData(move url, move data) => { + self.store_prefetched_image_data(move url, move data); + } + Decode(move url) => self.decode(move url), + StoreImage(move url, move image) => self.store_image(move url, move image), + GetImage(move url, move response) => self.get_image(move url, move response), + WaitForImage(move url, move response) => { + self.wait_for_image(move url, move response) + } + OnMsg(move handler) => msg_handlers += [move handler], + Exit(move response) => { + assert self.need_exit.is_none(); + self.need_exit = Some(move response); + } + } + + let need_exit = replace(&mut self.need_exit, None); + + match move need_exit { + Some(move response) => { + // Wait until we have no outstanding requests and subtasks + // before exiting + let mut can_exit = true; + for self.state_map.each_value |state| { + match state { + Prefetching(*) => can_exit = false, + Decoding => can_exit = false, + + Init + | Prefetched(*) + | Decoded(*) + | Failed => () + } + } + + if can_exit { + response.send(()); + break; + } else { + self.need_exit = Some(move response); + } + } + None => () + } + } + } + + priv fn get_state(url: Url) -> ImageState { + match move self.state_map.find(move url) { + Some(move state) => move state, + None => Init + } + } + + priv fn set_state(url: Url, state: ImageState) { + self.state_map.insert(move url, move state); + } + + priv fn prefetch(url: Url) { + match self.get_state(copy url) { + Init => { + let to_cache = self.chan.clone(); + let resource_task = self.resource_task; + let url_cell = Cell(copy url); + + do spawn |move to_cache, move url_cell| { + let url = url_cell.take(); + #debug("image_cache_task: started fetch for %s", url.to_str()); + + let image = load_image_data(copy url, resource_task); + + let result = if image.is_ok() { + Ok(Cell(result::unwrap(move image))) + } else { + Err(()) + }; + to_cache.send(StorePrefetchedImageData(copy url, move result)); + #debug("image_cache_task: ended fetch for %s", (copy url).to_str()); + } + + self.set_state(move url, Prefetching(DoNotDecode)); + } + + Prefetching(*) + | Prefetched(*) + | Decoding + | Decoded(*) + | Failed => { + // We've already begun working on this image + } + } + } + + priv fn store_prefetched_image_data(url: Url, data: Result, ()>) { + match self.get_state(copy url) { + Prefetching(next_step) => { + match data { + Ok(data_cell) => { + let data = data_cell.take(); + self.set_state(copy url, Prefetched(@Cell(move data))); + match next_step { + DoDecode => self.decode(move url), + _ => () + } + } + Err(*) => { + self.set_state(copy url, Failed); + self.purge_waiters(move url, || ImageFailed); + } + } + } + + Init + | Prefetched(*) + | Decoding + | Decoded(*) + | Failed => { + fail ~"wrong state for storing prefetched image" + } + } + } + + priv fn decode(url: Url) { + match self.get_state(copy url) { + Init => fail ~"decoding image before prefetch", + + Prefetching(DoNotDecode) => { + // We don't have the data yet, queue up the decode + self.set_state(move url, Prefetching(DoDecode)) + } + + Prefetching(DoDecode) => { + // We don't have the data yet, but the decode request is queued up + } + + Prefetched(data_cell) => { + assert !data_cell.is_empty(); + + let data = data_cell.take(); + let to_cache = self.chan.clone(); + let url_cell = Cell(copy url); + let decode = self.decoder_factory(); + + do spawn |move url_cell, move decode, move data, move to_cache| { + let url = url_cell.take(); + #debug("image_cache_task: started image decode for %s", url.to_str()); + let image = decode(data); + let image = if image.is_some() { + Some(ARC(~option::unwrap(move image))) + } else { + None + }; + to_cache.send(StoreImage(copy url, move image)); + #debug("image_cache_task: ended image decode for %s", url.to_str()); + } + + self.set_state(move url, Decoding); + } + + Decoding | Decoded(*) | Failed => { + // We've already begun decoding + } + } + } + + priv fn store_image(url: Url, image: Option>) { + + match self.get_state(copy url) { + Decoding => { + match image { + Some(image) => { + self.set_state(copy url, Decoded(@clone_arc(&image))); + self.purge_waiters(move url, || ImageReady(clone_arc(&image)) ); + } + None => { + self.set_state(copy url, Failed); + self.purge_waiters(move url, || ImageFailed ); + } + } + } + + Init + | Prefetching(*) + | Prefetched(*) + | Decoded(*) + | Failed => { + fail ~"incorrect state in store_image" + } + } + + } + + priv fn purge_waiters(url: Url, f: fn() -> ImageResponseMsg) { + match self.wait_map.find(copy url) { + Some(@waiters) => { + for waiters.each |response| { + response.send(f()); + } + self.wait_map.remove(move url); + } + None => () + } + } + + + priv fn get_image(url: Url, response: Chan) { + + match self.get_state(copy url) { + Init => fail ~"request for image before prefetch", + + Prefetching(DoDecode) => { + response.send(ImageNotReady); + } + + Prefetching(DoNotDecode) + | Prefetched(*) => fail ~"request for image before decode", + + Decoding => { + response.send(ImageNotReady) + } + + Decoded(image) => { + response.send(ImageReady(clone_arc(image))); + } + + Failed => { + response.send(ImageFailed); + } + } + } + + priv fn wait_for_image(url: Url, response: Chan) { + match self.get_state(copy url) { + Init => fail ~"request for image before prefetch", + + Prefetching(DoNotDecode) | Prefetched(*) => fail ~"request for image before decode", + + Prefetching(DoDecode) | Decoding => { + // We don't have this image yet + match self.wait_map.find(copy url) { + Some(waiters) => { + vec::push(&mut *waiters, move response); + } + None => { + self.wait_map.insert(move url, @mut ~[move response]); + } + } + } + + Decoded(image) => { + response.send(ImageReady(clone_arc(image))); + } + + Failed => { + response.send(ImageFailed); + } + } + } + +} + + +trait ImageCacheTaskClient { + fn exit(); +} + +impl ImageCacheTask: ImageCacheTaskClient { + + fn exit() { + let (response_chan, response_port) = stream(); + self.send(Exit(move response_chan)); + response_port.recv(); + } + +} + +fn load_image_data(url: Url, resource_task: ResourceTask) -> Result<~[u8], ()> { + let response_port = comm::Port(); + resource_task.send(resource_task::Load(move url, response_port.chan())); + + let mut image_data = ~[]; + + loop { + match response_port.recv() { + resource_task::Payload(data) => { + image_data += data; + } + resource_task::Done(result::Ok(*)) => { + return Ok(move image_data); + } + resource_task::Done(result::Err(*)) => { + return Err(()); + } + } + } +} + +fn default_decoder_factory() -> ~fn(&[u8]) -> Option { + fn~(data: &[u8]) -> Option { load_from_memory(data) } +} + +#[cfg(test)] +fn mock_resource_task(on_load: ~fn(resource: comm::Chan)) -> ResourceTask { + do spawn_listener |port: comm::Port, move on_load| { + loop { + match port.recv() { + resource_task::Load(_, response) => { + on_load(response); + } + resource_task::Exit => break + } + } + } +} + +#[test] +fn should_exit_on_request() { + + let mock_resource_task = mock_resource_task(|_response| () ); + + let image_cache_task = ImageCacheTask(mock_resource_task); + let _url = make_url(~"file", None); + + image_cache_task.exit(); + mock_resource_task.send(resource_task::Exit); +} + +#[test] +#[should_fail] +fn should_fail_if_unprefetched_image_is_requested() { + + let mock_resource_task = mock_resource_task(|_response| () ); + + let image_cache_task = ImageCacheTask(mock_resource_task); + let url = make_url(~"file", None); + + let (chan, port) = stream(); + image_cache_task.send(GetImage(move url, move chan)); + port.recv(); +} + +#[test] +fn should_request_url_from_resource_task_on_prefetch() { + let url_requested = comm::Port(); + let url_requested_chan = url_requested.chan(); + + let mock_resource_task = do mock_resource_task |response| { + url_requested_chan.send(()); + response.send(resource_task::Done(result::Ok(()))); + }; + + let image_cache_task = ImageCacheTask(mock_resource_task); + let url = make_url(~"file", None); + + image_cache_task.send(Prefetch(move url)); + url_requested.recv(); + image_cache_task.exit(); + mock_resource_task.send(resource_task::Exit); +} + + +#[test] +#[should_fail] +fn should_fail_if_requesting_decode_of_an_unprefetched_image() { + + let mock_resource_task = mock_resource_task(|_response| () ); + + let image_cache_task = ImageCacheTask(mock_resource_task); + let url = make_url(~"file", None); + + image_cache_task.send(Decode(move url)); + image_cache_task.exit(); +} + +#[test] +#[should_fail] +fn should_fail_if_requesting_image_before_requesting_decode() { + + let mock_resource_task = do mock_resource_task |response| { + response.send(resource_task::Done(result::Ok(()))); + }; + + let image_cache_task = ImageCacheTask(mock_resource_task); + let url = make_url(~"file", None); + + image_cache_task.send(Prefetch(copy url)); + // no decode message + + let (chan, _port) = stream(); + image_cache_task.send(GetImage(move url, move chan)); + + image_cache_task.exit(); + mock_resource_task.send(resource_task::Exit); +} + +#[test] +fn should_not_request_url_from_resource_task_on_multiple_prefetches() { + let url_requested = comm::Port(); + let url_requested_chan = url_requested.chan(); + + let mock_resource_task = do mock_resource_task |response| { + url_requested_chan.send(()); + response.send(resource_task::Done(result::Ok(()))); + }; + + let image_cache_task = ImageCacheTask(mock_resource_task); + let url = make_url(~"file", None); + + image_cache_task.send(Prefetch(copy url)); + image_cache_task.send(Prefetch(move url)); + url_requested.recv(); + image_cache_task.exit(); + mock_resource_task.send(resource_task::Exit); + assert !url_requested.peek() +} + +#[test] +fn should_return_image_not_ready_if_data_has_not_arrived() { + + let (wait_chan, wait_port) = pipes::stream(); + + let mock_resource_task = do mock_resource_task |response, move wait_port| { + // Don't send the data until after the client requests + // the image + wait_port.recv(); + response.send(resource_task::Payload(test_image_bin())); + response.send(resource_task::Done(result::Ok(()))); + }; + + let image_cache_task = ImageCacheTask(mock_resource_task); + let url = make_url(~"file", None); + + image_cache_task.send(Prefetch(copy url)); + image_cache_task.send(Decode(copy url)); + let (response_chan, response_port) = stream(); + image_cache_task.send(GetImage(move url, move response_chan)); + assert response_port.recv() == ImageNotReady; + wait_chan.send(()); + image_cache_task.exit(); + mock_resource_task.send(resource_task::Exit); +} + +#[test] +fn should_return_decoded_image_data_if_data_has_arrived() { + + let mock_resource_task = do mock_resource_task |response| { + response.send(resource_task::Payload(test_image_bin())); + response.send(resource_task::Done(result::Ok(()))); + }; + + let image_cache_task = ImageCacheTask(mock_resource_task); + let url = make_url(~"file", None); + + let wait_for_image = comm::Port(); + let wait_for_image_chan = wait_for_image.chan(); + + image_cache_task.send(OnMsg(|msg| { + match *msg { + StoreImage(*) => wait_for_image_chan.send(()), + _ => () + } + })); + + image_cache_task.send(Prefetch(copy url)); + image_cache_task.send(Decode(copy url)); + + // Wait until our mock resource task has sent the image to the image cache + wait_for_image_chan.recv(); + + let (response_chan, response_port) = stream(); + image_cache_task.send(GetImage(move url, move response_chan)); + match response_port.recv() { + ImageReady(_) => (), + _ => fail + } + + image_cache_task.exit(); + mock_resource_task.send(resource_task::Exit); +} + +#[test] +fn should_return_decoded_image_data_for_multiple_requests() { + + let mock_resource_task = do mock_resource_task |response| { + response.send(resource_task::Payload(test_image_bin())); + response.send(resource_task::Done(result::Ok(()))); + }; + + let image_cache_task = ImageCacheTask(mock_resource_task); + let url = make_url(~"file", None); + + let wait_for_image = comm::Port(); + let wait_for_image_chan = wait_for_image.chan(); + + image_cache_task.send(OnMsg(|msg| { + match *msg { + StoreImage(*) => wait_for_image_chan.send(()), + _ => () + } + })); + + image_cache_task.send(Prefetch(copy url)); + image_cache_task.send(Decode(copy url)); + + // Wait until our mock resource task has sent the image to the image cache + wait_for_image.recv(); + + for iter::repeat(2) { + let (response_chan, response_port) = stream(); + image_cache_task.send(GetImage(copy url, move response_chan)); + match response_port.recv() { + ImageReady(_) => (), + _ => fail + } + } + + image_cache_task.exit(); + mock_resource_task.send(resource_task::Exit); +} + +#[test] +fn should_not_request_image_from_resource_task_if_image_is_already_available() { + + let image_bin_sent = comm::Port(); + let image_bin_sent_chan = image_bin_sent.chan(); + + let resource_task_exited = comm::Port(); + let resource_task_exited_chan = resource_task_exited.chan(); + + let mock_resource_task = do spawn_listener |port: comm::Port| { + loop { + match port.recv() { + resource_task::Load(_, response) => { + response.send(resource_task::Payload(test_image_bin())); + response.send(resource_task::Done(result::Ok(()))); + image_bin_sent_chan.send(()); + } + resource_task::Exit => { + resource_task_exited_chan.send(()); + break + } + } + } + }; + + let image_cache_task = ImageCacheTask(mock_resource_task); + let url = make_url(~"file", None); + + image_cache_task.send(Prefetch(copy url)); + + // Wait until our mock resource task has sent the image to the image cache + image_bin_sent.recv(); + + image_cache_task.send(Prefetch(copy url)); + + image_cache_task.exit(); + mock_resource_task.send(resource_task::Exit); + + resource_task_exited.recv(); + + // Our resource task should not have received another request for the image + // because it's already cached + assert !image_bin_sent.peek(); +} + +#[test] +fn should_not_request_image_from_resource_task_if_image_fetch_already_failed() { + + let image_bin_sent = comm::Port(); + let image_bin_sent_chan = image_bin_sent.chan(); + + let resource_task_exited = comm::Port(); + let resource_task_exited_chan = resource_task_exited.chan(); + + let mock_resource_task = do spawn_listener |port: comm::Port| { + loop { + match port.recv() { + resource_task::Load(_, response) => { + response.send(resource_task::Payload(test_image_bin())); + response.send(resource_task::Done(result::Err(()))); + image_bin_sent_chan.send(()); + } + resource_task::Exit => { + resource_task_exited_chan.send(()); + break + } + } + } + }; + + let image_cache_task = ImageCacheTask(mock_resource_task); + let url = make_url(~"file", None); + + image_cache_task.send(Prefetch(copy url)); + image_cache_task.send(Decode(copy url)); + + // Wait until our mock resource task has sent the image to the image cache + image_bin_sent.recv(); + + image_cache_task.send(Prefetch(copy url)); + image_cache_task.send(Decode(copy url)); + + image_cache_task.exit(); + mock_resource_task.send(resource_task::Exit); + + resource_task_exited.recv(); + + // Our resource task should not have received another request for the image + // because it's already cached + assert !image_bin_sent.peek(); +} + +#[test] +fn should_return_failed_if_image_bin_cannot_be_fetched() { + + let mock_resource_task = do mock_resource_task |response| { + response.send(resource_task::Payload(test_image_bin())); + // ERROR fetching image + response.send(resource_task::Done(result::Err(()))); + }; + + let image_cache_task = ImageCacheTask(mock_resource_task); + let url = make_url(~"file", None); + + let wait_for_prefetech = comm::Port(); + let wait_for_prefetech_chan = wait_for_prefetech.chan(); + + image_cache_task.send(OnMsg(|msg| { + match *msg { + StorePrefetchedImageData(*) => wait_for_prefetech_chan.send(()), + _ => () + } + })); + + image_cache_task.send(Prefetch(copy url)); + image_cache_task.send(Decode(copy url)); + + // Wait until our mock resource task has sent the image to the image cache + wait_for_prefetech.recv(); + + let (response_chan, response_port) = stream(); + image_cache_task.send(GetImage(move url, move response_chan)); + match response_port.recv() { + ImageFailed => (), + _ => fail + } + + image_cache_task.exit(); + mock_resource_task.send(resource_task::Exit); +} + +#[test] +fn should_return_failed_for_multiple_get_image_requests_if_image_bin_cannot_be_fetched() { + + let mock_resource_task = do mock_resource_task |response | { + response.send(resource_task::Payload(test_image_bin())); + // ERROR fetching image + response.send(resource_task::Done(result::Err(()))); + }; + + let image_cache_task = ImageCacheTask(mock_resource_task); + let url = make_url(~"file", None); + + let wait_for_prefetech = comm::Port(); + let wait_for_prefetech_chan = wait_for_prefetech.chan(); + + image_cache_task.send(OnMsg(|msg| { + match *msg { + StorePrefetchedImageData(*) => wait_for_prefetech_chan.send(()), + _ => () + } + })); + + image_cache_task.send(Prefetch(copy url)); + image_cache_task.send(Decode(copy url)); + + // Wait until our mock resource task has sent the image to the image cache + wait_for_prefetech.recv(); + + let (response_chan, response_port) = stream(); + image_cache_task.send(GetImage(copy url, move response_chan)); + match response_port.recv() { + ImageFailed => (), + _ => fail + } + + // And ask again, we should get the same response + let (response_chan, response_port) = stream(); + image_cache_task.send(GetImage(move url, move response_chan)); + match response_port.recv() { + ImageFailed => (), + _ => fail + } + + image_cache_task.exit(); + mock_resource_task.send(resource_task::Exit); +} + +#[test] +fn should_return_not_ready_if_image_is_still_decoding() { + + let (wait_to_decode_chan, wait_to_decode_port) = pipes::stream(); + + let mock_resource_task = do mock_resource_task |response| { + response.send(resource_task::Payload(test_image_bin())); + response.send(resource_task::Done(result::Ok(()))); + }; + + let wait_to_decode_port_cell = Cell(move wait_to_decode_port); + let decoder_factory = fn~(move wait_to_decode_port_cell) -> ~fn(&[u8]) -> Option { + let wait_to_decode_port = wait_to_decode_port_cell.take(); + fn~(data: &[u8], move wait_to_decode_port) -> Option { + // Don't decode until after the client requests the image + wait_to_decode_port.recv(); + load_from_memory(data) + } + }; + + let image_cache_task = ImageCacheTask_(mock_resource_task, move decoder_factory); + let url = make_url(~"file", None); + + let wait_for_prefetech = comm::Port(); + let wait_for_prefetech_chan = wait_for_prefetech.chan(); + + image_cache_task.send(OnMsg(|msg| { + match *msg { + StorePrefetchedImageData(*) => wait_for_prefetech_chan.send(()), + _ => () + } + })); + + image_cache_task.send(Prefetch(copy url)); + image_cache_task.send(Decode(copy url)); + + // Wait until our mock resource task has sent the image to the image cache + wait_for_prefetech.recv(); + + // Make the request + let (response_chan, response_port) = stream(); + image_cache_task.send(GetImage(move url, move response_chan)); + + match response_port.recv() { + ImageNotReady => (), + _ => fail + } + + // Now decode + wait_to_decode_chan.send(()); + + image_cache_task.exit(); + mock_resource_task.send(resource_task::Exit); +} + +#[test] +fn should_return_failed_if_image_decode_fails() { + + let mock_resource_task = do mock_resource_task |response| { + // Bogus data + response.send(resource_task::Payload(~[])); + response.send(resource_task::Done(result::Ok(()))); + }; + + let image_cache_task = ImageCacheTask(mock_resource_task); + let url = make_url(~"file", None); + + let wait_for_decode = comm::Port(); + let wait_for_decode_chan = wait_for_decode.chan(); + + image_cache_task.send(OnMsg(|msg| { + match *msg { + StoreImage(*) => wait_for_decode_chan.send(()), + _ => () + } + })); + + image_cache_task.send(Prefetch(copy url)); + image_cache_task.send(Decode(copy url)); + + // Wait until our mock resource task has sent the image to the image cache + wait_for_decode.recv(); + + // Make the request + let (response_chan, response_port) = stream(); + image_cache_task.send(GetImage(move url, move response_chan)); + + match response_port.recv() { + ImageFailed => (), + _ => fail + } + + image_cache_task.exit(); + mock_resource_task.send(resource_task::Exit); +} + +#[test] +fn should_return_image_on_wait_if_image_is_already_loaded() { + + let mock_resource_task = do mock_resource_task |response| { + response.send(resource_task::Payload(test_image_bin())); + response.send(resource_task::Done(result::Ok(()))); + }; + + let image_cache_task = ImageCacheTask(mock_resource_task); + let url = make_url(~"file", None); + + let wait_for_decode = comm::Port(); + let wait_for_decode_chan = wait_for_decode.chan(); + + image_cache_task.send(OnMsg(|msg| { + match *msg { + StoreImage(*) => wait_for_decode_chan.send(()), + _ => () + } + })); + + image_cache_task.send(Prefetch(copy url)); + image_cache_task.send(Decode(copy url)); + + // Wait until our mock resource task has sent the image to the image cache + wait_for_decode.recv(); + + let (response_chan, response_port) = stream(); + image_cache_task.send(WaitForImage(move url, move response_chan)); + match response_port.recv() { + ImageReady(*) => (), + _ => fail + } + + image_cache_task.exit(); + mock_resource_task.send(resource_task::Exit); +} + +#[test] +fn should_return_image_on_wait_if_image_is_not_yet_loaded() { + + let (wait_chan, wait_port) = pipes::stream(); + + let mock_resource_task = do mock_resource_task |response, move wait_port| { + wait_port.recv(); + response.send(resource_task::Payload(test_image_bin())); + response.send(resource_task::Done(result::Ok(()))); + }; + + let image_cache_task = ImageCacheTask(mock_resource_task); + let url = make_url(~"file", None); + + image_cache_task.send(Prefetch(copy url)); + image_cache_task.send(Decode(copy url)); + + let (response_chan, response_port) = stream(); + image_cache_task.send(WaitForImage(move url, move response_chan)); + + wait_chan.send(()); + + match response_port.recv() { + ImageReady(*) => (), + _ => fail + } + + image_cache_task.exit(); + mock_resource_task.send(resource_task::Exit); +} + +#[test] +fn should_return_image_failed_on_wait_if_image_fails_to_load() { + + let (wait_chan, wait_port) = pipes::stream(); + + let mock_resource_task = do mock_resource_task |response, move wait_port| { + wait_port.recv(); + response.send(resource_task::Payload(test_image_bin())); + response.send(resource_task::Done(result::Err(()))); + }; + + let image_cache_task = ImageCacheTask(mock_resource_task); + let url = make_url(~"file", None); + + image_cache_task.send(Prefetch(copy url)); + image_cache_task.send(Decode(copy url)); + + let (response_chan, response_port) = stream(); + image_cache_task.send(WaitForImage(move url, move response_chan)); + + wait_chan.send(()); + + match response_port.recv() { + ImageFailed => (), + _ => fail + } + + image_cache_task.exit(); + mock_resource_task.send(resource_task::Exit); +} + +#[test] +fn sync_cache_should_wait_for_images() { + let mock_resource_task = do mock_resource_task |response| { + response.send(resource_task::Payload(test_image_bin())); + response.send(resource_task::Done(result::Ok(()))); + }; + + let image_cache_task = SyncImageCacheTask(mock_resource_task); + let url = make_url(~"file", None); + + image_cache_task.send(Prefetch(copy url)); + image_cache_task.send(Decode(copy url)); + + let (response_chan, response_port) = stream(); + image_cache_task.send(GetImage(move url, move response_chan)); + match response_port.recv() { + ImageReady(_) => (), + _ => fail + } + + image_cache_task.exit(); + mock_resource_task.send(resource_task::Exit); +} diff --git a/src/servo-gfx-2/resource/local_image_cache.rs b/src/servo-gfx-2/resource/local_image_cache.rs new file mode 100644 index 00000000000..7dcb82f6ad2 --- /dev/null +++ b/src/servo-gfx-2/resource/local_image_cache.rs @@ -0,0 +1,150 @@ +/*! +An adapter for ImageCacheTask that does local caching to avoid +extra message traffic, it also avoids waiting on the same image +multiple times and thus triggering reflows multiple times. +*/ + +use clone_arc = std::arc::clone; +use std::net::url::Url; +use pipes::{Port, Chan, stream}; +use image_cache_task::{ImageCacheTask, ImageResponseMsg, Prefetch, Decode, GetImage, WaitForImage, ImageReady, ImageNotReady, ImageFailed}; +use util::url::{UrlMap, url_map}; + +pub fn LocalImageCache(image_cache_task: ImageCacheTask) -> LocalImageCache { + LocalImageCache { + image_cache_task: move image_cache_task, + round_number: 1, + mut on_image_available: None, + state_map: url_map() + } +} + +pub struct LocalImageCache { + priv image_cache_task: ImageCacheTask, + priv mut round_number: uint, + priv mut on_image_available: Option<@fn() -> ~fn(ImageResponseMsg)>, + priv state_map: UrlMap<@ImageState> +} + +priv struct ImageState { + mut prefetched: bool, + mut decoded: bool, + mut last_request_round: uint, + mut last_response: ImageResponseMsg +} + +#[allow(non_implicitly_copyable_typarams)] // Using maps of Urls +pub impl LocalImageCache { + /// The local cache will only do a single remote request for a given + /// URL in each 'round'. Layout should call this each time it begins + // FIXME: 'pub' is an unexpected token? + /* pub */ fn next_round(on_image_available: @fn() -> ~fn(ImageResponseMsg)) { + self.round_number += 1; + self.on_image_available = Some(move on_image_available); + } + + pub fn prefetch(url: &Url) { + let state = self.get_state(url); + if !state.prefetched { + self.image_cache_task.send(Prefetch(copy *url)); + state.prefetched = true; + } + } + + pub fn decode(url: &Url) { + let state = self.get_state(url); + if !state.decoded { + self.image_cache_task.send(Decode(copy *url)); + state.decoded = true; + } + } + + // FIXME: Should return a Future + pub fn get_image(url: &Url) -> Port { + let state = self.get_state(url); + + // Save the previous round number for comparison + let last_round = state.last_request_round; + // Set the current round number for this image + state.last_request_round = self.round_number; + + match state.last_response { + ImageReady(ref image) => { + // FIXME: appease borrowck + unsafe { + let (chan, port) = pipes::stream(); + chan.send(ImageReady(clone_arc(image))); + return move port; + } + } + ImageNotReady => { + if last_round == self.round_number { + let (chan, port) = pipes::stream(); + chan.send(ImageNotReady); + return move port; + } else { + // We haven't requested the image from the + // remote cache this round + } + } + ImageFailed => { + let (chan, port) = pipes::stream(); + chan.send(ImageFailed); + return move port; + } + } + + let (response_chan, response_port) = pipes::stream(); + self.image_cache_task.send(GetImage(copy *url, move response_chan)); + + let response = response_port.recv(); + match response { + ImageNotReady => { + // Need to reflow when the image is available + // FIXME: Instead we should be just passing a Future + // to the caller, then to the display list. Finally, + // the compositor should be resonsible for waiting + // on the image to load and triggering layout + let image_cache_task = self.image_cache_task.clone(); + assert self.on_image_available.is_some(); + let on_image_available = self.on_image_available.get()(); + let url = copy *url; + do task::spawn |move url, move on_image_available, move image_cache_task| { + let (response_chan, response_port) = pipes::stream(); + image_cache_task.send(WaitForImage(copy url, move response_chan)); + on_image_available(response_port.recv()); + } + } + _ => () + } + + // Put a copy of the response in the cache + let response_copy = match response { + ImageReady(ref image) => ImageReady(clone_arc(image)), + ImageNotReady => ImageNotReady, + ImageFailed => ImageFailed + }; + state.last_response = move response_copy; + + let (chan, port) = pipes::stream(); + chan.send(move response); + return move port; + } + + priv fn get_state(url: &Url) -> @ImageState { + match self.state_map.find(copy *url) { + Some(state) => state, + None => { + let new_state = @ImageState { + prefetched: false, + decoded: false, + last_request_round: 0, + last_response: ImageNotReady + }; + self.state_map.insert(copy *url, new_state); + self.get_state(url) + } + } + } +} + diff --git a/src/servo-gfx-2/resource/resource_task.rs b/src/servo-gfx-2/resource/resource_task.rs new file mode 100644 index 00000000000..d6a19939c21 --- /dev/null +++ b/src/servo-gfx-2/resource/resource_task.rs @@ -0,0 +1,157 @@ +/*! + +A task that takes a URL and streams back the binary data + +*/ + +use comm::{Chan, Port}; +use task::{spawn, spawn_listener}; +use std::net::url; +use std::net::url::{Url, to_str}; + +pub enum ControlMsg { + /// Request the data associated with a particular URL + Load(Url, Chan), + Exit +} + +/// Messages sent in response to a `Load` message +pub enum ProgressMsg { + /// Binary data - there may be multiple of these + Payload(~[u8]), + /// Indicates loading is complete, either successfully or not + Done(Result<(), ()>) +} + +impl ProgressMsg: cmp::Eq { + pure fn eq(other: &ProgressMsg) -> bool { + match (copy self, copy *other) { + (Payload(a), Payload(b)) => a == b, + (Done(a), Done(b)) => a == b, + + (Payload(*), _) + | (Done(*), _) => false + } + } + pure fn ne(other: &ProgressMsg) -> bool { + return !self.eq(other); + } +} + +/// Handle to a resource task +pub type ResourceTask = Chan; + +/** +Creates a task to load a specific resource + +The ResourceManager delegates loading to a different type of loader task for +each URL scheme +*/ +type LoaderTaskFactory = fn~(url: Url, Chan); + +/// Create a ResourceTask with the default loaders +pub fn ResourceTask() -> ResourceTask { + let loaders = ~[ + (~"file", file_loader::factory), + (~"http", http_loader::factory) + ]; + create_resource_task_with_loaders(move loaders) +} + +fn create_resource_task_with_loaders(loaders: ~[(~str, LoaderTaskFactory)]) -> ResourceTask { + do spawn_listener |from_client, move loaders| { + // TODO: change copy to move once we can move out of closures + ResourceManager(from_client, copy loaders).start() + } +} + +pub struct ResourceManager { + from_client: Port, + /// Per-scheme resource loaders + loaders: ~[(~str, LoaderTaskFactory)], +} + + +pub fn ResourceManager(from_client: Port, + loaders: ~[(~str, LoaderTaskFactory)]) -> ResourceManager { + ResourceManager { + from_client : move from_client, + loaders : move loaders, + } +} + + +impl ResourceManager { + fn start() { + loop { + match self.from_client.recv() { + Load(url, progress_chan) => { + self.load(copy url, progress_chan) + } + Exit => { + break + } + } + } + } + + fn load(url: Url, progress_chan: Chan) { + + match self.get_loader_factory(&url) { + Some(loader_factory) => { + #debug("resource_task: loading url: %s", to_str(copy url)); + loader_factory(move url, progress_chan); + } + None => { + #debug("resource_task: no loader for scheme %s", url.scheme); + progress_chan.send(Done(Err(()))); + } + } + } + + fn get_loader_factory(url: &Url) -> Option { + for self.loaders.each |scheme_loader| { + let (scheme, loader_factory) = copy *scheme_loader; + if scheme == url.scheme { + return Some(move loader_factory); + } + } + return None; + } +} + +#[test] +fn test_exit() { + let resource_task = ResourceTask(); + resource_task.send(Exit); +} + +#[test] +#[allow(non_implicitly_copyable_typarams)] +fn test_bad_scheme() { + let resource_task = ResourceTask(); + let progress = Port(); + resource_task.send(Load(url::from_str(~"bogus://whatever").get(), progress.chan())); + match progress.recv() { + Done(result) => { assert result.is_err() } + _ => fail + } + resource_task.send(Exit); +} + +#[test] +#[allow(non_implicitly_copyable_typarams)] +fn should_delegate_to_scheme_loader() { + let payload = ~[1, 2, 3]; + let loader_factory = fn~(_url: Url, progress_chan: Chan, copy payload) { + progress_chan.send(Payload(copy payload)); + progress_chan.send(Done(Ok(()))); + }; + let loader_factories = ~[(~"snicklefritz", move loader_factory)]; + let resource_task = create_resource_task_with_loaders(move loader_factories); + let progress = Port(); + resource_task.send(Load(url::from_str(~"snicklefritz://heya").get(), progress.chan())); + assert progress.recv() == Payload(move payload); + assert progress.recv() == Done(Ok(())); + resource_task.send(Exit); +} diff --git a/src/servo-gfx-2/servo_gfx.rc b/src/servo-gfx-2/servo_gfx.rc new file mode 100644 index 00000000000..f96d16f2f09 --- /dev/null +++ b/src/servo-gfx-2/servo_gfx.rc @@ -0,0 +1,93 @@ +#[link(name = "servo_gfx", + vers = "0.1", + uuid = "0106bb54-6ea9-45bf-a39e-a738621f15e5", + url = "http://servo.org/")]; +#[crate_type = "lib"]; + +extern mod azure; +extern mod cairo; +extern mod geom; +extern mod http_client; +extern mod stb_image; +extern mod std; + +priv mod render_context; + +// Rendering +pub mod color; +pub mod compositor; +pub mod display_list; +pub mod geometry; +pub mod render_layers; +pub mod render_task; +pub mod surface; + +// Fonts +pub mod font; +pub mod font_context; +pub mod font_list; + +// Misc. +pub mod opts; + +// Pub-uses for multiple implementations. Platform selection happens in +// font.rs, font_list.rs, font_context.rs +pub mod native; + +#[cfg(target_os = "macos")] +pub mod quartz { + pub mod font; + pub mod font_context; + pub mod font_list; +} + +#[cfg(target_os = "linux")] +pub mod freetype { + pub mod font; + pub mod font_context; +} + +#[cfg(target_os = "linux")] +pub mod fontconfig { + pub mod font_list; +} + +// Images +pub mod image { + pub mod base; + pub mod encode { + pub mod tga; + } + pub mod holder; +} + +// Text +pub mod text { + pub mod glyph; + pub mod text_run; + pub mod util; + pub mod shaper; + + // Below are the actual platform-specific parts. + pub mod harfbuzz { + pub mod shaper; + } +} + +// FIXME: Blech. This does not belong in the GFX module. +pub mod resource { + pub mod file_loader; + pub mod http_loader; + pub mod image_cache_task; + pub mod local_image_cache; + pub mod resource_task; +} + +pub mod util { + pub mod cache; + pub mod range; + pub mod time; + pub mod url; + pub mod vec; +} + diff --git a/src/servo-gfx-2/servo_gfx.rs b/src/servo-gfx-2/servo_gfx.rs new file mode 100644 index 00000000000..013bab1a820 --- /dev/null +++ b/src/servo-gfx-2/servo_gfx.rs @@ -0,0 +1,5 @@ +// FIXME: Blech. We need crate-relative imports. +pub use servo_gfx_font = font; +pub use servo_gfx_font_list = font_list; +pub use servo_gfx_util = util; + diff --git a/src/servo-gfx-2/surface.rs b/src/servo-gfx-2/surface.rs new file mode 100644 index 00000000000..be1b5438ce3 --- /dev/null +++ b/src/servo-gfx-2/surface.rs @@ -0,0 +1,40 @@ +use geom::size::Size2D; + +pub enum format { + fo_rgba_8888 + // TODO: RGB 565, others? +} + +impl format: cmp::Eq { + pure fn eq(other: &format) -> bool { + match (self, *other) { + (fo_rgba_8888, fo_rgba_8888) => true, + } + } + pure fn ne(other: &format) -> bool { + return !self.eq(other); + } +} + +pub type image_surface = { + size: Size2D, + format: format, + buffer: ~[u8] +}; + +impl format { + fn bpp() -> uint { + match self { + fo_rgba_8888 => 32u + } + } +} + +pub fn image_surface(size: Size2D, format: format) -> image_surface { + { + size: copy size, + format: format, + buffer: vec::from_elem((size.area() as uint) * format.bpp(), 0u8) + } +} + diff --git a/src/servo-gfx-2/text.rs b/src/servo-gfx-2/text.rs new file mode 100644 index 00000000000..6b3fdd4ff0a --- /dev/null +++ b/src/servo-gfx-2/text.rs @@ -0,0 +1,9 @@ +/* This file exists just to make it easier to import things inside of + ./text/ without specifying the file they came out of imports. + +Note that you still must define each of the files as a module in +servo.rc. This is not ideal and may be changed in the future. */ + +pub use shaper::Shaper; +pub use text_run::TextRun; +pub use text_run::SendableTextRun; diff --git a/src/servo-gfx-2/text/glyph.rs b/src/servo-gfx-2/text/glyph.rs new file mode 100644 index 00000000000..c0dadcb180b --- /dev/null +++ b/src/servo-gfx-2/text/glyph.rs @@ -0,0 +1,624 @@ +use au = geometry; +use au::Au; +use servo_gfx_util::range::Range; +use servo_gfx_util::vec::*; + +use core::cmp::{Ord, Eq}; +use core::dvec::DVec; +use core::num::from_int; +use core::u16; +use geom::point::Point2D; +use std::sort; + + +// GlyphEntry is a port of Gecko's CompressedGlyph scheme for storing +// glyph data compactly. +// +// In the common case (reasonable glyph advances, no offsets from the +// font em-box, and one glyph per character), we pack glyph advance, +// glyph id, and some flags into a single u32. +// +// In the uncommon case (multiple glyphs per unicode character, large +// glyph index/advance, or glyph offsets), we pack the glyph count +// into GlyphEntry, and store the other glyph information in +// DetailedGlyphStore. +struct GlyphEntry { + value : u32 +} + +pure fn GlyphEntry(value: u32) -> GlyphEntry { GlyphEntry { value: value } } + +/// The index of a particular glyph within a font +type GlyphIndex = u32; + +// TODO: unify with bit flags? +enum BreakType { + BreakTypeNone, + BreakTypeNormal, + BreakTypeHyphen +} + +const BREAK_TYPE_NONE : u8 = 0x0u8; +const BREAK_TYPE_NORMAL : u8 = 0x1u8; +const BREAK_TYPE_HYPHEN : u8 = 0x2u8; + +pure fn break_flag_to_enum(flag: u8) -> BreakType { + if (flag & BREAK_TYPE_NONE) as bool { return BreakTypeNone; } + if (flag & BREAK_TYPE_NORMAL) as bool { return BreakTypeNormal; } + if (flag & BREAK_TYPE_HYPHEN) as bool { return BreakTypeHyphen; } + fail ~"Unknown break setting" +} + +pure fn break_enum_to_flag(e: BreakType) -> u8 { + match e { + BreakTypeNone => BREAK_TYPE_NONE, + BreakTypeNormal => BREAK_TYPE_NORMAL, + BreakTypeHyphen => BREAK_TYPE_HYPHEN, + } +} + +// TODO: make this more type-safe. + +const FLAG_CHAR_IS_SPACE : u32 = 0x10000000u32; +// These two bits store some BREAK_TYPE_* flags +const FLAG_CAN_BREAK_MASK : u32 = 0x60000000u32; +const FLAG_CAN_BREAK_SHIFT : u32 = 29; +const FLAG_IS_SIMPLE_GLYPH : u32 = 0x80000000u32; + +// glyph advance; in Au's. +const GLYPH_ADVANCE_MASK : u32 = 0x0FFF0000u32; +const GLYPH_ADVANCE_SHIFT : u32 = 16; +const GLYPH_ID_MASK : u32 = 0x0000FFFFu32; + +// Non-simple glyphs (more than one glyph per char; missing glyph, +// newline, tab, large advance, or nonzero x/y offsets) may have one +// or more detailed glyphs associated with them. They are stored in a +// side array so that there is a 1:1 mapping of GlyphEntry to +// unicode char. + +// The number of detailed glyphs for this char. If the char couldn't +// be mapped to a glyph (!FLAG_NOT_MISSING), then this actually holds +// the UTF8 code point instead. +const GLYPH_COUNT_MASK : u32 = 0x00FFFF00u32; +const GLYPH_COUNT_SHIFT : u32 = 8; +// N.B. following Gecko, these are all inverted so that a lot of +// missing chars can be memset with zeros in one fell swoop. +const FLAG_NOT_MISSING : u32 = 0x00000001u32; +const FLAG_NOT_CLUSTER_START : u32 = 0x00000002u32; +const FLAG_NOT_LIGATURE_GROUP_START : u32 = 0x00000004u32; + +const FLAG_CHAR_IS_TAB : u32 = 0x00000008u32; +const FLAG_CHAR_IS_NEWLINE : u32 = 0x00000010u32; +const FLAG_CHAR_IS_LOW_SURROGATE : u32 = 0x00000020u32; +const CHAR_IDENTITY_FLAGS_MASK : u32 = 0x00000038u32; + +pure fn is_simple_glyph_id(glyphId: GlyphIndex) -> bool { + ((glyphId as u32) & GLYPH_ID_MASK) == glyphId +} + +pure fn is_simple_advance(advance: Au) -> bool { + let unsignedAu = advance.to_int() as u32; + (unsignedAu & (GLYPH_ADVANCE_MASK >> GLYPH_ADVANCE_SHIFT)) == unsignedAu +} + +type DetailedGlyphCount = u16; + +pure fn InitialGlyphEntry() -> GlyphEntry { + GlyphEntry { value: 0 } +} + +// Creates a GlyphEntry for the common case +pure fn SimpleGlyphEntry(index: GlyphIndex, advance: Au) -> GlyphEntry { + assert is_simple_glyph_id(index); + assert is_simple_advance(advance); + + let index_mask = index as u32; + let advance_mask = (*advance as u32) << GLYPH_ADVANCE_SHIFT; + + GlyphEntry { + value: index_mask | advance_mask | FLAG_IS_SIMPLE_GLYPH + } +} + +// Create a GlyphEntry for uncommon case; should be accompanied by +// initialization of the actual DetailedGlyph data in DetailedGlyphStore +pure fn ComplexGlyphEntry(startsCluster: bool, startsLigature: bool, glyphCount: uint) -> GlyphEntry { + assert glyphCount <= u16::max_value as uint; + + let mut val = FLAG_NOT_MISSING; + + if !startsCluster { + val |= FLAG_NOT_CLUSTER_START; + } + if !startsLigature { + val |= FLAG_NOT_LIGATURE_GROUP_START; + } + val |= (glyphCount as u32) << GLYPH_COUNT_SHIFT; + + GlyphEntry { + value: val + } +} + +// Create a GlyphEntry for the case where glyphs couldn't be found +// for the specified character. +pure fn MissingGlyphsEntry(glyphCount: uint) -> GlyphEntry { + assert glyphCount <= u16::max_value as uint; + + GlyphEntry { + value: (glyphCount as u32) << GLYPH_COUNT_SHIFT + } +} + +// Getters and setters for GlyphEntry. Setter methods are functional, +// because GlyphEntry is immutable and only a u32 in size. +impl GlyphEntry { + // getter methods + pure fn advance() -> Au { + assert self.is_simple(); + from_int(((self.value & GLYPH_ADVANCE_MASK) >> GLYPH_ADVANCE_SHIFT) as int) + } + + pure fn index() -> GlyphIndex { + assert self.is_simple(); + self.value & GLYPH_ID_MASK + } + + pure fn offset() -> Point2D { + assert self.is_simple(); + Point2D(Au(0), Au(0)) + } + + pure fn is_ligature_start() -> bool { + self.has_flag(!FLAG_NOT_LIGATURE_GROUP_START) + } + + pure fn is_cluster_start() -> bool { + self.has_flag(!FLAG_NOT_CLUSTER_START) + } + + // True if original char was normal (U+0020) space. Other chars may + // map to space glyph, but this does not account for them. + pure fn char_is_space() -> bool { + self.has_flag(FLAG_CHAR_IS_SPACE) + } + + pure fn char_is_tab() -> bool { + !self.is_simple() && self.has_flag(FLAG_CHAR_IS_TAB) + } + + pure fn char_is_newline() -> bool { + !self.is_simple() && self.has_flag(FLAG_CHAR_IS_NEWLINE) + } + + pure fn can_break_before() -> BreakType { + let flag = ((self.value & FLAG_CAN_BREAK_MASK) >> FLAG_CAN_BREAK_SHIFT) as u8; + break_flag_to_enum(flag) + } + + // setter methods + pure fn set_char_is_space() -> GlyphEntry { + GlyphEntry(self.value | FLAG_CHAR_IS_SPACE) + } + + pure fn set_char_is_tab() -> GlyphEntry { + assert !self.is_simple(); + GlyphEntry(self.value | FLAG_CHAR_IS_TAB) + } + + pure fn set_char_is_newline() -> GlyphEntry { + assert !self.is_simple(); + GlyphEntry(self.value | FLAG_CHAR_IS_NEWLINE) + } + + // returns a glyph entry only if the setting had changed. + pure fn set_can_break_before(e: BreakType) -> Option { + let flag = break_enum_to_flag(e); + let mask = (flag as u32) << FLAG_CAN_BREAK_SHIFT; + let toggle = mask ^ (self.value & FLAG_CAN_BREAK_MASK); + + match (toggle as bool) { + true => Some(GlyphEntry(self.value ^ toggle)), + false => None + } + } + + // helper methods + + /*priv*/ pure fn glyph_count() -> u16 { + assert !self.is_simple(); + ((self.value & GLYPH_COUNT_MASK) >> GLYPH_COUNT_SHIFT) as u16 + } + + pure fn is_simple() -> bool { + self.has_flag(FLAG_IS_SIMPLE_GLYPH) + } + + /*priv*/ pure fn has_flag(flag: u32) -> bool { + (self.value & flag) != 0 + } +} + +// Stores data for a detailed glyph, in the case that several glyphs +// correspond to one character, or the glyph's data couldn't be packed. +struct DetailedGlyph { + index: GlyphIndex, + // glyph's advance, in the text's direction (RTL or RTL) + advance: Au, + // glyph's offset from the font's em-box (from top-left) + offset: Point2D +} + + +fn DetailedGlyph(index: GlyphIndex, + advance: Au, offset: Point2D) -> DetailedGlyph { + DetailedGlyph { + index: index, + advance: advance, + offset: offset + } +} + +struct DetailedGlyphRecord { + // source string offset/GlyphEntry offset in the TextRun + entry_offset: uint, + // offset into the detailed glyphs buffer + detail_offset: uint +} + +impl DetailedGlyphRecord : Ord { + pure fn lt(other: &DetailedGlyphRecord) -> bool { self.entry_offset < other.entry_offset } + pure fn le(other: &DetailedGlyphRecord) -> bool { self.entry_offset <= other.entry_offset } + pure fn ge(other: &DetailedGlyphRecord) -> bool { self.entry_offset >= other.entry_offset } + pure fn gt(other: &DetailedGlyphRecord) -> bool { self.entry_offset > other.entry_offset } +} + +impl DetailedGlyphRecord : Eq { + pure fn eq(other : &DetailedGlyphRecord) -> bool { self.entry_offset == other.entry_offset } + pure fn ne(other : &DetailedGlyphRecord) -> bool { self.entry_offset != other.entry_offset } +} + +// Manages the lookup table for detailed glyphs. Sorting is deferred +// until a lookup is actually performed; this matches the expected +// usage pattern of setting/appending all the detailed glyphs, and +// then querying without setting. +struct DetailedGlyphStore { + detail_buffer: DVec, + detail_lookup: DVec, + mut lookup_is_sorted: bool, +} + +fn DetailedGlyphStore() -> DetailedGlyphStore { + DetailedGlyphStore { + detail_buffer: DVec(), + detail_lookup: DVec(), + lookup_is_sorted: false + } +} + +impl DetailedGlyphStore { + fn add_detailed_glyphs_for_entry(entry_offset: uint, glyphs: &[DetailedGlyph]) { + let entry = DetailedGlyphRecord { + entry_offset: entry_offset, + detail_offset: self.detail_buffer.len() + }; + + /* TODO: don't actually assert this until asserts are compiled + in/out based on severity, debug/release, etc. This assertion + would wreck the complexity of the lookup. + + See Rust Issue #3647, #2228, #3627 for related information. + + do self.detail_lookup.borrow |arr| { + assert !arr.contains(entry) + } + */ + + self.detail_lookup.push(entry); + self.detail_buffer.push_all(glyphs); + self.lookup_is_sorted = false; + } + + // not pure; may perform a deferred sort. + fn get_detailed_glyphs_for_entry(&self, entry_offset: uint, count: u16) -> &[DetailedGlyph] { + assert count > 0; + assert (count as uint) <= self.detail_buffer.len(); + self.ensure_sorted(); + + let key = DetailedGlyphRecord { + entry_offset: entry_offset, + detail_offset: 0 // unused + }; + + do self.detail_lookup.borrow |records : &[DetailedGlyphRecord]| { + match records.binary_search_index(&key) { + None => fail ~"Invalid index not found in detailed glyph lookup table!", + Some(i) => { + do self.detail_buffer.borrow |glyphs : &[DetailedGlyph]| { + assert i + (count as uint) <= glyphs.len(); + // return a view into the buffer + vec::view(glyphs, i, i + count as uint) + } + } + } + } + } + + fn get_detailed_glyph_with_index(&self, entry_offset: uint, detail_offset: u16) -> &DetailedGlyph { + assert (detail_offset as uint) <= self.detail_buffer.len(); + self.ensure_sorted(); + + let key = DetailedGlyphRecord { + entry_offset: entry_offset, + detail_offset: 0 // unused + }; + + do self.detail_lookup.borrow |records : &[DetailedGlyphRecord]| { + match records.binary_search_index(&key) { + None => fail ~"Invalid index not found in detailed glyph lookup table!", + Some(i) => { + do self.detail_buffer.borrow |glyphs : &[DetailedGlyph]| { + assert i + (detail_offset as uint) < glyphs.len(); + &glyphs[i+(detail_offset as uint)] + } + } + } + } + } + + /*priv*/ fn ensure_sorted() { + if self.lookup_is_sorted { + return; + } + + do self.detail_lookup.borrow_mut |arr| { + sort::quick_sort3(arr); + }; + self.lookup_is_sorted = true; + } +} + +// This struct is used by GlyphStore clients to provide new glyph data. +// It should be allocated on the stack and passed by reference to GlyphStore. +struct GlyphData { + index: GlyphIndex, + advance: Au, + offset: Point2D, + is_missing: bool, + cluster_start: bool, + ligature_start: bool, +} + +pure fn GlyphData(index: GlyphIndex, + advance: Au, + offset: Option>, + is_missing: bool, + cluster_start: bool, + ligature_start: bool) -> GlyphData { + + let _offset = match offset { + None => au::zero_point(), + Some(o) => o + }; + + GlyphData { + index: index, + advance: advance, + offset: _offset, + is_missing: is_missing, + cluster_start: cluster_start, + ligature_start: ligature_start, + } +} + +// This enum is a proxy that's provided to GlyphStore clients when iterating +// through glyphs (either for a particular TextRun offset, or all glyphs). +// Rather than eagerly assembling and copying glyph data, it only retrieves +// values as they are needed from the GlyphStore, using provided offsets. +enum GlyphInfo { + SimpleGlyphInfo(&GlyphStore, uint), + DetailGlyphInfo(&GlyphStore, uint, u16) +} + +impl GlyphInfo { + fn index() -> GlyphIndex { + match self { + SimpleGlyphInfo(store, entry_i) => store.entry_buffer[entry_i].index(), + DetailGlyphInfo(store, entry_i, detail_j) => store.detail_store.get_detailed_glyph_with_index(entry_i, detail_j).index + } + } + + fn advance() -> Au { + match self { + SimpleGlyphInfo(store, entry_i) => store.entry_buffer[entry_i].advance(), + DetailGlyphInfo(store, entry_i, detail_j) => store.detail_store.get_detailed_glyph_with_index(entry_i, detail_j).advance + } + } + + fn offset() -> Option> { + match self { + SimpleGlyphInfo(_, _) => None, + DetailGlyphInfo(store, entry_i, detail_j) => Some(store.detail_store.get_detailed_glyph_with_index(entry_i, detail_j).offset) + } + } + + fn is_ligature_start() -> bool { + match self { + SimpleGlyphInfo(store, entry_i) => store.entry_buffer[entry_i].is_ligature_start(), + DetailGlyphInfo(store, entry_i, _) => store.entry_buffer[entry_i].is_ligature_start() + } + } + + fn is_cluster_start() -> bool { + match self { + SimpleGlyphInfo(store, entry_i) => store.entry_buffer[entry_i].is_cluster_start(), + DetailGlyphInfo(store, entry_i, _) => store.entry_buffer[entry_i].is_cluster_start() + } + } +} + +// Public data structure and API for storing and retrieving glyph data +struct GlyphStore { + // we use a DVec here instead of a mut vec, since this is much safer. + entry_buffer: DVec, + detail_store: DetailedGlyphStore, +} + +// Initializes the glyph store, but doesn't actually shape anything. +// Use the set_glyph, set_glyphs() methods to store glyph data. +fn GlyphStore(length: uint) -> GlyphStore { + assert length > 0; + + let buffer = vec::from_elem(length, InitialGlyphEntry()); + + GlyphStore { + entry_buffer: dvec::from_vec(move buffer), + detail_store: DetailedGlyphStore(), + } +} + +impl GlyphStore { + fn add_glyph_for_index(i: uint, data: &GlyphData) { + + pure fn glyph_is_compressible(data: &GlyphData) -> bool { + is_simple_glyph_id(data.index) + && is_simple_advance(data.advance) + && data.offset == au::zero_point() + } + + assert i < self.entry_buffer.len(); + + let entry = match (data.is_missing, glyph_is_compressible(data)) { + (true, _) => MissingGlyphsEntry(1), + (false, true) => { SimpleGlyphEntry(data.index, data.advance) }, + (false, false) => { + let glyph = [DetailedGlyph(data.index, data.advance, data.offset)]; + self.detail_store.add_detailed_glyphs_for_entry(i, glyph); + ComplexGlyphEntry(data.cluster_start, data.ligature_start, 1) + } + }; + + self.entry_buffer.set_elt(i, entry); + } + + fn add_glyphs_for_index(i: uint, data_for_glyphs: &[GlyphData]) { + assert i < self.entry_buffer.len(); + assert data_for_glyphs.len() > 0; + + let glyph_count = data_for_glyphs.len(); + + let first_glyph_data = data_for_glyphs[0]; + let entry = match first_glyph_data.is_missing { + true => MissingGlyphsEntry(glyph_count), + false => { + let glyphs_vec = vec::from_fn(glyph_count, |i| { + DetailedGlyph(data_for_glyphs[i].index, + data_for_glyphs[i].advance, + data_for_glyphs[i].offset) + }); + + self.detail_store.add_detailed_glyphs_for_entry(i, glyphs_vec); + ComplexGlyphEntry(first_glyph_data.cluster_start, + first_glyph_data.ligature_start, + glyph_count) + } + }; + + self.entry_buffer.set_elt(i, entry); + } + + fn iter_glyphs_for_index(&self, i: uint, cb: fn&(uint, GlyphInfo/&) -> bool) -> bool { + assert i < self.entry_buffer.len(); + + let entry = &self.entry_buffer[i]; + match entry.is_simple() { + true => { + let proxy = SimpleGlyphInfo(self, i); + if !cb(i, proxy) { return false; } + }, + false => { + let glyphs = self.detail_store.get_detailed_glyphs_for_entry(i, entry.glyph_count()); + for uint::range(0, glyphs.len()) |j| { + let proxy = DetailGlyphInfo(self, i, j as u16); + cb(i, proxy); + } + } + } + + return true; + } + + fn iter_glyphs_for_range(&self, range: Range, cb: fn&(uint, GlyphInfo/&) -> bool) { + assert range.begin() < self.entry_buffer.len(); + assert range.end() <= self.entry_buffer.len(); + + for range.eachi |i| { + if !self.iter_glyphs_for_index(i, cb) { break; } + } + } + + fn iter_all_glyphs(cb: fn&(uint, GlyphInfo/&) -> bool) { + for uint::range(0, self.entry_buffer.len()) |i| { + if !self.iter_glyphs_for_index(i, cb) { break; } + } + } + + // getter methods + fn char_is_space(i: uint) -> bool { + assert i < self.entry_buffer.len(); + self.entry_buffer[i].char_is_space() + } + + fn char_is_tab(i: uint) -> bool { + assert i < self.entry_buffer.len(); + self.entry_buffer[i].char_is_tab() + } + + fn char_is_newline(i: uint) -> bool { + assert i < self.entry_buffer.len(); + self.entry_buffer[i].char_is_newline() + } + + fn is_ligature_start(i: uint) -> bool { + assert i < self.entry_buffer.len(); + self.entry_buffer[i].is_ligature_start() + } + + fn is_cluster_start(i: uint) -> bool { + assert i < self.entry_buffer.len(); + self.entry_buffer[i].is_cluster_start() + } + + fn can_break_before(i: uint) -> BreakType { + assert i < self.entry_buffer.len(); + self.entry_buffer[i].can_break_before() + } + + // setter methods + fn set_char_is_space(i: uint) { + assert i < self.entry_buffer.len(); + let entry = self.entry_buffer[i]; + self.entry_buffer.set_elt(i, entry.set_char_is_space()) + } + + fn set_char_is_tab(i: uint) { + assert i < self.entry_buffer.len(); + let entry = self.entry_buffer[i]; + self.entry_buffer.set_elt(i, entry.set_char_is_tab()) + } + + fn set_char_is_newline(i: uint) { + assert i < self.entry_buffer.len(); + let entry = self.entry_buffer[i]; + self.entry_buffer.set_elt(i, entry.set_char_is_newline()) + } + + fn set_can_break_before(i: uint, t: BreakType) { + assert i < self.entry_buffer.len(); + let entry = self.entry_buffer[i]; + match entry.set_can_break_before(t) { + Some(e) => self.entry_buffer.set_elt(i, e), + None => {} + }; + } +} diff --git a/src/servo-gfx-2/text/harfbuzz/shaper.rs b/src/servo-gfx-2/text/harfbuzz/shaper.rs new file mode 100644 index 00000000000..16944f05a52 --- /dev/null +++ b/src/servo-gfx-2/text/harfbuzz/shaper.rs @@ -0,0 +1,177 @@ +extern mod harfbuzz; + +use geometry::Au; +use glyph::{GlyphStore, GlyphIndex, GlyphData}; +use servo_gfx_font::Font; + +use core::libc::types::common::c99::int32_t; +use core::libc::{c_uint, c_int, c_void, c_char}; +use core::ptr::{null, to_unsafe_ptr, offset}; +use geom::Point2D; +use harfbuzz::{HB_MEMORY_MODE_READONLY, HB_DIRECTION_LTR, hb_blob_t, hb_face_t, hb_font_t}; +use harfbuzz::{hb_font_funcs_t, hb_buffer_t, hb_codepoint_t, hb_bool_t, hb_glyph_position_t}; +use harfbuzz::{hb_glyph_info_t, hb_var_int_t, hb_position_t}; +use harfbuzz::bindgen::{hb_blob_create, hb_blob_destroy, hb_face_create, hb_face_destroy}; +use harfbuzz::bindgen::{hb_font_create, hb_font_destroy, hb_buffer_create, hb_buffer_destroy}; +use harfbuzz::bindgen::{hb_buffer_add_utf8, hb_shape, hb_buffer_get_glyph_infos}; +use harfbuzz::bindgen::{hb_buffer_get_glyph_positions, hb_font_set_ppem, hb_font_set_scale}; +use harfbuzz::bindgen::{hb_buffer_set_direction, hb_font_funcs_create, hb_font_funcs_destroy}; +use harfbuzz::bindgen::{hb_font_set_funcs, hb_font_funcs_set_glyph_h_advance_func}; +use harfbuzz::bindgen::{hb_font_funcs_set_glyph_func, hb_font_funcs_set_glyph_h_kerning_func}; +use std::arc; + +pub struct HarfbuzzShaper { + priv font: @Font, + priv hb_blob: *hb_blob_t, + priv hb_face: *hb_face_t, + priv hb_font: *hb_font_t, + priv hb_funcs: *hb_font_funcs_t, + + drop { + assert self.hb_blob.is_not_null(); + hb_blob_destroy(self.hb_blob); + + assert self.hb_face.is_not_null(); + hb_face_destroy(self.hb_face); + + assert self.hb_font.is_not_null(); + hb_font_destroy(self.hb_font); + + assert self.hb_funcs.is_not_null(); + hb_font_funcs_destroy(self.hb_funcs); + } +} + +pub impl HarfbuzzShaper { + static pub fn new(font: @Font) -> HarfbuzzShaper { + // TODO(Issue #92): font tables should be stored in Font object and cached per-task + let hb_blob: *hb_blob_t = vec::as_imm_buf(*(font).buf(), |buf: *u8, len: uint| { + hb_blob_create(buf as *c_char, + len as c_uint, + HB_MEMORY_MODE_READONLY, + null(), + null()) + }); + + let hb_face: *hb_face_t = hb_face_create(hb_blob, 0 as c_uint); + let hb_font: *hb_font_t = hb_font_create(hb_face); + // Set points-per-em. if zero, performs no hinting in that direction. + let pt_size = font.style.pt_size; + hb_font_set_ppem(hb_font, pt_size as c_uint, pt_size as c_uint); + // Set scaling. Note that this takes 16.16 fixed point. + hb_font_set_scale(hb_font, + HarfbuzzShaper::float_to_fixed(pt_size) as c_int, + HarfbuzzShaper::float_to_fixed(pt_size) as c_int); + + // configure static function callbacks. + // NB. This funcs structure could be reused globally, as it never changes. + let hb_funcs: *hb_font_funcs_t = hb_font_funcs_create(); + hb_font_funcs_set_glyph_func(hb_funcs, glyph_func, null(), null()); + hb_font_funcs_set_glyph_h_advance_func(hb_funcs, glyph_h_advance_func, null(), null()); + unsafe { + let font_data: *c_void = core::ptr::addr_of(font) as *c_void; + hb_font_set_funcs(hb_font, hb_funcs, font_data, null()); + }; + + HarfbuzzShaper { + font: font, + hb_blob: hb_blob, + hb_face: hb_face, + hb_font: hb_font, + hb_funcs: hb_funcs, + } + } + + /** + Calculate the layout metrics associated with a some given text + when rendered in a specific font. + */ + pub fn shape_text(text: &str, glyphs: &GlyphStore) { + debug!("shaping text '%s'", text); + + // TODO(Issue #94): harfbuzz fonts and faces should be cached on the + // Shaper object, which is owned by the Font instance. + + let hb_buffer: *hb_buffer_t = hb_buffer_create(); + hb_buffer_set_direction(hb_buffer, HB_DIRECTION_LTR); + + // Using as_buf because it never does a copy - we don't need the trailing null + str::as_buf(text, |ctext: *u8, _l: uint| { + hb_buffer_add_utf8(hb_buffer, + ctext as *c_char, + text.len() as c_int, + 0 as c_uint, + text.len() as c_int); + }); + + hb_shape(self.hb_font, hb_buffer, null(), 0 as c_uint); + + let info_buf_len = 0 as c_uint; + let info_buf = hb_buffer_get_glyph_infos(hb_buffer, to_unsafe_ptr(&info_buf_len)); + assert info_buf.is_not_null(); + let pos_buf_len = 0 as c_uint; + let pos_buf = hb_buffer_get_glyph_positions(hb_buffer, to_unsafe_ptr(&pos_buf_len)); + assert pos_buf.is_not_null(); + + assert info_buf_len == pos_buf_len; + + for uint::range(0u, info_buf_len as uint) |i| { unsafe { + let hb_info: hb_glyph_info_t = *offset(info_buf, i); + let hb_pos: hb_glyph_position_t = *offset(pos_buf, i); + let codepoint = hb_info.codepoint as GlyphIndex; + let advance: Au = Au::from_frac_px(HarfbuzzShaper::fixed_to_float(hb_pos.x_advance)); + let offset = match (hb_pos.x_offset, hb_pos.y_offset) { + (0, 0) => None, + (x, y) => Some(Point2D(Au::from_frac_px(HarfbuzzShaper::fixed_to_float(x)), + Au::from_frac_px(HarfbuzzShaper::fixed_to_float(y)))) + }; + // TODO: convert pos.y_advance into offset adjustment + // TODO: handle multiple glyphs per char, ligatures, etc. + // NB. this debug statement is commented out, as it must be checked for every shaped char. + //debug!("glyph %?: index %?, advance %?, offset %?", i, codepoint, advance, offset); + + let data = GlyphData(codepoint, advance, offset, false, false, false); + glyphs.add_glyph_for_index(i, &data); + } /* unsafe */ } + + hb_buffer_destroy(hb_buffer); + } + + static priv fn float_to_fixed(f: float) -> i32 { + util::float_to_fixed(16, f) + } + + static priv fn fixed_to_float(i: hb_position_t) -> float { + util::fixed_to_float(16, i) + } + + static priv fn fixed_to_rounded_int(f: hb_position_t) -> int { + util::fixed_to_rounded_int(16, f) + } +} + +/// Callbacks from Harfbuzz when font map and glyph advance lookup needed. +extern fn glyph_func(_font: *hb_font_t, + font_data: *c_void, + unicode: hb_codepoint_t, + _variant_selector: hb_codepoint_t, + glyph: *mut hb_codepoint_t, + _user_data: *c_void) -> hb_bool_t unsafe { + let font: *Font = font_data as *Font; + assert font.is_not_null(); + return match (*font).glyph_index(unicode as char) { + Some(g) => { *glyph = g as hb_codepoint_t; true }, + None => false + } as hb_bool_t; +} + +extern fn glyph_h_advance_func(_font: *hb_font_t, + font_data: *c_void, + glyph: hb_codepoint_t, + _user_data: *c_void) -> hb_position_t unsafe { + let font: *Font = font_data as *Font; + assert font.is_not_null(); + + let advance = (*font).glyph_h_advance(glyph as GlyphIndex); + HarfbuzzShaper::float_to_fixed(advance) +} diff --git a/src/servo-gfx-2/text/shaper.rs b/src/servo-gfx-2/text/shaper.rs new file mode 100644 index 00000000000..76f71704cff --- /dev/null +++ b/src/servo-gfx-2/text/shaper.rs @@ -0,0 +1,17 @@ +/** +Shaper encapsulates a specific shaper, such as Harfbuzz, +Uniscribe, Pango, or Coretext. + +Currently, only harfbuzz bindings are implemented. +*/ +use servo_gfx_font::Font; + +pub type Shaper/& = harfbuzz::shaper::HarfbuzzShaper; + +// TODO(Issue #163): this is a workaround for static methods and +// typedefs not working well together. It should be removed. +impl Shaper { + static pub fn new(font: @Font) -> Shaper { + harfbuzz::shaper::HarfbuzzShaper::new(font) + } +} diff --git a/src/servo-gfx-2/text/text_run.rs b/src/servo-gfx-2/text/text_run.rs new file mode 100644 index 00000000000..3ef6c150d3b --- /dev/null +++ b/src/servo-gfx-2/text/text_run.rs @@ -0,0 +1,203 @@ +use font_context::FontContext; +use geometry::Au; +use glyph::GlyphStore; +use servo_gfx_font::{Font, FontDescriptor, RunMetrics}; +use servo_gfx_util::range::{Range, MutableRange}; + +use core::libc::{c_void}; +use geom::point::Point2D; +use geom::size::Size2D; +use std::arc; +use std::arc::ARC; + +pub struct TextRun { + text: ~str, + font: @Font, + priv glyphs: GlyphStore, +} + +// This is a hack until TextRuns are normally sendable, or +// we instead use ARC everywhere. +pub struct SendableTextRun { + text: ~str, + font: FontDescriptor, + priv glyphs: GlyphStore, +} + +impl SendableTextRun { + pub fn deserialize(&self, fctx: @FontContext) -> TextRun { + let font = match fctx.get_font_by_descriptor(&self.font) { + Ok(f) => f, + Err(_) => fail fmt!("Font descriptor deserialization failed! desc=%?", self.font) + }; + + TextRun { + text: copy self.text, + font: font, + glyphs: copy self.glyphs + } + } +} + +impl TextRun { + static fn new(font: @Font, text: ~str) -> TextRun { + let glyph_store = font.shape_text(text); + let run = TextRun { + text: move text, + font: font, + glyphs: move glyph_store, + }; + return move run; + } + + pub fn serialize(&self) -> SendableTextRun { + SendableTextRun { + text: copy self.text, + font: self.font.get_descriptor(), + glyphs: copy self.glyphs, + } + } + + pure fn glyphs(&self) -> &self/GlyphStore { &self.glyphs } + + pure fn range_is_trimmable_whitespace(&self, range: Range) -> bool { + let mut i = range.begin(); + while i < range.end() { + // jump i to each new char + let {ch, next} = str::char_range_at(self.text, i); + match ch { + ' ' | '\t' | '\r' => {}, + _ => { return false; } + } + i = next; + } + return true; + } + + fn metrics_for_range(&self, range: Range) -> RunMetrics { + self.font.measure_text(self, range) + } + + fn min_width_for_range(&self, range: Range) -> Au { + assert range.is_valid_for_string(self.text); + + let mut max_piece_width = Au(0); + for self.iter_indivisible_pieces_for_range(range) |piece_range| { + let metrics = self.font.measure_text(self, piece_range); + max_piece_width = Au::max(max_piece_width, metrics.advance_width); + } + return max_piece_width; + } + + fn iter_natural_lines_for_range(&self, range: Range, f: fn(Range) -> bool) { + assert range.is_valid_for_string(self.text); + + let clump = MutableRange::new(range.begin(), 0); + let mut in_clump = false; + + // clump non-linebreaks of nonzero length + for range.eachi |i| { + match (self.glyphs.char_is_newline(i), in_clump) { + (false, true) => { clump.extend_by(1); } + (false, false) => { in_clump = true; clump.reset(i, 1); } + (true, false) => { /* chomp whitespace */ } + (true, true) => { + in_clump = false; + // don't include the linebreak 'glyph' + // (we assume there's one GlyphEntry for a newline, and no actual glyphs) + if !f(clump.as_immutable()) { break } + } + } + } + + // flush any remaining chars as a line + if in_clump { + clump.extend_to(range.end()); + f(clump.as_immutable()); + } + } + + fn iter_indivisible_pieces_for_range(&self, range: Range, f: fn(Range) -> bool) { + assert range.is_valid_for_string(self.text); + + let clump = MutableRange::new(range.begin(), 0); + loop { + // find next non-whitespace byte index, then clump all whitespace before it. + match str::find_between(self.text, clump.begin(), range.end(), |c| !char::is_whitespace(c)) { + Some(nonws_char_offset) => { + clump.extend_to(nonws_char_offset); + if !f(clump.as_immutable()) { break } + clump.reset(clump.end(), 0); + }, + None => { + // nothing left, flush last piece containing only whitespace + if clump.end() < range.end() { + clump.extend_to(range.end()); + f(clump.as_immutable()); + break; + } + } + }; + + // find next whitespace byte index, then clump all non-whitespace before it. + match str::find_between(self.text, clump.begin(), range.end(), |c| char::is_whitespace(c)) { + Some(ws_char_offset) => { + clump.extend_to(ws_char_offset); + if !f(clump.as_immutable()) { break } + clump.reset(clump.end(), 0); + } + None => { + // nothing left, flush last piece containing only non-whitespaces + if clump.end() < range.end() { + clump.extend_to(range.end()); + f(clump.as_immutable()); + break; + } + } + } + } + } +} + +// this test can't run until LayoutContext is removed as an argument +// to min_width_for_range. +/* +#[test] +fn test_calc_min_break_width() { + + fn test_min_width_for_run(text: ~str, width: Au) { + let flib = FontCache(); + let font = flib.get_test_font(); + let run = TextRun(font, text); + run.min_width_for_range(0, text.len()) + } + + test_min_width_for_run(~"firecracker", au::from_px(84)); + test_min_width_for_run(~"firecracker yumyum", au::from_px(84)); + test_min_width_for_run(~"yumyum firecracker", au::from_px(84)); + test_min_width_for_run(~"yumyum firecracker yumyum", au::from_px(84)); +} +*/ + +/*#[test] +#[ignore] +fn test_iter_indivisible_pieces() { + fn test_pieces(text: ~str, res: ~[~str]) { + let flib = FontCache(); + let font = flib.get_test_font(); + let run = TextRun::new(font, copy text); + let mut slices : ~[~str] = ~[]; + for run.iter_indivisible_pieces_for_range(Range(0, text.len())) |subrange| { + slices.push(str::slice(text, subrange.begin(), subrange.length())); + } + assert slices == res; + } + + test_pieces(~"firecracker yumyum woopwoop", ~[~"firecracker", ~" ", ~"yumyum", ~" ", ~"woopwoop"]); + test_pieces(~"firecracker yumyum ", ~[~"firecracker", ~" ", ~"yumyum", ~" "]); + test_pieces(~" firecracker yumyum", ~[~" ", ~"firecracker", ~" ", ~"yumyum"]); + test_pieces(~" ", ~[~" "]); + test_pieces(~"", ~[]); +} + +*/ diff --git a/src/servo-gfx-2/text/util.rs b/src/servo-gfx-2/text/util.rs new file mode 100644 index 00000000000..61bfa4db21f --- /dev/null +++ b/src/servo-gfx-2/text/util.rs @@ -0,0 +1,223 @@ +enum CompressionMode { + CompressNone, + CompressWhitespace, + CompressWhitespaceNewline, + DiscardNewline +} + +impl CompressionMode : cmp::Eq { + pure fn eq(other: &CompressionMode) -> bool { + match (self, *other) { + (CompressNone, CompressNone) => true, + (CompressWhitespace, CompressWhitespace) => true, + (CompressWhitespaceNewline, CompressWhitespaceNewline) => true, + (DiscardNewline, DiscardNewline) => true, + _ => false + } + } + pure fn ne(other: &CompressionMode) -> bool { + !self.eq(other) + } +} + +// ported from Gecko's nsTextFrameUtils::TransformText. +// +// High level TODOs: +// +// * Issue #113: consider incoming text state (preceding spaces, arabic, etc) +// and propogate outgoing text state (dual of above) +// +// * Issue #114: record skipped and kept chars for mapping original to new text +// +// * Untracked: various edge cases for bidi, CJK, etc. +pub fn transform_text(text: &str, mode: CompressionMode) -> ~str { + let mut out_str: ~str = ~""; + match mode { + CompressNone | DiscardNewline => { + for str::each_char(text) |ch: char| { + if is_discardable_char(ch, mode) { + // TODO: record skipped char + } else { + // TODO: record kept char + if ch == '\t' { + // TODO: set "has tab" flag + } + str::push_char(&mut out_str, ch); + } + } + }, + + CompressWhitespace | CompressWhitespaceNewline => { + let mut in_whitespace: bool = false; + for str::each_char(text) |ch: char| { + // TODO: discard newlines between CJK chars + let mut next_in_whitespace: bool = match (ch, mode) { + (' ', _) => true, + ('\t', _) => true, + ('\n', CompressWhitespaceNewline) => true, + (_, _) => false + }; + + if !next_in_whitespace { + if is_always_discardable_char(ch) { + // revert whitespace setting, since this char was discarded + next_in_whitespace = in_whitespace; + // TODO: record skipped char + } else { + // TODO: record kept char + str::push_char(&mut out_str, ch); + } + } else { /* next_in_whitespace; possibly add a space char */ + if in_whitespace { + // TODO: record skipped char + } else { + // TODO: record kept char + str::push_char(&mut out_str, ' '); + } + } + // save whitespace context for next char + in_whitespace = next_in_whitespace; + } /* /for str::each_char */ + } + } + + return move out_str; + + fn is_discardable_char(ch: char, mode: CompressionMode) -> bool { + if is_always_discardable_char(ch) { + return true; + } + match mode { + DiscardNewline | CompressWhitespaceNewline => ch == '\n', + _ => false + } + } + + fn is_always_discardable_char(_ch: char) -> bool { + // TODO: check for bidi control chars, soft hyphens. + false + } +} + +pub fn float_to_fixed(before: int, f: float) -> i32 { + (1i32 << before) * (f as i32) +} + +pub fn fixed_to_float(before: int, f: i32) -> float { + f as float * 1.0f / ((1i32 << before) as float) +} + +pub fn fixed_to_rounded_int(before: int, f: i32) -> int { + let half = 1i32 << (before-1); + if f > 0i32 { + ((half + f) >> before) as int + } else { + -((half - f) >> before) as int + } +} + +/* Generate a 32-bit TrueType tag from its 4 characters */ +pub fn true_type_tag(a: char, b: char, c: char, d: char) -> u32 { + (a << 24 | b << 16 | c << 8 | d) as u32 +} + +#[test] +fn test_true_type_tag() { + assert true_type_tag('c', 'm', 'a', 'p') == 0x_63_6D_61_70_u32; +} + +#[test] +fn test_transform_compress_none() { + + let test_strs : ~[~str] = ~[~" foo bar", + ~"foo bar ", + ~"foo\n bar", + ~"foo \nbar", + ~" foo bar \nbaz", + ~"foo bar baz", + ~"foobarbaz\n\n"]; + let mode = CompressNone; + + for uint::range(0, test_strs.len()) |i| { + assert transform_text(test_strs[i], mode) == test_strs[i]; + } +} + +#[test] +fn test_transform_discard_newline() { + + let test_strs : ~[~str] = ~[~" foo bar", + ~"foo bar ", + ~"foo\n bar", + ~"foo \nbar", + ~" foo bar \nbaz", + ~"foo bar baz", + ~"foobarbaz\n\n"]; + + let oracle_strs : ~[~str] = ~[~" foo bar", + ~"foo bar ", + ~"foo bar", + ~"foo bar", + ~" foo bar baz", + ~"foo bar baz", + ~"foobarbaz"]; + + assert test_strs.len() == oracle_strs.len(); + let mode = DiscardNewline; + + for uint::range(0, test_strs.len()) |i| { + assert transform_text(test_strs[i], mode) == oracle_strs[i]; + } +} + +#[test] +fn test_transform_compress_whitespace() { + let test_strs : ~[~str] = ~[~" foo bar", + ~"foo bar ", + ~"foo\n bar", + ~"foo \nbar", + ~" foo bar \nbaz", + ~"foo bar baz", + ~"foobarbaz\n\n"]; + + let oracle_strs : ~[~str] = ~[~" foo bar", + ~"foo bar ", + ~"foo\n bar", + ~"foo \nbar", + ~" foo bar \nbaz", + ~"foo bar baz", + ~"foobarbaz\n\n"]; + + assert test_strs.len() == oracle_strs.len(); + let mode = CompressWhitespace; + + for uint::range(0, test_strs.len()) |i| { + assert transform_text(test_strs[i], mode) == oracle_strs[i]; + } +} + +#[test] +fn test_transform_compress_whitespace_newline() { + let test_strs : ~[~str] = ~[~" foo bar", + ~"foo bar ", + ~"foo\n bar", + ~"foo \nbar", + ~" foo bar \nbaz", + ~"foo bar baz", + ~"foobarbaz\n\n"]; + + let oracle_strs : ~[~str] = ~[~" foo bar", + ~"foo bar ", + ~"foo bar", + ~"foo bar", + ~" foo bar baz", + ~"foo bar baz", + ~"foobarbaz "]; + + assert test_strs.len() == oracle_strs.len(); + let mode = CompressWhitespaceNewline; + + for uint::range(0, test_strs.len()) |i| { + assert transform_text(test_strs[i], mode) == oracle_strs[i]; + } +} diff --git a/src/servo-gfx-2/util/cache.rs b/src/servo-gfx-2/util/cache.rs new file mode 100644 index 00000000000..ef2825042db --- /dev/null +++ b/src/servo-gfx-2/util/cache.rs @@ -0,0 +1,59 @@ +use core::cmp::*; + +trait Cache { + static fn new(size: uint) -> self; + fn insert(key: &K, value: V); + fn find(key: &K) -> Option; + fn find_or_create(key: &K, blk: pure fn&(&K) -> V) -> V; + fn evict_all(); +} + +pub struct MonoCache { + mut entry: Option<(K,V)>, +} + +pub impl MonoCache : Cache { + static fn new(_size: uint) -> MonoCache { + MonoCache { entry: None } + } + + fn insert(key: &K, value: V) { + self.entry = Some((copy *key, value)); + } + + fn find(key: &K) -> Option { + match self.entry { + None => None, + Some((ref k,v)) => if *k == *key { Some(v) } else { None } + } + } + + fn find_or_create(key: &K, blk: pure fn&(&K) -> V) -> V { + return match self.find(key) { + None => { + let value = blk(key); + self.entry = Some((copy *key, copy value)); + move value + }, + Some(v) => v + }; + } + fn evict_all() { + self.entry = None; + } +} + +#[test] +fn test_monocache() { + // TODO: this is hideous because of Rust Issue #3902 + let cache = cache::new::>(10); + let one = @"one"; + let two = @"two"; + cache.insert(&1, one); + + assert cache.find(&1).is_some(); + assert cache.find(&2).is_none(); + cache.find_or_create(&2, |_v| { two }); + assert cache.find(&2).is_some(); + assert cache.find(&1).is_none(); +} \ No newline at end of file diff --git a/src/servo-gfx-2/util/range.rs b/src/servo-gfx-2/util/range.rs new file mode 100644 index 00000000000..1f54b7867cd --- /dev/null +++ b/src/servo-gfx-2/util/range.rs @@ -0,0 +1,164 @@ +pub struct Range { + priv off: u16, + priv len: u16 +} + +pub pure fn Range(off: uint, len: uint) -> Range { + assert off <= u16::max_value as uint; + assert len <= u16::max_value as uint; + + Range { + off: off as u16, + len: len as u16 + } +} + +pub pure fn empty() -> Range { Range(0,0) } + +enum RangeRelation { + OverlapsBegin(/* overlap */ uint), + OverlapsEnd(/* overlap */ uint), + ContainedBy, + Contains, + Coincides, + EntirelyBefore, + EntirelyAfter +} + +pub impl Range { + pub pure fn begin() -> uint { self.off as uint } + pub pure fn length() -> uint { self.len as uint } + pub pure fn end() -> uint { (self.off as uint) + (self.len as uint) } + + pub pure fn eachi(cb: fn&(uint) -> bool) { + do uint::range(self.off as uint, + (self.off as uint) + (self.len as uint)) |i| { + cb(i) + } + } + + pub pure fn is_valid_for_string(s: &str) -> bool { + self.begin() < s.len() && self.end() <= s.len() && self.length() <= s.len() + } + + pub pure fn shift_by(i: int) -> Range { + Range(((self.off as int) + i) as uint, self.len as uint) + } + + pub pure fn extend_by(i: int) -> Range { + Range(self.off as uint, ((self.len as int) + i) as uint) + } + + pub pure fn adjust_by(off_i: int, len_i: int) -> Range { + Range(((self.off as int) + off_i) as uint, ((self.len as int) + len_i) as uint) + } + + /// Computes the relationship between two ranges (`self` and `other`), + /// from the point of view of `self`. So, 'EntirelyBefore' means + /// that the `self` range is entirely before `other` range. + fn relation_to_range(&self, other: Range) -> RangeRelation { + if other.begin() > self.end() { + return EntirelyBefore; + } + if self.begin() > other.end() { + return EntirelyAfter; + } + if self.begin() == other.begin() && self.end() == other.end() { + return Coincides; + } + if self.begin() <= other.begin() && self.end() >= other.end() { + return Contains; + } + if self.begin() >= other.begin() && self.end() <= other.end() { + return ContainedBy; + } + if self.begin() < other.begin() && self.end() < other.end() { + let overlap = self.end() - other.begin(); + return OverlapsBegin(overlap); + } + if self.begin() > other.begin() && self.end() > other.end() { + let overlap = other.end() - self.begin(); + return OverlapsEnd(overlap); + } + fail fmt!("relation_to_range(): didn't classify self=%?, other=%?", + self, other); + } + + fn repair_after_coalesced_range(&self, other: Range) -> Range { + let relation = self.relation_to_range(other); + debug!("repair_after_coalesced_range: possibly repairing range %?", self); + debug!("repair_after_coalesced_range: relation of original range and coalesced range(%?): %?", + other, relation); + let new_range = match relation { + EntirelyBefore => { *self }, + EntirelyAfter => { self.shift_by(-(other.length() as int)) }, + Coincides | ContainedBy => { Range(other.begin(), 1) }, + Contains => { self.extend_by(-(other.length() as int)) }, + OverlapsBegin(overlap) => { self.extend_by(1 - (overlap as int)) }, + OverlapsEnd(overlap) => + { Range(other.begin(), self.length() - overlap + 1) } + }; + debug!("repair_after_coalesced_range: new range: ---- %?", new_range); + new_range + } +} + +pub pure fn empty_mut() -> MutableRange { MutableRange::new(0, 0) } + +pub struct MutableRange { + priv mut off: uint, + priv mut len: uint +} + +pub impl MutableRange { + static pub pure fn new(off: uint, len: uint) -> MutableRange { + MutableRange { off: off, len: len } + } + + static pub pure fn empty() -> MutableRange { + MutableRange::new(0, 0) + } +} + +impl MutableRange { + pub pure fn begin() -> uint { self.off } + pub pure fn length() -> uint { self.len } + pub pure fn end() -> uint { self.off + self.len } + pub pure fn eachi(cb: fn&(uint) -> bool) { + do uint::range(self.off, self.off + self.len) |i| { cb(i) } + } + + fn relation_to_range(&self, other: &MutableRange) -> RangeRelation { + self.as_immutable().relation_to_range(other.as_immutable()) + } + + pub pure fn as_immutable() -> Range { + Range(self.begin(), self.length()) + } + + pub pure fn is_valid_for_string(s: &str) -> bool { + self.begin() < s.len() && self.end() <= s.len() && self.length() <= s.len() + } + + pub fn shift_by(i: int) { + self.off = ((self.off as int) + i) as uint; + } + + pub fn extend_by(i: int) { + self.len = ((self.len as int) + i) as uint; + } + + pub fn extend_to(i: uint) { + self.len = i - self.off; + } + + pub fn adjust_by(off_i: int, len_i: int) { + self.off = ((self.off as int) + off_i) as uint; + self.len = ((self.len as int) + len_i) as uint; + } + + pub fn reset(off_i: uint, len_i: uint) { + self.off = off_i; + self.len = len_i; + } +} diff --git a/src/servo-gfx-2/util/time.rs b/src/servo-gfx-2/util/time.rs new file mode 100644 index 00000000000..a62626b5284 --- /dev/null +++ b/src/servo-gfx-2/util/time.rs @@ -0,0 +1,15 @@ +// Timing functions. +use std::time::precise_time_ns; + +pub fn time(msg: &str, callback: fn() -> T) -> T{ + let start_time = precise_time_ns(); + let val = callback(); + let end_time = precise_time_ns(); + let ms = ((end_time - start_time) / 1000000u64) as uint; + if ms >= 5 { + #debug("%s took %u ms", msg, ms); + } + return move val; +} + + diff --git a/src/servo-gfx-2/util/url.rs b/src/servo-gfx-2/util/url.rs new file mode 100644 index 00000000000..d2e4eb9ccd3 --- /dev/null +++ b/src/servo-gfx-2/util/url.rs @@ -0,0 +1,107 @@ +use core::path::Path; +use std::map::HashMap; +use std::net::url; +use std::net::url::Url; + +/** +Create a URL object from a string. Does various helpful browsery things like + +* If there's no current url and the path looks like a file then it will + create a file url based of the current working directory +* If there's a current url and the new path is relative then the new url + is based off the current url + +*/ +#[allow(non_implicitly_copyable_typarams)] +pub fn make_url(str_url: ~str, current_url: Option) -> Url { + let mut schm = url::get_scheme(str_url); + let str_url = if result::is_err(&schm) { + if current_url.is_none() { + // If all we have is a filename, assume it's a local relative file + // and build an absolute path with the cwd + ~"file://" + os::getcwd().push(str_url).to_str() + } else { + let current_url = current_url.get(); + #debug("make_url: current_url: %?", current_url); + if current_url.path.is_empty() || current_url.path.ends_with("/") { + current_url.scheme + "://" + current_url.host + "/" + str_url + } else { + let path = str::split_char(current_url.path, '/'); + let path = path.init(); + let path = str::connect(path + ~[move str_url], "/"); + + current_url.scheme + "://" + current_url.host + path + } + } + } else { + move str_url + }; + + // FIXME: Need to handle errors + url::from_str(str_url).get() +} + +mod make_url_tests { + + #[test] + fn should_create_absolute_file_url_if_current_url_is_none_and_str_url_looks_filey() { + let file = ~"local.html"; + let url = make_url(move file, None); + #debug("url: %?", url); + assert url.scheme == ~"file"; + assert url.path.contains(os::getcwd().to_str()); + } + + #[test] + fn should_create_url_based_on_old_url_1() { + let old_str = ~"http://example.com"; + let old_url = make_url(move old_str, None); + let new_str = ~"index.html"; + let new_url = make_url(move new_str, Some(move old_url)); + assert new_url.scheme == ~"http"; + assert new_url.host == ~"example.com"; + assert new_url.path == ~"/index.html"; + } + + #[test] + fn should_create_url_based_on_old_url_2() { + let old_str = ~"http://example.com/"; + let old_url = make_url(move old_str, None); + let new_str = ~"index.html"; + let new_url = make_url(move new_str, Some(move old_url)); + assert new_url.scheme == ~"http"; + assert new_url.host == ~"example.com"; + assert new_url.path == ~"/index.html"; + } + + #[test] + fn should_create_url_based_on_old_url_3() { + let old_str = ~"http://example.com/index.html"; + let old_url = make_url(move old_str, None); + let new_str = ~"crumpet.html"; + let new_url = make_url(move new_str, Some(move old_url)); + assert new_url.scheme == ~"http"; + assert new_url.host == ~"example.com"; + assert new_url.path == ~"/crumpet.html"; + } + + #[test] + fn should_create_url_based_on_old_url_4() { + let old_str = ~"http://example.com/snarf/index.html"; + let old_url = make_url(move old_str, None); + let new_str = ~"crumpet.html"; + let new_url = make_url(move new_str, Some(move old_url)); + assert new_url.scheme == ~"http"; + assert new_url.host == ~"example.com"; + assert new_url.path == ~"/snarf/crumpet.html"; + } + +} + +pub type UrlMap = HashMap; + +pub fn url_map() -> UrlMap { + use core::to_str::ToStr; + + HashMap::() +} diff --git a/src/servo-gfx-2/util/vec.rs b/src/servo-gfx-2/util/vec.rs new file mode 100644 index 00000000000..1d476ab2420 --- /dev/null +++ b/src/servo-gfx-2/util/vec.rs @@ -0,0 +1,102 @@ +use core::cmp::{Ord, Eq}; + +export BinarySearchMethods, binary_search, binary_search_index; + +trait BinarySearchMethods { + pure fn binary_search(&self, key: &T) -> Option<&self/T>; + pure fn binary_search_index(&self, key: &T) -> Option; +} + +impl &[T]: BinarySearchMethods { + pure fn binary_search(&self, key: &T) -> Option<&self/T> { + match self.binary_search_index(key) { + None => None, + Some(i) => Some(&self[i]) + } + } + + pure fn binary_search_index(&self, key: &T) -> Option { + if self.len() == 0 { + return None; + } + + let mut low : int = 0; + let mut high : int = (self.len() as int) - 1; + + while (low <= high) { + // http://googleresearch.blogspot.com/2006/06/extra-extra-read-all-about-it-nearly.html + let mid : int = (((low as uint) + (high as uint)) >> 1) as int; + let midv = &self[mid]; + + if (midv < key) { + low = mid + 1; + } else if (midv > key) { + high = mid - 1; + } else { + return Some(mid as uint); + } + } + return None; + } +} + +fn test_find_all_elems(arr: &[T]) { + let mut i = 0; + while i < arr.len() { + assert test_match(&arr[i], arr.binary_search(&arr[i])); + i += 1; + } +} + +fn test_miss_all_elems(arr: &[T], misses: &[T]) { + let mut i = 0; + while i < misses.len() { + let res = arr.binary_search(&misses[i]); + debug!("%? == %? ?", misses[i], res); + assert !test_match(&misses[i], arr.binary_search(&misses[i])); + i += 1; + } +} + +fn test_match(b: &T, a: Option<&T>) -> bool { + match a { + None => false, + Some(t) => t == b + } +} + +fn should_find_all_elements() { + #[test]; + + let arr_odd = [1, 2, 4, 6, 7, 8, 9]; + let arr_even = [1, 2, 5, 6, 7, 8, 9, 42]; + let arr_double = [1, 1, 2, 2, 6, 8, 22]; + let arr_one = [234986325]; + let arr_two = [3044, 8393]; + let arr_three = [12, 23, 34]; + + test_find_all_elems(arr_odd); + test_find_all_elems(arr_even); + test_find_all_elems(arr_double); + test_find_all_elems(arr_one); + test_find_all_elems(arr_two); + test_find_all_elems(arr_three); +} + +fn should_not_find_missing_elements() { + #[test]; + + let arr_odd = [1, 2, 4, 6, 7, 8, 9]; + let arr_even = [1, 2, 5, 6, 7, 8, 9, 42]; + let arr_double = [1, 1, 2, 2, 6, 8, 22]; + let arr_one = [234986325]; + let arr_two = [3044, 8393]; + let arr_three = [12, 23, 34]; + + test_miss_all_elems(arr_odd, [-22, 0, 3, 5, 34938, 10, 11, 12]); + test_miss_all_elems(arr_even, [-1, 0, 3, 34938, 10, 11, 12]); + test_miss_all_elems(arr_double, [-1, 0, 3, 4, 34938, 10, 11, 12, 234, 234, 33]); + test_miss_all_elems(arr_one, [-1, 0, 3, 34938, 10, 11, 12, 234, 234, 33]); + test_miss_all_elems(arr_two, [-1, 0, 3, 34938, 10, 11, 12, 234, 234, 33]); + test_miss_all_elems(arr_three, [-2, 0, 1, 2, 3, 34938, 10, 11, 234, 234, 33]); +}