Repository URL to install this package:
Version:
5.0.0 ▾
|
chaco
/
label.py
|
---|
# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX
# All rights reserved.
#
# This software is provided without warranty under the terms of the BSD
# license included in LICENSE.txt and may be redistributed only under
# the conditions described in the aforementioned license. The license
# is also available online at http://www.enthought.com/licenses/BSD.txt
#
# Thanks for using Enthought open source!
""" Defines the Label class.
"""
# Major library imports
from math import cos, sin, pi
from numpy import array, dot
# Enthought library imports
from enable.api import black_color_trait, transparent_color_trait
from kiva.constants import FILL
from kiva.trait_defs.kiva_font_trait import KivaFont
from traits.api import Any, Bool, Float, HasTraits, Int, List, Str, observe
class Label(HasTraits):
"""A label used by overlays.
Label is not a Component; it's just an object encapsulating text settings
and appearance attributes. It can be used by components that need text
labels to store state, perform layout, and render the text.
"""
# The anchor point is the position on the label that is placed at the
# label's position. The label is also rotated relative to this point.
# "Left" refers to the left edge of the text's bounding box (including
# margin), while "center" refers to the horizontal and vertical center
# of the bounding box.
# TODO: Implement this and test thoroughly
# anchor = Enum("left", "right", "top", "bottom", "center",
# "top left", "top right", "bottom left", "bottom right")
#: The label text. Carriage returns (\n) are always connverted into
#: line breaks.
text = Str
#: The angle of rotation of the label.
rotate_angle = Float(0)
#: The color of the label text.
color = black_color_trait
#: The background color of the label.
bgcolor = transparent_color_trait
#: The width of the label border. If it is 0, then it is not shown.
border_width = Int(0)
#: The color of the border.
border_color = black_color_trait
#: Whether or not the border is visible
border_visible = Bool(True)
#: The font of the label text.
font = KivaFont("modern 10")
#: Number of pixels of margin around the label, for both X and Y dimensions.
margin = Int(2)
#: Number of pixels of spacing between lines of text.
line_spacing = Int(5)
#: Number of pixels to limit the width of the label to. Lines which are
#: too long will be broken to fit on word boundaries. Line width is
#: calculated without considering the value of `margin`.
#: A `max_width` of 0.0 means that lines will not be broken.
max_width = Float(0.0)
# ------------------------------------------------------------------------
# Private traits
# ------------------------------------------------------------------------
_bounding_box = List()
_position_cache_valid = Bool(False, transient=True)
_text_needs_fitting = Bool(False)
_line_xpos = Any()
_line_ypos = Any()
_rot_matrix = Any()
def __init__(self, **traits):
super().__init__(**traits)
self._bounding_box = [0, 0]
def get_width_height(self, gc):
"""Returns the width and height of the label, in the rotated frame of
reference.
"""
self._fit_text_to_max_width(gc)
self._calc_line_positions(gc)
width, height = self._bounding_box
return width, height
def get_bounding_box(self, gc):
"""Returns a rectangular bounding box for the Label as (width,height)."""
width, height = self.get_width_height(gc)
if self.rotate_angle in (90.0, 270.0):
return (height, width)
elif self.rotate_angle in (0.0, 180.0):
return (width, height)
else:
angle = self.rotate_angle
return (
abs(width * cos(angle)) + abs(height * sin(angle)),
abs(height * sin(angle)) + abs(width * cos(angle)),
)
def get_bounding_poly(self, gc):
"""Returns a list [(x0,y0), (x1,y1),...] of tuples representing a
polygon that bounds the label.
"""
width, height = self.get_width_height(gc)
offset = array(self.get_bounding_box(gc)) / 2.0
# unrotated points relative to centre
base_points = [
array([[-width / 2.0], [-height / 2.0]]),
array([[-width / 2.0], [height / 2.0]]),
array([[width / 2.0], [height / 2.0]]),
array([[width / 2.0], [-height / 2.0]]),
array([[-width / 2.0], [-height / 2.0]]),
]
# rotate about centre, and offset to bounding box coords
points = [
dot(self.get_rotation_matrix(), point).transpose()[0] + offset
for point in base_points
]
return points
def get_rotation_matrix(self):
return array(
[
[cos(self.rotate_angle), -sin(self.rotate_angle)],
[sin(self.rotate_angle), cos(self.rotate_angle)],
]
)
def draw(self, gc):
"""Draws the label.
This method assumes the graphics context has been translated to the
correct position such that the origin is at the lower left-hand corner
of this text label's box.
"""
# Make sure `max_width` is respected
self._fit_text_to_max_width(gc)
# For this version we're not supporting rotated text.
self._calc_line_positions(gc)
with gc:
bb_width, bb_height = self.get_bounding_box(gc)
# Rotate label about center of bounding box
width, height = self._bounding_box
gc.translate_ctm(bb_width / 2.0, bb_height / 2.0)
gc.rotate_ctm(pi / 180.0 * self.rotate_angle)
gc.translate_ctm(-width / 2.0, -height / 2.0)
# Draw border and fill background
if self.bgcolor != "transparent":
gc.set_fill_color(self.bgcolor_)
gc.draw_rect((0, 0, width, height), FILL)
if self.border_visible and self.border_width > 0:
gc.set_stroke_color(self.border_color_)
gc.set_line_width(self.border_width)
border_offset = (self.border_width - 1) / 2.0
gc.rect(
border_offset,
border_offset,
width - 2 * border_offset,
height - 2 * border_offset,
)
gc.stroke_path()
gc.set_fill_color(self.color_)
gc.set_stroke_color(self.color_)
gc.set_font(self.font)
if self.font.size <= 8.0:
gc.set_antialias(0)
else:
gc.set_antialias(1)
lines = self.text.split("\n")
if self.border_visible:
gc.translate_ctm(self.border_width, self.border_width)
width, height = self.get_width_height(gc)
for i, line in enumerate(lines):
if line == "":
continue
x_offset = round(self._line_xpos[i])
y_offset = round(self._line_ypos[i])
gc.set_text_position(x_offset, y_offset)
gc.show_text(line)
# ------------------------------------------------------------------------
# Trait handlers
# ------------------------------------------------------------------------
def _text_changed(self):
self._text_needs_fitting = self.max_width > 0.0
@observe("font,margin,text,rotate_angle")
def _invalidate_position_cache(self, event):
self._position_cache_valid = False
# ------------------------------------------------------------------------
# Private methods
# ------------------------------------------------------------------------
def _fit_text_to_max_width(self, gc):
"""Break the text into lines whose width is no greater than
`max_width`.
"""
if self._text_needs_fitting:
lines = []
with gc:
gc.set_font(self.font)
for line in self.text.split("\n"):
if line == "":
lines.append(line)
continue
width = gc.get_full_text_extent(line)[0]
if width > self.max_width:
line_words = []
for word in line.split():
line_words.append(word)
test_line = " ".join(line_words)
width = gc.get_full_text_extent(test_line)[0]
if width > self.max_width:
if len(line_words) > 1:
lines.append(" ".join(line_words[:-1]))
line_words = [word]
else:
lines.append(word)
line_words = []
if len(line_words) > 0:
lines.append(" ".join(line_words))
else:
lines.append(line)
self.trait_setq(text="\n".join(lines))
self._text_needs_fitting = False
def _calc_line_positions(self, gc):
if not self._position_cache_valid:
with gc:
gc.set_font(self.font)
# The bottommost line starts at postion (0, 0).
x_pos = []
y_pos = []
self._bounding_box = [0, 0]
margin = self.margin
prev_y_pos = margin
prev_y_height = -self.line_spacing
max_width = 0
for line in self.text.split("\n")[::-1]:
if line != "":
(
width,
height,
descent,
leading,
) = gc.get_full_text_extent(line)
ascent = height - abs(descent)
if width > max_width:
max_width = width
new_y_pos = (
prev_y_pos + prev_y_height + self.line_spacing
)
else:
# For blank lines, we use the height of the previous
# line, if there is one. The width is 0.
leading = 0
if prev_y_height != -self.line_spacing:
new_y_pos = (
prev_y_pos + prev_y_height + self.line_spacing
)
ascent = prev_y_height
else:
new_y_pos = prev_y_pos
ascent = 0
x_pos.append(-leading + margin)
y_pos.append(new_y_pos)
prev_y_pos = new_y_pos
prev_y_height = ascent
self._line_xpos = x_pos[::-1]
self._line_ypos = y_pos[::-1]
border_width = self.border_width if self.border_visible else 0
self._bounding_box[0] = max_width + 2 * margin + 2 * border_width
self._bounding_box[1] = (
prev_y_pos + prev_y_height + margin + 2 * border_width
)
self._position_cache_valid = True