Anforderungen  |   Konzepte  |   Entwurf  |   Entwicklung  |   Qualitätssicherung  |   Lebenszyklus  |   Steuerung
 
 
 
 


Quelle  border.rs   Sprache: unbekannt

 
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

use api::{BorderRadius, BorderSide, BorderStyle, ColorF, ColorU};
use api::{NormalBorder as ApiNormalBorder, RepeatMode};
use api::units::*;
use crate::clip::ClipNodeId;
use crate::ellipse::Ellipse;
use euclid::vec2;
use crate::scene_building::SceneBuilder;
use crate::spatial_tree::SpatialNodeIndex;
use crate::gpu_types::{BorderInstance, BorderSegment, BrushFlags};
use crate::prim_store::{BorderSegmentInfo, BrushSegment, NinePatchDescriptor};
use crate::prim_store::borders::{NormalBorderPrim, NormalBorderData};
use crate::util::{lerp, RectHelpers};
use crate::internal_types::LayoutPrimitiveInfo;
use crate::segment::EdgeAaSegmentMask;

// Using 2048 as the maximum radius in device space before which we
// start stretching is up for debate.
// the value must be chosen so that the corners will not use an
// unreasonable amount of memory but should allow crisp corners in the
// common cases.

/// Maximum resolution in device pixels at which borders are rasterized.
pub const MAX_BORDER_RESOLUTION: u32 = 2048;
/// Maximum number of dots or dashes per segment to avoid freezing and filling up
/// memory with unreasonable inputs. It would be better to address this by not building
/// a list of per-dot information in the first place.
pub const MAX_DASH_COUNT: u32 = 2048;

// TODO(gw): Perhaps there is a better way to store
//           the border cache key than duplicating
//           all the border structs with hashable
//           variants...

#[derive(Copy, Clone, Debug, Hash, MallocSizeOf, PartialEq, Eq)]
#[cfg_attr(feature = "capture", derive(Serialize))]
#[cfg_attr(feature = "replay", derive(Deserialize))]
pub struct BorderRadiusAu {
    pub top_left: LayoutSizeAu,
    pub top_right: LayoutSizeAu,
    pub bottom_left: LayoutSizeAu,
    pub bottom_right: LayoutSizeAu,
}

impl From<BorderRadius> for BorderRadiusAu {
    fn from(radius: BorderRadius) -> BorderRadiusAu {
        BorderRadiusAu {
            top_left: radius.top_left.to_au(),
            top_right: radius.top_right.to_au(),
            bottom_right: radius.bottom_right.to_au(),
            bottom_left: radius.bottom_left.to_au(),
        }
    }
}

impl From<BorderRadiusAu> for BorderRadius {
    fn from(radius: BorderRadiusAu) -> Self {
        BorderRadius {
            top_left: LayoutSize::from_au(radius.top_left),
            top_right: LayoutSize::from_au(radius.top_right),
            bottom_right: LayoutSize::from_au(radius.bottom_right),
            bottom_left: LayoutSize::from_au(radius.bottom_left),
        }
    }
}

#[derive(Clone, Debug, Hash, MallocSizeOf, PartialEq, Eq)]
#[cfg_attr(feature = "capture", derive(Serialize))]
#[cfg_attr(feature = "replay", derive(Deserialize))]
pub struct BorderSideAu {
    pub color: ColorU,
    pub style: BorderStyle,
}

impl From<BorderSide> for BorderSideAu {
    fn from(side: BorderSide) -> Self {
        BorderSideAu {
            color: side.color.into(),
            style: side.style,
        }
    }
}

impl From<BorderSideAu> for BorderSide {
    fn from(side: BorderSideAu) -> Self {
        BorderSide {
            color: side.color.into(),
            style: side.style,
        }
    }
}

#[cfg_attr(feature = "capture", derive(Serialize))]
#[cfg_attr(feature = "replay", derive(Deserialize))]
#[derive(Debug, Clone, Hash, Eq, MallocSizeOf, PartialEq)]
pub struct NormalBorderAu {
    pub left: BorderSideAu,
    pub right: BorderSideAu,
    pub top: BorderSideAu,
    pub bottom: BorderSideAu,
    pub radius: BorderRadiusAu,
    /// Whether to apply anti-aliasing on the border corners.
    ///
    /// Note that for this to be `false` and work, this requires the borders to
    /// be solid, and no border-radius.
    pub do_aa: bool,
}

impl NormalBorderAu {
    // Construct a border based upon self with color
    pub fn with_color(&self, color: ColorU) -> Self {
        let mut b = self.clone();
        b.left.color = color;
        b.right.color = color;
        b.top.color = color;
        b.bottom.color = color;
        b
    }
}

impl From<ApiNormalBorder> for NormalBorderAu {
    fn from(border: ApiNormalBorder) -> Self {
        NormalBorderAu {
            left: border.left.into(),
            right: border.right.into(),
            top: border.top.into(),
            bottom: border.bottom.into(),
            radius: border.radius.into(),
            do_aa: border.do_aa,
        }
    }
}

impl From<NormalBorderAu> for ApiNormalBorder {
    fn from(border: NormalBorderAu) -> Self {
        ApiNormalBorder {
            left: border.left.into(),
            right: border.right.into(),
            top: border.top.into(),
            bottom: border.bottom.into(),
            radius: border.radius.into(),
            do_aa: border.do_aa,
        }
    }
}

/// Cache key that uniquely identifies a border
/// segment in the render task cache.
#[derive(Clone, Debug, Hash, MallocSizeOf, PartialEq, Eq)]
#[cfg_attr(feature = "capture", derive(Serialize))]
#[cfg_attr(feature = "replay", derive(Deserialize))]
pub struct BorderSegmentCacheKey {
    pub size: LayoutSizeAu,
    pub radius: LayoutSizeAu,
    pub side0: BorderSideAu,
    pub side1: BorderSideAu,
    pub segment: BorderSegment,
    pub do_aa: bool,
    pub h_adjacent_corner_outer: LayoutPointAu,
    pub h_adjacent_corner_radius: LayoutSizeAu,
    pub v_adjacent_corner_outer: LayoutPointAu,
    pub v_adjacent_corner_radius: LayoutSizeAu,
}

pub fn ensure_no_corner_overlap(
    radius: &mut BorderRadius,
    size: LayoutSize,
) {
    let mut ratio = 1.0;
    let top_left_radius = &mut radius.top_left;
    let top_right_radius = &mut radius.top_right;
    let bottom_right_radius = &mut radius.bottom_right;
    let bottom_left_radius = &mut radius.bottom_left;

    if size.width > 0.0 {
        let sum = top_left_radius.width + top_right_radius.width;
        if size.width < sum {
            ratio = f32::min(ratio, size.width / sum);
        }

        let sum = bottom_left_radius.width + bottom_right_radius.width;
        if size.width < sum {
            ratio = f32::min(ratio, size.width / sum);
        }
    }

    if size.height > 0.0 {
        let sum = top_left_radius.height + bottom_left_radius.height;
        if size.height < sum {
            ratio = f32::min(ratio, size.height / sum);
        }

        let sum = top_right_radius.height + bottom_right_radius.height;
        if size.height < sum {
            ratio = f32::min(ratio, size.height / sum);
        }
    }

    if ratio < 1. {
        top_left_radius.width *= ratio;
        top_left_radius.height *= ratio;

        top_right_radius.width *= ratio;
        top_right_radius.height *= ratio;

        bottom_left_radius.width *= ratio;
        bottom_left_radius.height *= ratio;

        bottom_right_radius.width *= ratio;
        bottom_right_radius.height *= ratio;
    }
}

impl<'a> SceneBuilder<'a> {
    pub fn add_normal_border(
        &mut self,
        info: &LayoutPrimitiveInfo,
        border: &ApiNormalBorder,
        widths: LayoutSideOffsets,
        spatial_node_index: SpatialNodeIndex,
        clip_node_id: ClipNodeId,
    ) {
        let mut border = *border;
        ensure_no_corner_overlap(&mut border.radius, info.rect.size());

        self.add_primitive(
            spatial_node_index,
            clip_node_id,
            info,
            Vec::new(),
            NormalBorderPrim {
                border: border.into(),
                widths: widths.to_au(),
            },
        );
    }
}

pub trait BorderSideHelpers {
    fn border_color(&self, is_inner_border: bool) -> ColorF;
}

impl BorderSideHelpers for BorderSide {
    fn border_color(&self, is_inner_border: bool) -> ColorF {
        let lighter = match self.style {
            BorderStyle::Inset => is_inner_border,
            BorderStyle::Outset => !is_inner_border,
            _ => return self.color,
        };

        // The modulate colors below are not part of the specification. They are
        // derived from the Gecko source code and experimentation, and used to
        // modulate the colors in order to generate colors for the inset/outset
        // and groove/ridge border styles.
        //
        // NOTE(emilio): Gecko at least takes the background color into
        // account, should we do the same? Looks a bit annoying for this.
        //
        // NOTE(emilio): If you change this algorithm, do the same change on
        // get_colors_for_side in cs_border_segment.glsl.
        if self.color.r != 0.0 || self.color.g != 0.0 || self.color.b != 0.0 {
            let scale = if lighter { 1.0 } else { 2.0 / 3.0 };
            return self.color.scale_rgb(scale)
        }

        let black = if lighter { 0.7 } else { 0.3 };
        ColorF::new(black, black, black, self.color.a)
    }
}

/// The kind of border corner clip.
#[repr(C)]
#[derive(Copy, Debug, Clone, PartialEq)]
pub enum BorderClipKind {
    DashCorner = 1,
    DashEdge = 2,
    Dot = 3,
}

fn compute_outer_and_clip_sign(
    corner_segment: BorderSegment,
    radius: DeviceSize,
) -> (DevicePoint, DeviceVector2D) {
    let outer_scale = match corner_segment {
        BorderSegment::TopLeft => DeviceVector2D::new(0.0, 0.0),
        BorderSegment::TopRight => DeviceVector2D::new(1.0, 0.0),
        BorderSegment::BottomRight => DeviceVector2D::new(1.0, 1.0),
        BorderSegment::BottomLeft => DeviceVector2D::new(0.0, 1.0),
        _ => panic!("bug: expected a corner segment"),
    };
    let outer = DevicePoint::new(
        outer_scale.x * radius.width,
        outer_scale.y * radius.height,
    );

    let clip_sign = DeviceVector2D::new(
        1.0 - 2.0 * outer_scale.x,
        1.0 - 2.0 * outer_scale.y,
    );

    (outer, clip_sign)
}

fn write_dashed_corner_instances(
    corner_radius: DeviceSize,
    widths: DeviceSize,
    segment: BorderSegment,
    base_instance: &BorderInstance,
    instances: &mut Vec<BorderInstance>,
) -> Result<(), ()> {
    let ellipse = Ellipse::new(corner_radius);

    let average_border_width = 0.5 * (widths.width + widths.height);

    let (_half_dash, num_half_dashes) =
        compute_half_dash(average_border_width, ellipse.total_arc_length);

    if num_half_dashes == 0 {
        return Err(());
    }

    let num_half_dashes = num_half_dashes.min(MAX_DASH_COUNT);

    let (outer, clip_sign) = compute_outer_and_clip_sign(segment, corner_radius);

    let instance_count = num_half_dashes / 4 + 1;
    instances.reserve(instance_count as usize);

    let half_dash_arc_length =
        ellipse.total_arc_length / num_half_dashes as f32;
    let dash_length = 2. * half_dash_arc_length;

    let mut current_length = 0.;
    for i in 0..instance_count {
        let arc_length0 = current_length;
        current_length += if i == 0 {
            half_dash_arc_length
        } else {
            dash_length
        };

        let arc_length1 = current_length;
        current_length += dash_length;

        let alpha = ellipse.find_angle_for_arc_length(arc_length0);
        let beta = ellipse.find_angle_for_arc_length(arc_length1);

        let (point0, tangent0) = ellipse.get_point_and_tangent(alpha);
        let (point1, tangent1) = ellipse.get_point_and_tangent(beta);

        let point0 = DevicePoint::new(
            outer.x + clip_sign.x * (corner_radius.width - point0.x),
            outer.y + clip_sign.y * (corner_radius.height - point0.y),
        );

        let tangent0 = DeviceVector2D::new(
            -tangent0.x * clip_sign.x,
            -tangent0.y * clip_sign.y,
        );

        let point1 = DevicePoint::new(
            outer.x + clip_sign.x * (corner_radius.width - point1.x),
            outer.y + clip_sign.y * (corner_radius.height - point1.y),
        );

        let tangent1 = DeviceVector2D::new(
            -tangent1.x * clip_sign.x,
            -tangent1.y * clip_sign.y,
        );

        instances.push(BorderInstance {
            flags: base_instance.flags | ((BorderClipKind::DashCorner as i32) << 24),
            clip_params: [
                point0.x,
                point0.y,
                tangent0.x,
                tangent0.y,
                point1.x,
                point1.y,
                tangent1.x,
                tangent1.y,
            ],
            .. *base_instance
        });
    }

    Ok(())
}

fn write_dotted_corner_instances(
    corner_radius: DeviceSize,
    widths: DeviceSize,
    segment: BorderSegment,
    base_instance: &BorderInstance,
    instances: &mut Vec<BorderInstance>,
) -> Result<(), ()> {
    let mut corner_radius = corner_radius;
    if corner_radius.width < (widths.width / 2.0) {
        corner_radius.width = 0.0;
    }
    if corner_radius.height < (widths.height / 2.0) {
        corner_radius.height = 0.0;
    }

    let (ellipse, max_dot_count) =
        if corner_radius.width == 0. && corner_radius.height == 0. {
            (Ellipse::new(corner_radius), 1)
        } else {
            // The centers of dots follow an ellipse along the middle of the
            // border radius.
            let inner_radius = (corner_radius - widths * 0.5).abs();
            let ellipse = Ellipse::new(inner_radius);

            // Allocate a "worst case" number of dot clips. This can be
            // calculated by taking the minimum edge radius, since that
            // will result in the maximum number of dots along the path.
            let min_diameter = widths.width.min(widths.height);

            // Get the number of circles (assuming spacing of one diameter
            // between dots).
            let max_dot_count = 0.5 * ellipse.total_arc_length / min_diameter;

            // Add space for one extra dot since they are centered at the
            // start of the arc.
            (ellipse, max_dot_count.ceil() as usize)
        };

    if max_dot_count == 0 {
        return Err(());
    }

    if max_dot_count == 1 {
        let dot_diameter = lerp(widths.width, widths.height, 0.5);
        instances.push(BorderInstance {
            flags: base_instance.flags | ((BorderClipKind::Dot as i32) << 24),
            clip_params: [
                widths.width / 2.0, widths.height / 2.0, 0.5 * dot_diameter, 0.,
                0., 0., 0., 0.,
            ],
            .. *base_instance
        });
        return Ok(());
    }

    let max_dot_count = max_dot_count.min(MAX_DASH_COUNT as usize);

    // FIXME(emilio): Should probably use SmallVec.
    let mut forward_dots = Vec::with_capacity(max_dot_count / 2 + 1);
    let mut back_dots = Vec::with_capacity(max_dot_count / 2 + 1);
    let mut leftover_arc_length = 0.0;

    // Alternate between adding dots at the start and end of the
    // ellipse arc. This ensures that we always end up with an exact
    // half dot at each end of the arc, to match up with the edges.
    forward_dots.push(DotInfo::new(widths.width, widths.width));
    back_dots.push(DotInfo::new(
        ellipse.total_arc_length - widths.height,
        widths.height,
    ));

    let (outer, clip_sign) = compute_outer_and_clip_sign(segment, corner_radius);
    for dot_index in 0 .. max_dot_count {
        let prev_forward_pos = *forward_dots.last().unwrap();
        let prev_back_pos = *back_dots.last().unwrap();

        // Select which end of the arc to place a dot from.
        // This just alternates between the start and end of
        // the arc, which ensures that there is always an
        // exact half-dot at each end of the ellipse.
        let going_forward = dot_index & 1 == 0;

        let (next_dot_pos, leftover) = if going_forward {
            let next_dot_pos =
                prev_forward_pos.arc_pos + 2.0 * prev_forward_pos.diameter;
            (next_dot_pos, prev_back_pos.arc_pos - next_dot_pos)
        } else {
            let next_dot_pos = prev_back_pos.arc_pos - 2.0 * prev_back_pos.diameter;
            (next_dot_pos, next_dot_pos - prev_forward_pos.arc_pos)
        };

        // Use a lerp between each edge's dot
        // diameter, based on the linear distance
        // along the arc to get the diameter of the
        // dot at this arc position.
        let t = next_dot_pos / ellipse.total_arc_length;
        let dot_diameter = lerp(widths.width, widths.height, t);

        // If we can't fit a dot, bail out.
        if leftover < dot_diameter {
            leftover_arc_length = leftover;
            break;
        }

        // We can place a dot!
        let dot = DotInfo::new(next_dot_pos, dot_diameter);
        if going_forward {
            forward_dots.push(dot);
        } else {
            back_dots.push(dot);
        }
    }

    // Now step through the dots, and distribute any extra
    // leftover space on the arc between them evenly. Once
    // the final arc position is determined, generate the correct
    // arc positions and angles that get passed to the clip shader.
    let number_of_dots = forward_dots.len() + back_dots.len();
    let extra_space_per_dot = leftover_arc_length / (number_of_dots - 1) as f32;

    let create_dot_data = |arc_length: f32, dot_radius: f32| -> [f32; 8] {
        // Represents the GPU data for drawing a single dot to a clip mask. The order
        // these are specified must stay in sync with the way this data is read in the
        // dot clip shader.
        let theta = ellipse.find_angle_for_arc_length(arc_length);
        let (center, _) = ellipse.get_point_and_tangent(theta);

        let center = DevicePoint::new(
            outer.x + clip_sign.x * (corner_radius.width - center.x),
            outer.y + clip_sign.y * (corner_radius.height - center.y),
        );

        [center.x, center.y, dot_radius, 0.0, 0.0, 0.0, 0.0, 0.0]
    };

    instances.reserve(number_of_dots);
    for (i, dot) in forward_dots.iter().enumerate() {
        let extra_dist = i as f32 * extra_space_per_dot;
        instances.push(BorderInstance {
            flags: base_instance.flags | ((BorderClipKind::Dot as i32) << 24),
            clip_params: create_dot_data(dot.arc_pos + extra_dist, 0.5 * dot.diameter),
            .. *base_instance
        });
    }

    for (i, dot) in back_dots.iter().enumerate() {
        let extra_dist = i as f32 * extra_space_per_dot;
        instances.push(BorderInstance {
            flags: base_instance.flags | ((BorderClipKind::Dot as i32) << 24),
            clip_params: create_dot_data(dot.arc_pos - extra_dist, 0.5 * dot.diameter),
            .. *base_instance
        });
    }

    Ok(())
}

#[derive(Copy, Clone, Debug)]
struct DotInfo {
    arc_pos: f32,
    diameter: f32,
}

impl DotInfo {
    fn new(arc_pos: f32, diameter: f32) -> DotInfo {
        DotInfo { arc_pos, diameter }
    }
}

/// Information needed to place and draw a border edge.
#[derive(Debug)]
struct EdgeInfo {
    /// Offset in local space to place the edge from origin.
    local_offset: f32,
    /// Size of the edge in local space.
    local_size: f32,
    /// Local stretch size for this edge (repeat past this).
    stretch_size: f32,
}

impl EdgeInfo {
    fn new(
        local_offset: f32,
        local_size: f32,
        stretch_size: f32,
    ) -> Self {
        Self {
            local_offset,
            local_size,
            stretch_size,
        }
    }
}

// Given a side width and the available space, compute the half-dash (half of
// the 'on' segment) and the count of them for a given segment.
fn compute_half_dash(side_width: f32, total_size: f32) -> (f32, u32) {
    let half_dash = side_width * 1.5;
    // 16k dashes should be enough for anyone
    let num_half_dashes = (total_size / half_dash).ceil().min(16.0 * 1024.0) as u32;

    if num_half_dashes == 0 {
        return (0., 0);
    }

    // TODO(emilio): Gecko has some other heuristics here to start with a full
    // dash when the border side is zero, for example. We might consider those
    // in the future.
    let num_half_dashes = if num_half_dashes % 4 != 0 {
        num_half_dashes + 4 - num_half_dashes % 4
    } else {
        num_half_dashes
    };

    let half_dash = total_size / num_half_dashes as f32;
    (half_dash, num_half_dashes)
}


// Get the needed size in device pixels for an edge,
// based on the border style of that edge. This is used
// to determine how big the render task should be.
fn get_edge_info(
    style: BorderStyle,
    side_width: f32,
    avail_size: f32,
) -> EdgeInfo {
    // To avoid division by zero below.
    if side_width <= 0.0 || avail_size <= 0.0 {
        return EdgeInfo::new(0.0, 0.0, 0.0);
    }

    match style {
        BorderStyle::Dashed => {
            // Basically, two times the dash size.
            let (half_dash, _num_half_dashes) =
                compute_half_dash(side_width, avail_size);
            let stretch_size = 2.0 * 2.0 * half_dash;
            EdgeInfo::new(0., avail_size, stretch_size)
        }
        BorderStyle::Dotted => {
            let dot_and_space_size = 2.0 * side_width;
            if avail_size < dot_and_space_size * 0.75 {
                return EdgeInfo::new(0.0, 0.0, 0.0);
            }
            let approx_dot_count = avail_size / dot_and_space_size;
            let dot_count = approx_dot_count.floor().max(1.0);
            let used_size = dot_count * dot_and_space_size;
            let extra_space = avail_size - used_size;
            let stretch_size = dot_and_space_size;
            let offset = (extra_space * 0.5).round();
            EdgeInfo::new(offset, used_size, stretch_size)
        }
        _ => {
            EdgeInfo::new(0.0, avail_size, 8.0)
        }
    }
}

/// Create the set of border segments and render task
/// cache keys for a given CSS border.
pub fn create_border_segments(
    size: LayoutSize,
    border: &ApiNormalBorder,
    widths: &LayoutSideOffsets,
    border_segments: &mut Vec<BorderSegmentInfo>,
    brush_segments: &mut Vec<BrushSegment>,
) {
    let rect = LayoutRect::from_size(size);

    let overlap = LayoutSize::new(
        (widths.left + widths.right - size.width).max(0.0),
        (widths.top + widths.bottom - size.height).max(0.0),
    );
    let non_overlapping_widths = LayoutSideOffsets::new(
        widths.top - overlap.height / 2.0,
        widths.right - overlap.width / 2.0,
        widths.bottom - overlap.height / 2.0,
        widths.left - overlap.width / 2.0,
    );

    let local_size_tl = LayoutSize::new(
        border.radius.top_left.width.max(widths.left),
        border.radius.top_left.height.max(widths.top),
    );
    let local_size_tr = LayoutSize::new(
        border.radius.top_right.width.max(widths.right),
        border.radius.top_right.height.max(widths.top),
    );
    let local_size_br = LayoutSize::new(
        border.radius.bottom_right.width.max(widths.right),
        border.radius.bottom_right.height.max(widths.bottom),
    );
    let local_size_bl = LayoutSize::new(
        border.radius.bottom_left.width.max(widths.left),
        border.radius.bottom_left.height.max(widths.bottom),
    );

    let top_edge_info = get_edge_info(
        border.top.style,
        widths.top,
        rect.width() - local_size_tl.width - local_size_tr.width,
    );
    let bottom_edge_info = get_edge_info(
        border.bottom.style,
        widths.bottom,
        rect.width() - local_size_bl.width - local_size_br.width,
    );

    let left_edge_info = get_edge_info(
        border.left.style,
        widths.left,
        rect.height() - local_size_tl.height - local_size_bl.height,
    );
    let right_edge_info = get_edge_info(
        border.right.style,
        widths.right,
        rect.height() - local_size_tr.height - local_size_br.height,
    );

    add_edge_segment(
        LayoutRect::from_floats(
            rect.min.x,
            rect.min.y + local_size_tl.height + left_edge_info.local_offset,
            rect.min.x + non_overlapping_widths.left,
            rect.min.y + local_size_tl.height + left_edge_info.local_offset + left_edge_info.local_size,
        ),
        &left_edge_info,
        border.left,
        non_overlapping_widths.left,
        BorderSegment::Left,
        EdgeAaSegmentMask::LEFT | EdgeAaSegmentMask::RIGHT,
        brush_segments,
        border_segments,
        border.do_aa,
    );
    add_edge_segment(
        LayoutRect::from_floats(
            rect.min.x + local_size_tl.width + top_edge_info.local_offset,
            rect.min.y,
            rect.min.x + local_size_tl.width + top_edge_info.local_offset + top_edge_info.local_size,
            rect.min.y + non_overlapping_widths.top,
        ),
        &top_edge_info,
        border.top,
        non_overlapping_widths.top,
        BorderSegment::Top,
        EdgeAaSegmentMask::TOP | EdgeAaSegmentMask::BOTTOM,
        brush_segments,
        border_segments,
        border.do_aa,
    );
    add_edge_segment(
        LayoutRect::from_floats(
            rect.min.x + rect.width() - non_overlapping_widths.right,
            rect.min.y + local_size_tr.height + right_edge_info.local_offset,
            rect.min.x + rect.width(),
            rect.min.y + local_size_tr.height + right_edge_info.local_offset + right_edge_info.local_size,
        ),
        &right_edge_info,
        border.right,
        non_overlapping_widths.right,
        BorderSegment::Right,
        EdgeAaSegmentMask::RIGHT | EdgeAaSegmentMask::LEFT,
        brush_segments,
        border_segments,
        border.do_aa,
    );
    add_edge_segment(
        LayoutRect::from_floats(
            rect.min.x + local_size_bl.width + bottom_edge_info.local_offset,
            rect.min.y + rect.height() - non_overlapping_widths.bottom,
            rect.min.x + local_size_bl.width + bottom_edge_info.local_offset + bottom_edge_info.local_size,
            rect.min.y + rect.height(),
        ),
        &bottom_edge_info,
        border.bottom,
        non_overlapping_widths.bottom,
        BorderSegment::Bottom,
        EdgeAaSegmentMask::BOTTOM | EdgeAaSegmentMask::TOP,
        brush_segments,
        border_segments,
        border.do_aa,
    );

    add_corner_segment(
        LayoutRect::from_floats(
            rect.min.x,
            rect.min.y,
            rect.min.x + local_size_tl.width,
            rect.min.y + local_size_tl.height,
        ),
        LayoutRect::from_floats(
            rect.min.x,
            rect.min.y,
            rect.max.x - non_overlapping_widths.right,
            rect.max.y - non_overlapping_widths.bottom
        ),
        border.left,
        border.top,
        LayoutSize::new(widths.left, widths.top),
        border.radius.top_left,
        BorderSegment::TopLeft,
        EdgeAaSegmentMask::TOP | EdgeAaSegmentMask::LEFT,
        rect.top_right(),
        border.radius.top_right,
        rect.bottom_left(),
        border.radius.bottom_left,
        brush_segments,
        border_segments,
        border.do_aa,
    );
    add_corner_segment(
        LayoutRect::from_floats(
            rect.min.x + rect.width() - local_size_tr.width,
            rect.min.y,
            rect.min.x + rect.width(),
            rect.min.y + local_size_tr.height,
        ),
        LayoutRect::from_floats(
            rect.min.x + non_overlapping_widths.left,
            rect.min.y,
            rect.max.x,
            rect.max.y - non_overlapping_widths.bottom,
        ),
        border.top,
        border.right,
        LayoutSize::new(widths.right, widths.top),
        border.radius.top_right,
        BorderSegment::TopRight,
        EdgeAaSegmentMask::TOP | EdgeAaSegmentMask::RIGHT,
        rect.min,
        border.radius.top_left,
        rect.max,
        border.radius.bottom_right,
        brush_segments,
        border_segments,
        border.do_aa,
    );
    add_corner_segment(
        LayoutRect::from_floats(
            rect.min.x + rect.width() - local_size_br.width,
            rect.min.y + rect.height() - local_size_br.height,
            rect.min.x + rect.width(),
            rect.min.y + rect.height(),
        ),
        LayoutRect::from_floats(
            rect.min.x + non_overlapping_widths.left,
            rect.min.y + non_overlapping_widths.top,
            rect.max.x,
            rect.max.y,
        ),
        border.right,
        border.bottom,
        LayoutSize::new(widths.right, widths.bottom),
        border.radius.bottom_right,
        BorderSegment::BottomRight,
        EdgeAaSegmentMask::BOTTOM | EdgeAaSegmentMask::RIGHT,
        rect.bottom_left(),
        border.radius.bottom_left,
        rect.top_right(),
        border.radius.top_right,
        brush_segments,
        border_segments,
        border.do_aa,
    );
    add_corner_segment(
        LayoutRect::from_floats(
            rect.min.x,
            rect.min.y + rect.height() - local_size_bl.height,
            rect.min.x + local_size_bl.width,
            rect.min.y + rect.height(),
        ),
        LayoutRect::from_floats(
            rect.min.x,
            rect.min.y + non_overlapping_widths.top,
            rect.max.x - non_overlapping_widths.right,
            rect.max.y,
        ),
        border.bottom,
        border.left,
        LayoutSize::new(widths.left, widths.bottom),
        border.radius.bottom_left,
        BorderSegment::BottomLeft,
        EdgeAaSegmentMask::BOTTOM | EdgeAaSegmentMask::LEFT,
        rect.max,
        border.radius.bottom_right,
        rect.min,
        border.radius.top_left,
        brush_segments,
        border_segments,
        border.do_aa,
    );
}

/// Computes the maximum scale that we allow for this set of border parameters.
/// capping the scale will result in rendering very large corners at a lower
/// resolution and stretching them, so they will have the right shape, but
/// blurrier.
pub fn get_max_scale_for_border(
    border_data: &NormalBorderData,
) -> LayoutToDeviceScale {
    let mut r = 1.0;
    for segment in &border_data.border_segments {
        let size = segment.local_task_size;
        r = size.width.max(size.height.max(r));
    }

    LayoutToDeviceScale::new(MAX_BORDER_RESOLUTION as f32 / r)
}

fn add_segment(
    task_rect: DeviceRect,
    style0: BorderStyle,
    style1: BorderStyle,
    color0: ColorF,
    color1: ColorF,
    segment: BorderSegment,
    instances: &mut Vec<BorderInstance>,
    widths: DeviceSize,
    radius: DeviceSize,
    do_aa: bool,
    h_adjacent_corner_outer: DevicePoint,
    h_adjacent_corner_radius: DeviceSize,
    v_adjacent_corner_outer: DevicePoint,
    v_adjacent_corner_radius: DeviceSize,
) {
    let base_flags = (segment as i32) |
                     ((style0 as i32) << 8) |
                     ((style1 as i32) << 16) |
                     ((do_aa as i32) << 28);

    let base_instance = BorderInstance {
        task_origin: DevicePoint::zero(),
        local_rect: task_rect,
        flags: base_flags,
        color0: color0.premultiplied(),
        color1: color1.premultiplied(),
        widths,
        radius,
        clip_params: [0.0; 8],
    };

    match segment {
        BorderSegment::TopLeft |
        BorderSegment::TopRight |
        BorderSegment::BottomLeft |
        BorderSegment::BottomRight => {
            // TODO(gw): Similarly to the old border code, we don't correctly handle a a corner
            //           that is dashed on one edge, and dotted on another. We can handle this
            //           in the future by submitting two instances, each one with one side
            //           color set to have an alpha of 0.
            if (style0 == BorderStyle::Dotted && style1 == BorderStyle::Dashed) ||
               (style0 == BorderStyle::Dashed && style0 == BorderStyle::Dotted) {
                warn!("TODO: Handle a corner with dotted / dashed transition.");
            }

            let dashed_or_dotted_corner = match style0 {
                BorderStyle::Dashed => {
                    write_dashed_corner_instances(
                        radius,
                        widths,
                        segment,
                        &base_instance,
                        instances,
                    )
                }
                BorderStyle::Dotted => {
                    write_dotted_corner_instances(
                        radius,
                        widths,
                        segment,
                        &base_instance,
                        instances,
                    )
                }
                _ => Err(()),
            };

            if dashed_or_dotted_corner.is_err() {
                let clip_params = [
                    h_adjacent_corner_outer.x,
                    h_adjacent_corner_outer.y,
                    h_adjacent_corner_radius.width,
                    h_adjacent_corner_radius.height,
                    v_adjacent_corner_outer.x,
                    v_adjacent_corner_outer.y,
                    v_adjacent_corner_radius.width,
                    v_adjacent_corner_radius.height,
                ];

                instances.push(BorderInstance {
                    clip_params,
                    ..base_instance
                });
            }
        }
        BorderSegment::Top |
        BorderSegment::Bottom |
        BorderSegment::Right |
        BorderSegment::Left => {
            let is_vertical = segment == BorderSegment::Left ||
                              segment == BorderSegment::Right;

            match style0 {
                BorderStyle::Dashed => {
                    let (x, y) = if is_vertical {
                        let half_dash_size = task_rect.height() * 0.25;
                        (0., half_dash_size)
                    } else {
                        let half_dash_size = task_rect.width() * 0.25;
                        (half_dash_size, 0.)
                    };

                    instances.push(BorderInstance {
                        flags: base_flags | ((BorderClipKind::DashEdge as i32) << 24),
                        clip_params: [
                            x, y, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
                        ],
                        ..base_instance
                    });
                }
                BorderStyle::Dotted => {
                    let (x, y, r) = if is_vertical {
                        (widths.width * 0.5,
                         widths.width,
                         widths.width * 0.5)
                    } else {
                        (widths.height,
                         widths.height * 0.5,
                         widths.height * 0.5)
                    };

                    instances.push(BorderInstance {
                        flags: base_flags | ((BorderClipKind::Dot as i32) << 24),
                        clip_params: [
                            x, y, r, 0.0, 0.0, 0.0, 0.0, 0.0,
                        ],
                        ..base_instance
                    });
                }
                _ => {
                    instances.push(base_instance);
                }
            }
        }
    }
}

/// Add a corner segment (if valid) to the list of
/// border segments for this primitive.
fn add_corner_segment(
    image_rect: LayoutRect,
    non_overlapping_rect: LayoutRect,
    side0: BorderSide,
    side1: BorderSide,
    widths: LayoutSize,
    radius: LayoutSize,
    segment: BorderSegment,
    edge_flags: EdgeAaSegmentMask,
    h_adjacent_corner_outer: LayoutPoint,
    h_adjacent_corner_radius: LayoutSize,
    v_adjacent_corner_outer: LayoutPoint,
    v_adjacent_corner_radius: LayoutSize,
    brush_segments: &mut Vec<BrushSegment>,
    border_segments: &mut Vec<BorderSegmentInfo>,
    do_aa: bool,
) {
    if side0.color.a <= 0.0 && side1.color.a <= 0.0 {
        return;
    }

    if widths.width <= 0.0 && widths.height <= 0.0 {
        return;
    }

    if side0.style.is_hidden() && side1.style.is_hidden() {
        return;
    }

    let segment_rect = match image_rect.intersection(&non_overlapping_rect) {
        Some(rect) => rect,
        None => {
            return;
        }
    };

    let texture_rect = segment_rect
        .translate(-image_rect.min.to_vector())
        .scale(1.0 / image_rect.width(), 1.0 / image_rect.height());

    brush_segments.push(
        BrushSegment::new(
            segment_rect,
            /* may_need_clip_mask = */ true,
            edge_flags,
            [texture_rect.min.x, texture_rect.min.y, texture_rect.max.x, texture_rect.max.y],
            BrushFlags::SEGMENT_RELATIVE | BrushFlags::SEGMENT_TEXEL_RECT,
        )
    );

    // If the radii of the adjacent corners do not overlap with this segment,
    // then set the outer position to this segment's corner and the radii to zero.
    // That way the cache key is unaffected by non-overlapping corners, resulting
    // in fewer misses.
    let (h_corner_outer, h_corner_radius) = match segment {
        BorderSegment::TopLeft => {
            if h_adjacent_corner_outer.x - h_adjacent_corner_radius.width < image_rect.max.x {
                (h_adjacent_corner_outer, h_adjacent_corner_radius)
            } else {
                (LayoutPoint::new(image_rect.max.x, image_rect.min.y), LayoutSize::zero())
            }
        }
        BorderSegment::TopRight => {
            if h_adjacent_corner_outer.x + h_adjacent_corner_radius.width > image_rect.min.x {
                (h_adjacent_corner_outer, h_adjacent_corner_radius)
            } else {
                (LayoutPoint::new(image_rect.min.x, image_rect.min.y), LayoutSize::zero())
            }
        }
        BorderSegment::BottomRight => {
            if h_adjacent_corner_outer.x + h_adjacent_corner_radius.width > image_rect.min.x {
                (h_adjacent_corner_outer, h_adjacent_corner_radius)
            } else {
                (LayoutPoint::new(image_rect.min.x, image_rect.max.y), LayoutSize::zero())
            }
        }
        BorderSegment::BottomLeft => {
            if h_adjacent_corner_outer.x - h_adjacent_corner_radius.width < image_rect.max.x {
                (h_adjacent_corner_outer, h_adjacent_corner_radius)
            } else {
                (image_rect.max, LayoutSize::zero())
            }
        }
        _ => unreachable!()
    };

    let (v_corner_outer, v_corner_radius) = match segment {
        BorderSegment::TopLeft => {
            if v_adjacent_corner_outer.y - v_adjacent_corner_radius.height < image_rect.max.y {
                (v_adjacent_corner_outer, v_adjacent_corner_radius)
            } else {
                (LayoutPoint::new(image_rect.min.x, image_rect.max.y), LayoutSize::zero())
            }
        }
        BorderSegment::TopRight => {
            if v_adjacent_corner_outer.y - v_adjacent_corner_radius.height < image_rect.max.y {
                (v_adjacent_corner_outer, v_adjacent_corner_radius)
            } else {
                (image_rect.max, LayoutSize::zero())
            }
        }
        BorderSegment::BottomRight => {
            if v_adjacent_corner_outer.y + v_adjacent_corner_radius.height > image_rect.min.y {
                (v_adjacent_corner_outer, v_adjacent_corner_radius)
            } else {
                (LayoutPoint::new(image_rect.max.x, image_rect.min.y), LayoutSize::zero())
            }
        }
        BorderSegment::BottomLeft => {
            if v_adjacent_corner_outer.y + v_adjacent_corner_radius.height > image_rect.min.y {
                (v_adjacent_corner_outer, v_adjacent_corner_radius)
            } else {
                (LayoutPoint::new(image_rect.min.x, image_rect.min.y), LayoutSize::zero())
            }
        }
        _ => unreachable!()
    };

    border_segments.push(BorderSegmentInfo {
        local_task_size: image_rect.size(),
        cache_key: BorderSegmentCacheKey {
            do_aa,
            side0: side0.into(),
            side1: side1.into(),
            segment,
            radius: radius.to_au(),
            size: widths.to_au(),
            h_adjacent_corner_outer: (h_corner_outer - image_rect.min).to_point().to_au(),
            h_adjacent_corner_radius: h_corner_radius.to_au(),
            v_adjacent_corner_outer: (v_corner_outer - image_rect.min).to_point().to_au(),
            v_adjacent_corner_radius: v_corner_radius.to_au(),
        },
    });
}

/// Add an edge segment (if valid) to the list of
/// border segments for this primitive.
fn add_edge_segment(
    image_rect: LayoutRect,
    edge_info: &EdgeInfo,
    side: BorderSide,
    width: f32,
    segment: BorderSegment,
    edge_flags: EdgeAaSegmentMask,
    brush_segments: &mut Vec<BrushSegment>,
    border_segments: &mut Vec<BorderSegmentInfo>,
    do_aa: bool,
) {
    if side.color.a <= 0.0 {
        return;
    }

    if side.style.is_hidden() {
        return;
    }

    let (size, brush_flags) = match segment {
        BorderSegment::Left | BorderSegment::Right => {
            (LayoutSize::new(width, edge_info.stretch_size), BrushFlags::SEGMENT_REPEAT_Y)
        }
        BorderSegment::Top | BorderSegment::Bottom => {
            (LayoutSize::new(edge_info.stretch_size, width), BrushFlags::SEGMENT_REPEAT_X)
        }
        _ => {
            unreachable!();
        }
    };

    if image_rect.width() <= 0. || image_rect.height() <= 0. {
        return;
    }

    brush_segments.push(
        BrushSegment::new(
            image_rect,
            /* may_need_clip_mask = */ true,
            edge_flags,
            [0.0, 0.0, size.width, size.height],
            BrushFlags::SEGMENT_RELATIVE | brush_flags,
        )
    );

    border_segments.push(BorderSegmentInfo {
        local_task_size: size,
        cache_key: BorderSegmentCacheKey {
            do_aa,
            side0: side.into(),
            side1: side.into(),
            radius: LayoutSizeAu::zero(),
            size: size.to_au(),
            segment,
            h_adjacent_corner_outer: LayoutPointAu::zero(),
            h_adjacent_corner_radius: LayoutSizeAu::zero(),
            v_adjacent_corner_outer: LayoutPointAu::zero(),
            v_adjacent_corner_radius: LayoutSizeAu::zero(),
        },
    });
}

/// Build the set of border instances needed to draw a border
/// segment into the render task cache.
pub fn build_border_instances(
    cache_key: &BorderSegmentCacheKey,
    cache_size: DeviceIntSize,
    border: &ApiNormalBorder,
    scale: LayoutToDeviceScale,
) -> Vec<BorderInstance> {
    let mut instances = Vec::new();

    let (side0, side1, flip0, flip1) = match cache_key.segment {
        BorderSegment::Left => (&border.left, &border.left, false, false),
        BorderSegment::Top => (&border.top, &border.top, false, false),
        BorderSegment::Right => (&border.right, &border.right, true, true),
        BorderSegment::Bottom => (&border.bottom, &border.bottom, true, true),
        BorderSegment::TopLeft => (&border.left, &border.top, false, false),
        BorderSegment::TopRight => (&border.top, &border.right, false, true),
        BorderSegment::BottomRight => (&border.right, &border.bottom, true, true),
        BorderSegment::BottomLeft => (&border.bottom, &border.left, true, false),
    };

    let style0 = if side0.style.is_hidden() {
        side1.style
    } else {
        side0.style
    };
    let style1 = if side1.style.is_hidden() {
        side0.style
    } else {
        side1.style
    };

    let color0 = side0.border_color(flip0);
    let color1 = side1.border_color(flip1);

    let widths = (LayoutSize::from_au(cache_key.size) * scale).ceil();
    let radius = (LayoutSize::from_au(cache_key.radius) * scale).ceil();

    let h_corner_outer = (LayoutPoint::from_au(cache_key.h_adjacent_corner_outer) * scale).round();
    let h_corner_radius = (LayoutSize::from_au(cache_key.h_adjacent_corner_radius) * scale).ceil();
    let v_corner_outer = (LayoutPoint::from_au(cache_key.v_adjacent_corner_outer) * scale).round();
    let v_corner_radius = (LayoutSize::from_au(cache_key.v_adjacent_corner_radius) * scale).ceil();

    add_segment(
        DeviceRect::from_size(cache_size.to_f32()),
        style0,
        style1,
        color0,
        color1,
        cache_key.segment,
        &mut instances,
        widths,
        radius,
        border.do_aa,
        h_corner_outer,
        h_corner_radius,
        v_corner_outer,
        v_corner_radius,
    );

    instances
}

impl NinePatchDescriptor {
    pub fn create_segments(
        &self,
        size: LayoutSize,
    ) -> Vec<BrushSegment> {
        let rect = LayoutRect::from_size(size);

        // Calculate the local texel coords of the slices.
        let px0 = 0.0;
        let px1 = self.slice.left as f32 / self.width as f32;
        let px2 = (self.width as f32 - self.slice.right as f32) / self.width as f32;
        let px3 = 1.0;

        let py0 = 0.0;
        let py1 = self.slice.top as f32 / self.height as f32;
        let py2 = (self.height as f32 - self.slice.bottom as f32) / self.height as f32;
        let py3 = 1.0;

        let tl_outer = LayoutPoint::new(rect.min.x, rect.min.y);
        let tl_inner = tl_outer + vec2(self.widths.left, self.widths.top);

        let tr_outer = LayoutPoint::new(rect.min.x + rect.width(), rect.min.y);
        let tr_inner = tr_outer + vec2(-self.widths.right, self.widths.top);

        let bl_outer = LayoutPoint::new(rect.min.x, rect.min.y + rect.height());
        let bl_inner = bl_outer + vec2(self.widths.left, -self.widths.bottom);

        let br_outer = rect.max;

        let br_inner = br_outer - vec2(self.widths.right, self.widths.bottom);

        fn add_segment(
            segments: &mut Vec<BrushSegment>,
            rect: LayoutRect,
            uv_rect: TexelRect,
            repeat_horizontal: RepeatMode,
            repeat_vertical: RepeatMode,
            extra_flags: BrushFlags,
        ) {
            if uv_rect.uv1.x <= uv_rect.uv0.x || uv_rect.uv1.y <= uv_rect.uv0.y {
                return;
            }

            // Use segment relative interpolation for all
            // instances in this primitive.
            let mut brush_flags =
                BrushFlags::SEGMENT_RELATIVE |
                BrushFlags::SEGMENT_TEXEL_RECT |
                extra_flags;

            // Enable repeat modes on the segment.
            if repeat_horizontal == RepeatMode::Repeat {
                brush_flags |= BrushFlags::SEGMENT_REPEAT_X | BrushFlags::SEGMENT_REPEAT_X_CENTERED;
            } else if repeat_horizontal == RepeatMode::Round {
                brush_flags |= BrushFlags::SEGMENT_REPEAT_X | BrushFlags::SEGMENT_REPEAT_X_ROUND;
            }

            if repeat_vertical == RepeatMode::Repeat {
                brush_flags |= BrushFlags::SEGMENT_REPEAT_Y | BrushFlags::SEGMENT_REPEAT_Y_CENTERED;
            } else if repeat_vertical == RepeatMode::Round {
                brush_flags |= BrushFlags::SEGMENT_REPEAT_Y | BrushFlags::SEGMENT_REPEAT_Y_ROUND;
            }

            let segment = BrushSegment::new(
                rect,
                true,
                EdgeAaSegmentMask::empty(),
                [
                    uv_rect.uv0.x,
                    uv_rect.uv0.y,
                    uv_rect.uv1.x,
                    uv_rect.uv1.y,
                ],
                brush_flags,
            );

            segments.push(segment);
        }

        // Build the list of image segments
        let mut segments = Vec::new();

        // Top left
        add_segment(
            &mut segments,
            LayoutRect::from_floats(tl_outer.x, tl_outer.y, tl_inner.x, tl_inner.y),
            TexelRect::new(px0, py0, px1, py1),
            RepeatMode::Stretch,
            RepeatMode::Stretch,
            BrushFlags::empty(),
        );
        // Top right
        add_segment(
            &mut segments,
            LayoutRect::from_floats(tr_inner.x, tr_outer.y, tr_outer.x, tr_inner.y),
            TexelRect::new(px2, py0, px3, py1),
            RepeatMode::Stretch,
            RepeatMode::Stretch,
            BrushFlags::empty(),
        );
        // Bottom right
        add_segment(
            &mut segments,
            LayoutRect::from_floats(br_inner.x, br_inner.y, br_outer.x, br_outer.y),
            TexelRect::new(px2, py2, px3, py3),
            RepeatMode::Stretch,
            RepeatMode::Stretch,
            BrushFlags::empty(),
        );
        // Bottom left
        add_segment(
            &mut segments,
            LayoutRect::from_floats(bl_outer.x, bl_inner.y, bl_inner.x, bl_outer.y),
            TexelRect::new(px0, py2, px1, py3),
            RepeatMode::Stretch,
            RepeatMode::Stretch,
            BrushFlags::empty(),
        );

        // Center
        if self.fill {
            add_segment(
                &mut segments,
                LayoutRect::from_floats(tl_inner.x, tl_inner.y, tr_inner.x, bl_inner.y),
                TexelRect::new(px1, py1, px2, py2),
                self.repeat_horizontal,
                self.repeat_vertical,
                BrushFlags::SEGMENT_NINEPATCH_MIDDLE,
            );
        }

        // Add edge segments.

        // Top
        add_segment(
            &mut segments,
            LayoutRect::from_floats(tl_inner.x, tl_outer.y, tr_inner.x, tl_inner.y),
            TexelRect::new(px1, py0, px2, py1),
            self.repeat_horizontal,
            RepeatMode::Stretch,
            BrushFlags::empty(),
        );
        // Bottom
        add_segment(
            &mut segments,
            LayoutRect::from_floats(bl_inner.x, bl_inner.y, br_inner.x, bl_outer.y),
            TexelRect::new(px1, py2, px2, py3),
            self.repeat_horizontal,
            RepeatMode::Stretch,
            BrushFlags::empty(),
        );
        // Left
        add_segment(
            &mut segments,
            LayoutRect::from_floats(tl_outer.x, tl_inner.y, tl_inner.x, bl_inner.y),
            TexelRect::new(px0, py1, px1, py2),
            RepeatMode::Stretch,
            self.repeat_vertical,
            BrushFlags::empty(),
        );
        // Right
        add_segment(
            &mut segments,
            LayoutRect::from_floats(tr_inner.x, tr_inner.y, br_outer.x, br_inner.y),
            TexelRect::new(px2, py1, px3, py2),
            RepeatMode::Stretch,
            self.repeat_vertical,
            BrushFlags::empty(),
        );

        segments
    }
}

[ Dauer der Verarbeitung: 0.39 Sekunden  (vorverarbeitet)  ]

                                                                                                                                                                                                                                                                                                                                                                                                     


Neuigkeiten

     Aktuelles
     Motto des Tages

Software

     Produkte
     Quellcodebibliothek

Aktivitäten

     Artikel über Sicherheit
     Anleitung zur Aktivierung von SSL

Muße

     Gedichte
     Musik
     Bilder

Jenseits des Üblichen ....
    

Besucherstatistik

Besucherstatistik

Monitoring

Montastic status badge