File: cairo_sub_parts_message_base.rb

package info (click to toggle)
mikutter 5.0.4%2Bdfsg1-2
  • links: PTS, VCS
  • area: main
  • in suites: bookworm
  • size: 9,700 kB
  • sloc: ruby: 21,307; sh: 181; makefile: 19
file content (526 lines) | stat: -rw-r--r-- 18,666 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
# -*- coding: utf-8 -*-

require 'gtk3'
require 'cairo'

=begin rdoc
ナウい引用っぽく _Cairo::MiraclePainter_ のSubPartsとして別の _Message_ を表示するSubPartsを作るときの基底クラス。

= 使い方
このクラスを継承しましょう。
そして、以下のドキュメントを参考に、必要なメソッドをオーバライドします。
=end
class Gdk::SubPartsMessageBase < Gdk::SubParts
  extend Memoist

  DEFAULT_ICON_SIZE = 32
  WHITE = [1.0, 1.0, 1.0].freeze

  # SubPartsに表示する _Message_ 。
  # 複数表示可能なので、それらを上に表示されるものから順番に返す。
  # サブクラスで処理を実装すること。
  # このメソッドはサブパーツの描画中に何回も呼ばれるので、キャッシュなどで高速化に努めること。
  # ==== Return
  # _Array_ :: このSubParts上に表示する _Message_
  def messages
    [] end

  # :nodoc:
  memoize def score(message)
    Plugin[:gtk3].score_of(message)
  end

  # ヘッダの左の、Screen name、名前が表示されている場所に表示するテキスト。
  # オーバライドしなければ、 _message_ の投稿者のscreen nameと名前が表示される。
  # nilを返した場合、ヘッダは表示されない。この場合、ヘッダ右も表示されない。
  # ==== Args
  # [message] Message 表示するMessage
  # ==== Return
  # 次の3つの値またはnil(ヘッダ左を使用しない場合)
  # [String] 表示する文字列
  # [Pango::FontDescription] フォント情報
  # [Pango::Attribute] マークアップ情報
  def header_left_content(message)
    user = message.user
    if user.respond_to?(:idname)
      attr_list, text = Pango.parse_markup("<b>#{Pango.escape(user.idname)}</b> #{Pango.escape(user.name || '')}")
    else
      attr_list, text = Pango.parse_markup(Pango.escape(user.name || ''))
    end
    return text, header_left_font(message), attr_list
  end

  # ヘッダ左に使用するフォントを返す
  # ==== Args
  # [message] Message 表示するMessage
  # ==== Return
  # [Pango::FontDescription] フォント情報
  def header_left_font(message)
    default_font end

  # ヘッダの右の、タイムスタンプが表示されているところに表示するテキスト。
  # オーバーライドしなければ、 _message_ のタイムスタンプが表示される。
  # 表示される時に、 Pango.escape を通るので、この戻り値がエスケープを考慮する必要はないが、装飾を指定することはできない。
  # ==== Args
  # [message] Message 対象のMessage
  # ==== Return
  # [String] 表示する文字列。
  def header_right_text(message)
    now = Time.now
    if message[:created].year == now.year && message[:created].month == now.month && message[:created].day == now.day
      message[:created].strftime('%H:%M:%S'.freeze)
    else
      message[:created].strftime('%Y/%m/%d %H:%M:%S'.freeze) end end

  # Gdk::SubPartsMessageBase#header_right_text にマークアップを足した文字列を返す。
  # 通常は header_right_text をオーバライドするようにし、
  # テキストがエスケープされるのが問題になる場合は、こちらをオーバライドする。
  # nilを返した場合、ヘッダ右は表示されない。この場合、ヘッダ左のみが表示される。
  # ヘッダ自体を消す方法については、 Gdk::SubPartsMessageBase#header_left_text を参照
  # ==== Args
  # [message] Message 対象のMessage
  # ==== Return
  # 次の3つの値またはnil(ヘッダ右を使用しない場合)
  # [String] 表示する文字列
  # [Pango::FontDescription] フォント情報
  # [Pango::Attribute] マークアップ情報
  def header_right_content(message)
    attr_list, text = Pango.parse_markup("<span foreground=\"#999999\">#{Pango.escape(header_right_text(message))}</span>")
    return text, header_right_font(message), attr_list end

  # ヘッダ右に使用するフォントを返す
  # ==== Args
  # [message] Message 表示するMessage
  # ==== Return
  # [Pango::FontDescription] フォント情報
  def header_right_font(message)
    default_font end

  # SubParts内の _Message_ の左上に表示するバッジ。
  # サブクラスで処理を実装すること。
  # ==== Args
  # [message] Message
  # ==== Return
  # 以下の値のいずれか一つ
  # GdkPixbuf::Pixbuf :: _message_ の左上に表示するバッジ画像
  # nil :: バッジを表示しない
  def badge(message)
    nil end

  # 表示している _Message_ がクリックされた時、その _Message_ を引数に呼ばれる。
  # サブクラスで処理を実装すること。
  # ==== Args
  # [e] Gdk::EventButton クリックイベント
  # [message] Message クリックされた _Message_
  def on_click(e, message)
  end

  # SubParts内の _Diva::Model_ の背景色を返す
  # ==== Args
  # [model] Diva::Model
  # ==== Return
  # Array :: red, green, blueの配列。各要素は0.0..1.0の範囲。
  def background_color(model)
    color = Plugin.filtering(:message_bg_color, model, nil).last
    rgb(color || WHITE)
  end

  # SubParts内の _Message_ の枠の色を返す
  # ==== Args
  # [message] Message
  # ==== Return
  # Array :: red, green, blueの配列。各要素は0.0..1.0の範囲。
  def edge_color(message)
    [0.5]*3 end

  # GTK2のGtk::ColorとGTK3のGdk::RGBAをRGBの_Float_値に変換する
  def rgb(color)
    r, g, b = color
    return [r, g, b] if r.is_a? Float
    [r.fdiv(65536), g.fdiv(65536), b.fdiv(65536)].freeze
  end

  # アイコンのサイズを返す。
  # ==== Return
  # [Gdk::Rectangle] サイズ(px)。xとyは無視され、widthとheightのみが利用される
  # [nil] アイコンを表示しない
  def icon_size
    Gdk::Rectangle.new(0, 0, Gdk.scale(DEFAULT_ICON_SIZE), Gdk.scale(DEFAULT_ICON_SIZE))
  end

  # _message_ の本文のテキスト色を返す
  # ==== Args
  # [message] Message
  # ==== Return
  # Array :: red, green, blueの配列。各要素は0.0..1.0の範囲。
  def main_text_color(message)
    ([0,0,0]).map{ |c| c.to_f / 65536 } end

  # 本文使用するフォントを返す
  # ==== Args
  # [message] Message 表示するMessage
  # ==== Return
  # [Pango::FontDescription] フォント情報
  def main_text_font(message)
    default_font end

  # 本文の最大表示行数を返す。
  # この行数を超えた行は表示されない
  # ==== Args
  # [message] Message 表示するMessage
  # ==== Return
  # Fixnum 行数
  def text_max_line_count(message)
    3 end

  # :nodoc:
  memoize def default_font
    helper.font_description(UserConfig[:reply_text_font])
  end

  def margin
    Gdk.scale(@margin)
  end

  def edge
    Gdk.scale(@edge)
  end

  # Fixnum 枠線の太さ(px)
  def border_weight
    Gdk.scale(@border_weight)
  end

  # Fixnum バッジの半径(px)
  def badge_radius
    Gdk.scale(@badge_radius)
  end

  # :nodoc:
  def initialize(*args)
    super
    @margin, @edge, @border_weight, @badge_radius = 2, 8, 1, 6 end

  # :nodoc:
  def render_messages
    if not helper.destroyed?
      helper.queue_allocate
      helper.ssc(:clicked) { |_, ev|
        x, y = ev.x, ev.y
        ofsty = helper.mainpart_height
        helper.subparts.each { |part|
          break if part == self
          ofsty += part.height }
        if ofsty <= y and (ofsty + height) >= y
          my = 0
          messages.each { |m|
            my += message_height(m)
            if y <= ofsty + my
              on_click(ev, m)
              break end } end } end end

  # :nodoc:
  def render(context)
    if messages and not messages.empty?
      messages.inject(0) { |base_y, message|
        render_single_message(message, context, base_y) } end end

  # :nodoc:
  def height
    if helper.destroyed?
      0
    else
      messages&.sum(&method(:message_height)) || 0
    end
  end

  private

  def icon_width
    size = icon_size
    if size
      size.width
    else
      0 end end

  def icon_height
    size = icon_size
    if size
      size.height
    else
      0 end end

  def render_single_message(message, context, base_y)
    render_outline(message, context, base_y)
    _header_width, header_height = render_header(message, context, base_y)
    context.save do
      context.translate(margin + edge, margin + edge + base_y)
      render_icon(message, context)
      context.save do
        context.translate(icon_width + margin*2, header_height || 0)
        context.set_source_rgb(*main_text_color(message))
        pango_layout = main_message(message)
        if pango_layout.line_count <= text_max_line_count(message)
          context.show_pango_layout(pango_layout)
        else
          line_height = pango_layout.pixel_size[1] / pango_layout.line_count + pango_layout.spacing / Pango::SCALE
          context.translate(0, line_height*0.75)
          (0...text_max_line_count(message)).map(&pango_layout.method(:get_line)).each do |line|
            context.show_pango_layout_line(line)
            context.translate(0, line_height) end end end
      render_badge(message, context) end

    base_y + message_height(message) end

  def message_height(message)
    header_height = [0, *[header_left(message), header_right(message)].compact.map{|h|
                       h.pixel_size[1]}].max
    [icon_height, (header_height + main_message_height(message))].max + (margin + edge) * 2 end

  def main_message_height(message)
    pango_layout = main_message(message)
    result = pango_layout.pixel_size[1]
    if pango_layout.line_count <= text_max_line_count(message)
      result
    else
      (result / pango_layout.line_count) * text_max_line_count(message) + pango_layout.spacing/Pango::SCALE * 2 end end

  def header_left_context(message)
    PangoCairo::FontMap.default.create_context.tap do |context|
      context.set_font_description(header_left_font(message))
    end
  end

  # ヘッダ(左)のための Pango::Layout のインスタンスを返す
  def header_left(message, context=nil)
    text, _font, attr_list = header_left_content(message)
    if text
      layout = Pango::Layout.new(header_left_context(message))
      layout.attributes = attr_list if attr_list
      layout.text = text
      layout
    end
  end

  def header_right_context(message)
    PangoCairo::FontMap.default.create_context.tap do |context|
      context.set_font_description(header_right_font(message))
    end
  end

  # ヘッダ(右)のための Pango::Layout のインスタンスを返す
  def header_right(message, context=nil)
    text, _font, attr_list = header_right_content(message)
    if text
      layout = Pango::Layout.new(header_right_context(message))
      layout.attributes = attr_list if attr_list
      layout.text = text
      layout.alignment = Pango::Alignment::RIGHT
      layout
    end
  end

  def render_header(message, context, base_y)
    context.save do
      context.translate(icon_width + margin*2 + edge, margin + edge + base_y)
      context.set_source_rgb(0,0,0)
      hl_layout = header_left(message, context)
      if hl_layout
        context.show_pango_layout(hl_layout)
        hl_w, hl_h = hl_layout.pixel_size
        hr_layout = render_header_right(message, context, base_y, hl_w)
        if hr_layout
          [[hl_w, hr_layout.pixel_size[0]].max,
           [hl_h, hr_layout.pixel_size[1]].max]
        else
          [hl_w, hl_h] end end end end

  def render_header_right(message, context, base_y, header_left_width)
    header_w = width - icon_width - margin*3 - edge*2
    hr_layout = header_right(message, context)
    if hr_layout
      context.save do
        context.translate(header_w - hr_layout.pixel_size[0], 0)
        if header_left_width > header_w - hr_layout.pixel_size[0] - 20
          r, g, b = background_color(message)
          grad = Cairo::LinearPattern.new(-20, base_y, hr_layout.pixel_size[0] + 20, base_y)
          grad.add_color_stop_rgba(0.0, r, g, b, 0.0)
          grad.add_color_stop_rgba(20.0 / (hr_layout.pixel_size[0] + 20), r, g, b, 1.0)
          grad.add_color_stop_rgba(1.0, r, g, b, 1.0)
          context.rectangle(-20, 0, hr_layout.pixel_size[0] + 20, hr_layout.pixel_size[1])
          context.set_source(grad)
          context.fill() end
        context.show_pango_layout(hr_layout)
        hr_layout end end end

  def main_message_context
    PangoCairo::FontMap.default.create_context.tap do |context|
      context.set_font_description(default_font)
    end
  end

  # _message_ に対する _Pango::Layout_ を得る。
  # @params message [Diva::Model] 対象Message Model
  # @params _context [nil] 互換性のため
  def main_message(message, _context=nil)
    Pango::Layout.new(main_message_context).tap do |layout|
      layout.width = (width - icon_width - margin*3 - edge*2) * Pango::SCALE
      layout.attributes = description_attr_list(
        message,
        emoji_height: layout.context.font_description.forecast_font_size
      )
      layout.wrap = Pango::WrapMode::CHAR
      layout.text = plain_description(message)
      layout.context.set_shape_renderer do |c, shape, _|
        photo = shape.data
        if photo
          draw_area = layout.index_to_pos(shape.start_index)
          width = draw_area.width / Pango::SCALE
          height = draw_area.height / Pango::SCALE
          pixbuf = photo.load_pixbuf(width: width, height: height) do
            helper.queue_resize
          end
          x = draw_area.x / Pango::SCALE
          y = draw_area.y / Pango::SCALE
          c.translate(x, y)
          c.set_source_pixbuf(pixbuf)
          c.rectangle(0, 0, width, height)
          c.fill
        end
      end
    end
  end

  def render_outline(message, context, base_y)
    render_outline_floating(message, context, base_y) end

  # エッジの描画。
  # 影にblurを入れて、浮いているような感じに
  def render_outline_floating(message, context, base_y, radius: 4, blur: 4)
    x,y,w,h = edge, edge + base_y, width - edge*2, message_height(message) - edge*2
    context.save {
      context.pseudo_blur(blur) {
        context.fill {
          context.set_source_rgb(*edge_color(message))
          context.rounded_rectangle(x,y,w,h, radius) } }
      context.fill {
        context.set_source_rgb(*background_color(message))
        context.rounded_rectangle(x,y,w,h, radius) } } end

  # エッジの描画。
  # 細い線を入れる
  def render_outline_solid(message, context, base_y, radius: 4)
    context.save {
      x,y,w,h = edge, edge + base_y, width - edge*2, message_height(message) - edge*2
      #context.fill {
        context.rounded_rectangle(x,y,w,h, radius)
        context.set_source_rgb(*background_color(message))
        context.fill_preserve
        context.set_line_width(border_weight)
        context.set_source_rgb(*edge_color(message))
        context.stroke } end

  # エッジの描画。
  # 枠線なし
  def render_outline_flat(message, context, base_y, radius: 4)
    context.save {
      x,y,w,h = edge, edge + base_y, width - edge*2, message_height(message) - edge*2
      context.fill {
        context.set_source_rgb(*background_color(message))
        context.rounded_rectangle(x,y,w,h, radius) } } end

  def render_badge(message, context)
    render_badge_floating(message, context) end

  # バッジの描画。
  # 影にblurを入れて、浮いているような感じに
  def render_badge_floating(message, context)
    badge_pixbuf = badge(message)
    if badge_pixbuf
      context.save {
        context.pseudo_blur(4) {
          context.fill {
            context.set_source_rgb(*edge_color(message))
            context.circle(0, 0, badge_radius) } }
        context.fill {
          context.set_source_rgb(*background_color(message))
          context.circle(0, 0, badge_radius) } }
      context.translate(-badge_radius, -badge_radius)
      context.set_source_pixbuf(badge_pixbuf)
      context.paint end end

  # バッジの描画。
  # 細い線を入れる
  def render_badge_solid(message, context)
    badge_pixbuf = badge(message)
    if badge_pixbuf
      context.save {
        context.circle(0, 0, badge_radius)
        context.set_source_rgb(*background_color(message))
        context.fill_preserve
        context.set_source_rgb(*edge_color(message))
        context.set_line_width(border_weight)
        context.stroke }
      context.translate(-badge_radius, -badge_radius)
      context.set_source_pixbuf(badge_pixbuf)
      context.paint end end

  # バッジの描画。
  # 枠線を入れない
  def render_badge_flat(message, context)
    badge_pixbuf = badge(message)
    if badge_pixbuf
      context.fill {
        context.set_source_rgb(*background_color(message))
        context.circle(0, 0, badge_radius) }
      context.translate(-badge_radius, -badge_radius)
      context.set_source_pixbuf(badge_pixbuf)
      context.paint end end

  def render_icon(message, context)
    if icon_size
      context.set_source_pixbuf(main_icon(message))
      context.paint end end

  def main_icon(message)
    message.user.icon.load_pixbuf(width: icon_size.width, height: icon_size.width){ helper.queue_draw }
  end

  # 表示する際に本文に適用すべき装飾オブジェクトを作成する
  # ==== Return
  # Pango::AttrList 本文に適用する装飾
  def description_attr_list(message, attr_list: Pango::AttrList.new, emoji_height: 24)
    score(message).inject(0){|start_index, note|
      end_index = start_index + note.description.bytesize
      if UserConfig[:miraclepainter_expand_custom_emoji] && note.respond_to?(:inline_photo)
        end_index += -note.description.bytesize + 1
        rect = Pango::Rectangle.new(0, 0, emoji_height * Pango::SCALE, emoji_height * Pango::SCALE)
        shape = Pango::AttrShape.new(rect, rect, note.inline_photo)
        shape.start_index = start_index
        shape.end_index = end_index
        attr_list.insert(shape)
      end
      end_index
    }
    attr_list
  end

  # Entityを適用したあとのプレーンテキストを返す。
  # Pangoの都合上、絵文字は1文字で表現する
  def plain_description(message)
    _plain_description[[message, UserConfig[:miraclepainter_expand_custom_emoji]]]
  end

  private memoize def _plain_description
    Hash.new do |h, k|
      message, expand_emoji = k
      h[k] = score(message).map{|note|
        if expand_emoji && note.respond_to?(:inline_photo)
          '.'
        else
          note.description
        end
      }.to_a.join
    end
  end
end