toga-code-editor 0.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,264 @@
1
+ from bisect import bisect_right
2
+
3
+ from rubicon.objc import (
4
+ SEL,
5
+ NSPoint,
6
+ NSRange,
7
+ NSRect,
8
+ ObjCClass,
9
+ objc_method,
10
+ objc_property,
11
+ )
12
+ from toga_cocoa.colors import native_color
13
+ from toga_cocoa.libs import (
14
+ NSAttributedString,
15
+ NSBezelBorder,
16
+ NSColor,
17
+ NSFontAttributeName,
18
+ NSFontManager,
19
+ NSForegroundColorAttributeName,
20
+ NSMutableDictionary,
21
+ NSNotificationCenter,
22
+ NSScrollView,
23
+ NSViewBoundsDidChangeNotification,
24
+ NSViewHeightSizable,
25
+ NSViewWidthSizable,
26
+ )
27
+ from toga_cocoa.widgets.multilinetextinput import MultilineTextInput, TogaTextView
28
+
29
+ from ..highlighting import to_utf16_spans, utf16_line_starts
30
+
31
+ NSRulerView = ObjCClass("NSRulerView")
32
+
33
+ # NSRulerOrientation
34
+ NSVerticalRuler = 1
35
+ # NSFontTraitMask
36
+ NSItalicFontMask = 1 << 0
37
+ NSBoldFontMask = 1 << 1
38
+
39
+ GUTTER_PADDING = 6
40
+
41
+
42
+ class TogaCodeTextView(TogaTextView):
43
+ @objc_method
44
+ def textDidChange_(self, notification) -> None:
45
+ self.interface._schedule_rehighlight()
46
+ self.interface.on_change()
47
+ self.impl.text_changed()
48
+
49
+
50
+ class TogaLineNumberView(NSRulerView):
51
+ impl = objc_property(object, weak=True)
52
+
53
+ @objc_method
54
+ def boundsDidChange_(self, notification) -> None:
55
+ # The text scrolled; the visible line numbers changed with it.
56
+ self.setNeedsDisplay(True)
57
+
58
+ @objc_method
59
+ def drawHashMarksAndLabelsInRect_(self, rect: NSRect) -> None:
60
+ self.impl.draw_line_numbers()
61
+
62
+
63
+ class CodeEditor(MultilineTextInput):
64
+ def create(self):
65
+ # Mirrors toga_cocoa's MultilineTextInput.create(), swapping in
66
+ # TogaCodeTextView, turning off prose features, and adding a ruler.
67
+ self.native = NSScrollView.alloc().init()
68
+ self.native.hasVerticalScroller = True
69
+ self.native.hasHorizontalScroller = False
70
+ self.native.autohidesScrollers = False
71
+ self.native.borderType = NSBezelBorder
72
+
73
+ self.native_text = TogaCodeTextView.alloc().init()
74
+ self.native_text.interface = self.interface
75
+ self.native_text.impl = self
76
+ self.native_text.delegate = self.native_text
77
+
78
+ self.native_text.editable = True
79
+ self.native_text.selectable = True
80
+ self.native_text.allowsUndo = True
81
+ self.native_text.verticallyResizable = True
82
+ self.native_text.horizontallyResizable = False
83
+ self.native_text.usesAdaptiveColorMappingForDarkAppearance = True
84
+ self.native_text.setAutomaticQuoteSubstitutionEnabled(False)
85
+ self.native_text.setAutomaticDashSubstitutionEnabled(False)
86
+ # Code is not prose.
87
+ self.native_text.setContinuousSpellCheckingEnabled(False)
88
+ self.native_text.setGrammarCheckingEnabled(False)
89
+ self.native_text.setAutomaticSpellingCorrectionEnabled(False)
90
+ self.native_text.setAutomaticTextReplacementEnabled(False)
91
+ self.native_text.setAutomaticTextCompletionEnabled(False)
92
+
93
+ self.native_text.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable
94
+ self.native.documentView = self.native_text
95
+
96
+ # Reading layoutManager opts the view into TextKit 1, which is what the
97
+ # gutter's line-fragment queries need. Do it once, up front.
98
+ self.layout_manager = self.native_text.layoutManager
99
+
100
+ self.ruler = TogaLineNumberView.alloc().initWithScrollView(
101
+ self.native, orientation=NSVerticalRuler
102
+ )
103
+ self.ruler.impl = self
104
+ self.ruler.clientView = self.native_text
105
+ self.ruler.reservedThicknessForMarkers = 0
106
+ self.ruler.reservedThicknessForAccessoryView = 0
107
+ # Since macOS 14 views no longer clip by default, and AppKit hands the ruler
108
+ # a dirty rect that reaches the window's top edge, so its rule line would be
109
+ # drawn above the scroll view. Earlier systems always clip.
110
+ if self.ruler.respondsToSelector_(SEL("setClipsToBounds:")):
111
+ self.ruler.clipsToBounds = True
112
+ self.native.hasVerticalRuler = True
113
+ self.native.verticalRulerView = self.ruler
114
+ self.native.rulersVisible = True
115
+
116
+ # Redraw the gutter whenever the clip view scrolls.
117
+ self.native.contentView.postsBoundsChangedNotifications = True
118
+ NSNotificationCenter.defaultCenter.addObserver(
119
+ self.ruler,
120
+ selector=SEL("boundsDidChange:"),
121
+ name=NSViewBoundsDidChangeNotification,
122
+ object=self.native.contentView,
123
+ )
124
+
125
+ # NSTextView.font and .textColor read back the *first character's* attributes
126
+ # once the storage is styled, so the base style is tracked here instead.
127
+ self.base_font = self.native_text.font
128
+ self.base_color = NSColor.textColor
129
+
130
+ self.theme = {}
131
+ self.attributes = {}
132
+ self.spans = []
133
+ self.line_starts = [0]
134
+
135
+ self.add_constraints()
136
+
137
+ # Inherited MultilineTextInput methods that must keep the gutter and colors in sync
138
+
139
+ def set_value(self, value):
140
+ super().set_value(value)
141
+ self.text_changed()
142
+
143
+ def set_font(self, font):
144
+ super().set_font(font)
145
+ self.base_font = font._impl.native
146
+ self.rebuild_attributes()
147
+ self.apply_highlights()
148
+ self.text_changed()
149
+
150
+ def set_color(self, value):
151
+ super().set_color(value)
152
+ self.base_color = native_color(value) or NSColor.textColor
153
+ self.apply_highlights()
154
+
155
+ # CodeEditor backend contract
156
+
157
+ def set_theme(self, theme):
158
+ self.theme = theme
159
+ self.rebuild_attributes()
160
+
161
+ def set_highlights(self, spans):
162
+ self.spans = spans
163
+ self.apply_highlights()
164
+
165
+ def set_show_line_numbers(self, value):
166
+ self.native.rulersVisible = value
167
+
168
+ # Highlighting
169
+
170
+ def rebuild_attributes(self):
171
+ base_font = self.base_font
172
+ manager = NSFontManager.sharedFontManager
173
+ self.attributes = {}
174
+ for kind, style in self.theme.items():
175
+ attributes = NSMutableDictionary.alloc().init()
176
+ attributes[NSForegroundColorAttributeName] = native_color(style.color)
177
+ traits = 0
178
+ if style.bold:
179
+ traits |= NSBoldFontMask
180
+ if style.italic:
181
+ traits |= NSItalicFontMask
182
+ if traits:
183
+ attributes[NSFontAttributeName] = manager.convertFont(
184
+ base_font, toHaveTrait=traits
185
+ )
186
+ self.attributes[kind] = attributes
187
+
188
+ def apply_highlights(self):
189
+ storage = self.native_text.textStorage
190
+ length = storage.length()
191
+ full_range = NSRange(0, length)
192
+ # Between a native edit and the debounced re-highlight the spans describe the
193
+ # previous text, so clamp them: a range past the end raises NSRangeException
194
+ # inside AppKit, which cannot be caught from Python. Convert before editing
195
+ # so a Python error cannot leave the storage mid-edit either.
196
+ spans = [
197
+ (span.start, min(span.end, length), self.attributes.get(span.kind))
198
+ for span in to_utf16_spans(self.get_value(), self.spans)
199
+ if span.start < length
200
+ ]
201
+ storage.beginEditing()
202
+ # Reset to the base font and color, then paint each span.
203
+ storage.addAttribute(
204
+ NSFontAttributeName, value=self.base_font, range=full_range
205
+ )
206
+ storage.addAttribute(
207
+ NSForegroundColorAttributeName, value=self.base_color, range=full_range
208
+ )
209
+ for start, end, attributes in spans:
210
+ if attributes is not None and end > start:
211
+ storage.addAttributes(attributes, range=NSRange(start, end - start))
212
+ storage.endEditing()
213
+
214
+ # Gutter
215
+
216
+ def text_changed(self):
217
+ self.line_starts = utf16_line_starts(self.get_value())
218
+ digits = len(str(len(self.line_starts)))
219
+ label = self.gutter_label("0" * digits)
220
+ thickness = label.size().width + 2 * GUTTER_PADDING
221
+ if self.ruler.ruleThickness != thickness:
222
+ self.ruler.ruleThickness = thickness
223
+ self.ruler.setNeedsDisplay(True)
224
+
225
+ def gutter_label(self, text):
226
+ attributes = NSMutableDictionary.alloc().init()
227
+ attributes[NSFontAttributeName] = self.base_font
228
+ attributes[NSForegroundColorAttributeName] = NSColor.secondaryLabelColor
229
+ return NSAttributedString.alloc().initWithString(text, attributes=attributes)
230
+
231
+ def draw_line_numbers(self):
232
+ layout = self.layout_manager
233
+ container = self.native_text.textContainer
234
+ visible = self.native_text.visibleRect
235
+ inset = self.native_text.textContainerInset
236
+ text_length = self.native_text.textStorage.length()
237
+
238
+ glyph_range = layout.glyphRangeForBoundingRect(
239
+ visible, inTextContainer=container
240
+ )
241
+ char_range = layout.characterRangeForGlyphRange(
242
+ glyph_range, actualGlyphRange=None
243
+ )
244
+ first_line = max(bisect_right(self.line_starts, char_range.location) - 1, 0)
245
+ last_char = char_range.location + char_range.length
246
+ thickness = self.ruler.ruleThickness
247
+
248
+ for number, start in enumerate(
249
+ self.line_starts[first_line:], start=first_line + 1
250
+ ):
251
+ if start > last_char:
252
+ break
253
+ if start == text_length:
254
+ # The empty line after a trailing newline, or an empty document.
255
+ fragment = layout.extraLineFragmentRect
256
+ else:
257
+ glyph = layout.glyphIndexForCharacterAtIndex(start)
258
+ fragment = layout.lineFragmentRectForGlyphAtIndex(
259
+ glyph, effectiveRange=None
260
+ )
261
+ label = self.gutter_label(str(number))
262
+ x = thickness - GUTTER_PADDING - label.size().width
263
+ y = fragment.origin.y - visible.origin.y + inset.height
264
+ label.drawAtPoint(NSPoint(x, y))
@@ -0,0 +1,21 @@
1
+ from toga_dummy.widgets.multilinetextinput import MultilineTextInput
2
+
3
+
4
+ class CodeEditor(MultilineTextInput):
5
+ def create(self):
6
+ self._action("create CodeEditor")
7
+
8
+ def set_theme(self, theme):
9
+ self._set_value("theme", theme)
10
+
11
+ def set_highlights(self, spans):
12
+ self._set_value("highlights", spans)
13
+
14
+ def set_show_line_numbers(self, value):
15
+ self._set_value("show_line_numbers", value)
16
+
17
+ def simulate_change(self):
18
+ # Mirror the real backends: the re-highlight is scheduled before the
19
+ # user's handler runs.
20
+ self.interface._schedule_rehighlight()
21
+ self.interface.on_change()
@@ -0,0 +1,314 @@
1
+ from bisect import bisect_right
2
+
3
+ from rubicon.objc import (
4
+ CGPoint,
5
+ CGRect,
6
+ NSMutableDictionary,
7
+ NSRange,
8
+ objc_method,
9
+ send_message,
10
+ send_super,
11
+ )
12
+ from rubicon.objc.types import NSInteger, UIEdgeInsets
13
+ from toga_iOS.colors import native_color
14
+ from toga_iOS.libs import (
15
+ NSAttributedString,
16
+ NSFontAttributeName,
17
+ NSForegroundColorAttributeName,
18
+ NSLayoutAttributeBottom,
19
+ NSLayoutAttributeLeading,
20
+ NSLayoutAttributeTop,
21
+ NSLayoutAttributeTrailing,
22
+ NSLayoutConstraint,
23
+ NSLayoutRelationEqual,
24
+ UIColor,
25
+ UIFont,
26
+ UIFontDescriptorTraitBold,
27
+ UIFontDescriptorTraitItalic,
28
+ UILabel,
29
+ )
30
+ from toga_iOS.widgets.multilinetextinput import (
31
+ MultilineTextInput,
32
+ TogaMultilineTextView,
33
+ )
34
+
35
+ from ..highlighting import to_utf16_spans, utf16_line_starts
36
+
37
+ UIColor.declare_class_property("labelColor")
38
+ UIColor.declare_class_property("secondaryLabelColor")
39
+
40
+ # UIViewContentMode
41
+ UIViewContentModeRedraw = 3
42
+ # UITextAutocapitalizationType / UITextAutocorrectionType / UITextSpellCheckingType /
43
+ # UITextSmartQuotesType / UITextSmartDashesType / UITextSmartInsertDeleteType
44
+ UITextAutocapitalizationTypeNone = 0
45
+ UITextAutocorrectionTypeNo = 1
46
+ UITextSpellCheckingTypeNo = 1
47
+ UITextSmartQuotesTypeNo = 1
48
+ UITextSmartDashesTypeNo = 1
49
+ UITextSmartInsertDeleteTypeNo = 1
50
+
51
+ GUTTER_PADDING = 6
52
+ PLACEHOLDER_LEADING = 4.0
53
+
54
+
55
+ class TogaCodeTextView(TogaMultilineTextView):
56
+ @objc_method
57
+ def textViewDidChange_(self, text_view):
58
+ self.interface._schedule_rehighlight()
59
+ self.interface.on_change()
60
+ self.impl.text_changed()
61
+
62
+ @objc_method
63
+ def drawRect_(self, rect: CGRect) -> None:
64
+ send_super(__class__, self, "drawRect:", rect, argtypes=[CGRect])
65
+ self.impl.draw_line_numbers()
66
+
67
+
68
+ class CodeEditor(MultilineTextInput):
69
+ def create(self):
70
+ # Mirrors toga_iOS's MultilineTextInput.create(), swapping in
71
+ # TogaCodeTextView and turning off prose features.
72
+ self.native = TogaCodeTextView.alloc().init()
73
+ self.native.interface = self.interface
74
+ self.native.impl = self
75
+ self.native.delegate = self.native
76
+ self.native.contentMode = UIViewContentModeRedraw
77
+ # UITextView forwards its UITextInputTraits setters to an internal object, so
78
+ # rubicon cannot see them as properties and attribute assignment is silently
79
+ # dropped. Send the messages directly.
80
+ for selector, value in (
81
+ ("setAutocapitalizationType:", UITextAutocapitalizationTypeNone),
82
+ ("setAutocorrectionType:", UITextAutocorrectionTypeNo),
83
+ ("setSpellCheckingType:", UITextSpellCheckingTypeNo),
84
+ ("setSmartQuotesType:", UITextSmartQuotesTypeNo),
85
+ ("setSmartDashesType:", UITextSmartDashesTypeNo),
86
+ ("setSmartInsertDeleteType:", UITextSmartInsertDeleteTypeNo),
87
+ ):
88
+ send_message(
89
+ self.native, selector, value, restype=None, argtypes=[NSInteger]
90
+ )
91
+
92
+ # Reading layoutManager opts the view into TextKit 1, which is what the
93
+ # gutter's line-fragment queries need. Do it once, up front.
94
+ self.layout_manager = self.native.layoutManager
95
+
96
+ # Placeholder isn't natively supported, so we create our own
97
+ self.placeholder_label = UILabel.alloc().init()
98
+ self.placeholder_label.translatesAutoresizingMaskIntoConstraints = False
99
+ self.placeholder_label.font = self.native.font
100
+ self.placeholder_label.alpha = 0.5
101
+ self.native.addSubview(self.placeholder_label)
102
+ self.constrain_placeholder_label()
103
+ self.native.placeholder_label = self.placeholder_label
104
+
105
+ # UITextView.font and .textColor read back the *first character's* attributes
106
+ # once the storage is styled, so the base style is tracked here instead.
107
+ self.base_font = self.native.font
108
+ self.base_color = UIColor.labelColor
109
+
110
+ self.theme = {}
111
+ self.attributes = {}
112
+ self.spans = []
113
+ self.line_starts = [0]
114
+ self.show_line_numbers = True
115
+ self.gutter_width = 0
116
+ self.default_inset = self.native.textContainerInset
117
+
118
+ self.add_constraints()
119
+ self.text_changed()
120
+
121
+ def constrain_placeholder_label(self):
122
+ # Same constraints as toga_iOS, but keeping a reference to the leading one
123
+ # so the gutter can push the placeholder right.
124
+ self.placeholder_leading = NSLayoutConstraint.constraintWithItem(
125
+ self.placeholder_label,
126
+ attribute__1=NSLayoutAttributeLeading,
127
+ relatedBy=NSLayoutRelationEqual,
128
+ toItem=self.native,
129
+ attribute__2=NSLayoutAttributeLeading,
130
+ multiplier=1.0,
131
+ constant=PLACEHOLDER_LEADING,
132
+ )
133
+ trailing_constraint = NSLayoutConstraint.constraintWithItem(
134
+ self.placeholder_label,
135
+ attribute__1=NSLayoutAttributeTrailing,
136
+ relatedBy=NSLayoutRelationEqual,
137
+ toItem=self.native,
138
+ attribute__2=NSLayoutAttributeTrailing,
139
+ multiplier=1.0,
140
+ constant=0,
141
+ )
142
+ top_constraint = NSLayoutConstraint.constraintWithItem(
143
+ self.placeholder_label,
144
+ attribute__1=NSLayoutAttributeTop,
145
+ relatedBy=NSLayoutRelationEqual,
146
+ toItem=self.native,
147
+ attribute__2=NSLayoutAttributeTop,
148
+ multiplier=1.0,
149
+ constant=8.0,
150
+ )
151
+ bottom_constraint = NSLayoutConstraint.constraintWithItem(
152
+ self.placeholder_label,
153
+ attribute__1=NSLayoutAttributeBottom,
154
+ relatedBy=NSLayoutRelationEqual,
155
+ toItem=self.native,
156
+ attribute__2=NSLayoutAttributeBottom,
157
+ multiplier=1.0,
158
+ constant=0,
159
+ )
160
+ self.native.addConstraints(
161
+ [
162
+ self.placeholder_leading,
163
+ trailing_constraint,
164
+ top_constraint,
165
+ bottom_constraint,
166
+ ]
167
+ )
168
+
169
+ # Inherited MultilineTextInput methods that must keep the gutter and colors in sync
170
+
171
+ def set_value(self, value):
172
+ super().set_value(value)
173
+ self.text_changed()
174
+
175
+ def set_font(self, font):
176
+ super().set_font(font)
177
+ self.base_font = font._impl.native
178
+ self.rebuild_attributes()
179
+ self.apply_highlights()
180
+ self.text_changed()
181
+
182
+ def set_color(self, value):
183
+ super().set_color(value)
184
+ self.base_color = native_color(value) or UIColor.labelColor
185
+ self.apply_highlights()
186
+
187
+ # CodeEditor backend contract
188
+
189
+ def set_theme(self, theme):
190
+ self.theme = theme
191
+ self.rebuild_attributes()
192
+
193
+ def set_highlights(self, spans):
194
+ self.spans = spans
195
+ self.apply_highlights()
196
+
197
+ def set_show_line_numbers(self, value):
198
+ self.show_line_numbers = value
199
+ self.text_changed()
200
+
201
+ # Highlighting
202
+
203
+ def rebuild_attributes(self):
204
+ base_font = self.base_font
205
+ self.attributes = {}
206
+ for kind, style in self.theme.items():
207
+ attributes = NSMutableDictionary.alloc().init()
208
+ attributes[NSForegroundColorAttributeName] = native_color(style.color)
209
+ traits = 0
210
+ if style.bold:
211
+ traits |= UIFontDescriptorTraitBold
212
+ if style.italic:
213
+ traits |= UIFontDescriptorTraitItalic
214
+ if traits and base_font is not None:
215
+ # If there is no font with the requested traits, this returns None.
216
+ font = UIFont.fontWithDescriptor(
217
+ base_font.fontDescriptor.fontDescriptorWithSymbolicTraits(traits),
218
+ size=base_font.pointSize,
219
+ )
220
+ if font is not None:
221
+ attributes[NSFontAttributeName] = font
222
+ self.attributes[kind] = attributes
223
+
224
+ def apply_highlights(self):
225
+ storage = self.native.textStorage
226
+ length = storage.length()
227
+ full_range = NSRange(0, length)
228
+ # Between a native edit and the debounced re-highlight the spans describe the
229
+ # previous text, so clamp them: a range past the end raises NSRangeException
230
+ # inside UIKit, which cannot be caught from Python.
231
+ spans = [
232
+ (span.start, min(span.end, length), self.attributes.get(span.kind))
233
+ for span in to_utf16_spans(self.get_value(), self.spans)
234
+ if span.start < length
235
+ ]
236
+ storage.beginEditing()
237
+ # Reset to the base font and color, then paint each span.
238
+ if self.base_font is not None:
239
+ storage.addAttribute(
240
+ NSFontAttributeName, value=self.base_font, range=full_range
241
+ )
242
+ storage.addAttribute(
243
+ NSForegroundColorAttributeName, value=self.base_color, range=full_range
244
+ )
245
+ for start, end, attributes in spans:
246
+ if attributes is not None and end > start:
247
+ storage.addAttributes(attributes, range=NSRange(start, end - start))
248
+ storage.endEditing()
249
+
250
+ # Gutter
251
+
252
+ def text_changed(self):
253
+ self.line_starts = utf16_line_starts(self.get_value())
254
+ if self.show_line_numbers:
255
+ digits = len(str(len(self.line_starts)))
256
+ width = self.gutter_label("0" * digits).size().width + 2 * GUTTER_PADDING
257
+ else:
258
+ width = 0
259
+ if width != self.gutter_width:
260
+ self.gutter_width = width
261
+ inset = self.default_inset
262
+ self.native.textContainerInset = UIEdgeInsets(
263
+ inset.top, inset.left + width, inset.bottom, inset.right
264
+ )
265
+ self.placeholder_leading.constant = PLACEHOLDER_LEADING + width
266
+ self.native.setNeedsDisplay()
267
+
268
+ def gutter_label(self, text):
269
+ attributes = NSMutableDictionary.alloc().init()
270
+ if self.base_font is not None:
271
+ attributes[NSFontAttributeName] = self.base_font
272
+ attributes[NSForegroundColorAttributeName] = UIColor.secondaryLabelColor
273
+ return NSAttributedString.alloc().initWithString(text, attributes=attributes)
274
+
275
+ def draw_line_numbers(self):
276
+ if not self.show_line_numbers:
277
+ return
278
+ layout = self.layout_manager
279
+ container = self.native.textContainer
280
+ inset = self.native.textContainerInset
281
+ text_length = self.native.textStorage.length()
282
+
283
+ # bounds.origin is the scroll offset; the container is inset within it.
284
+ bounds = self.native.bounds
285
+ visible = CGRect(
286
+ CGPoint(bounds.origin.x - inset.left, bounds.origin.y - inset.top),
287
+ bounds.size,
288
+ )
289
+ glyph_range = layout.glyphRangeForBoundingRect(
290
+ visible, inTextContainer=container
291
+ )
292
+ char_range = layout.characterRangeForGlyphRange(
293
+ glyph_range, actualGlyphRange=None
294
+ )
295
+ first_line = max(bisect_right(self.line_starts, char_range.location) - 1, 0)
296
+ last_char = char_range.location + char_range.length
297
+
298
+ for number, start in enumerate(
299
+ self.line_starts[first_line:], start=first_line + 1
300
+ ):
301
+ if start > last_char:
302
+ break
303
+ if start == text_length:
304
+ # The empty line after a trailing newline, or an empty document.
305
+ fragment = layout.extraLineFragmentRect
306
+ else:
307
+ glyph = layout.glyphIndexForCharacterAtIndex(start)
308
+ fragment = layout.lineFragmentRectForGlyphAtIndex(
309
+ glyph, effectiveRange=None
310
+ )
311
+ label = self.gutter_label(str(number))
312
+ x = self.gutter_width - GUTTER_PADDING - label.size().width
313
+ y = fragment.origin.y + inset.top
314
+ label.drawAtPoint(CGPoint(x, y))