diff --git a/backend/app/controllers/public/tournament_pages_controller.rb b/backend/app/controllers/public/tournament_pages_controller.rb index 18a23e6..e4c4324 100644 --- a/backend/app/controllers/public/tournament_pages_controller.rb +++ b/backend/app/controllers/public/tournament_pages_controller.rb @@ -38,19 +38,21 @@ module Public .includes( :tournament_group, :tournament_round, + :stream_sessions, home_participant: { logo_file_attachment: :blob }, away_participant: { logo_file_attachment: :blob } ) .order(Arel.sql("scheduled_at ASC NULLS LAST"), created_at: :asc) - @live_sessions = StreamSession + broadcasting = StreamSession .broadcasting - .publicly_listed - .where(platform: "matchlivetv") .includes(:score_state, match: [:home_participant, :away_participant, { team: :club }]) .where(match_id: @tournament.matches.select(:id)) .order(Arel.sql("started_at DESC NULLS LAST"), created_at: :desc) - @live_by_match_id = @live_sessions.index_by(&:match_id) + .to_a + @live_by_match_id = {} + broadcasting.each { |session| @live_by_match_id[session.match_id] ||= session } + @live_sessions = broadcasting.select(&:public_watchable?) @recordings = Recording.ready.publicly_listed .joins(stream_session: :match) diff --git a/backend/app/helpers/public/tournaments_helper.rb b/backend/app/helpers/public/tournaments_helper.rb index b82cc5b..289e82f 100644 --- a/backend/app/helpers/public/tournaments_helper.rb +++ b/backend/app/helpers/public/tournaments_helper.rb @@ -67,7 +67,7 @@ module Public sessions = match.stream_sessions.to_a latest = sessions.max_by(&:created_at) if latest && !latest.status.in?(%w[ended error]) - return :live if latest.status.in?(%w[live reconnecting paused]) + return :live if latest.status.in?(%w[live reconnecting paused connecting]) return :waiting_operator end @@ -106,6 +106,18 @@ module Public :waiting end + def tournament_public_watch_target(session) + return unless session + if session.matchlivetv_platform? && session.publicly_listed? + return [public_live_path(session), t("tournaments.page.watch_live"), {}] + end + if session.youtube_watch_url.present? + return [session.youtube_watch_url, t("live.show.youtube_watch_link"), { target: "_blank", rel: "noopener" }] + end + + nil + end + def tournament_public_score_label(match) return "#{match.home_score}–#{match.away_score}" if match.home_score.present? && match.away_score.present? diff --git a/backend/app/models/stream_session.rb b/backend/app/models/stream_session.rb index ebb7cd2..b0a638e 100644 --- a/backend/app/models/stream_session.rb +++ b/backend/app/models/stream_session.rb @@ -186,6 +186,12 @@ class StreamSession < ApplicationRecord platform == "youtube" && youtube_watch_url.present? end + def public_watchable? + return true if matchlivetv_platform? && publicly_listed? + + youtube_ready? + end + def link_only? privacy_status.in?(%w[unlisted private]) end diff --git a/backend/app/views/layouts/marketing.html.erb b/backend/app/views/layouts/marketing.html.erb index aeeca7c..ef34de4 100644 --- a/backend/app/views/layouts/marketing.html.erb +++ b/backend/app/views/layouts/marketing.html.erb @@ -10,7 +10,7 @@ <%= render "shared/analytics_suppress" %> <%= yield :head %> - + data-ga-id="<%= MatchLiveTv.google_analytics_measurement_id %>"<% end %>> <%= render "shared/cookie_banner" %> @@ -24,7 +24,7 @@ <%= render(@app_store_review_chrome ? "shared/marketing_footer_app_store" : "shared/marketing_footer") %> - + diff --git a/backend/app/views/layouts/marketing_live.html.erb b/backend/app/views/layouts/marketing_live.html.erb index 590c7b1..0110175 100644 --- a/backend/app/views/layouts/marketing_live.html.erb +++ b/backend/app/views/layouts/marketing_live.html.erb @@ -8,7 +8,7 @@ <%= render "shared/meta_tags" %> <%= render "shared/analytics_suppress" %> - + <%= yield :head %> diff --git a/backend/app/views/public/tournament_pages/_board_row.html.erb b/backend/app/views/public/tournament_pages/_board_row.html.erb index 20063be..b47ddd2 100644 --- a/backend/app/views/public/tournament_pages/_board_row.html.erb +++ b/backend/app/views/public/tournament_pages/_board_row.html.erb @@ -37,8 +37,12 @@ - <% if live_session %> - <%= link_to t("tournaments.page.watch_live"), public_live_path(live_session), class: "tournament-board__cta tournament-board__cta--live" %> + <% watch = tournament_public_watch_target(live_session) %> + <% if watch %> + <% url, label, html_opts = watch %> + <%= link_to label, url, { class: "tournament-board__cta tournament-board__cta--live" }.merge(html_opts) %> + <% elsif live_session %> + <%= t("tournaments.page.no_media") %> <% elsif recording %> <%= link_to t("tournaments.page.watch_replay"), public_replay_path(recording.stream_session_id), class: "tournament-board__cta" %> <% else %> diff --git a/backend/app/views/public/tournament_pages/show.html.erb b/backend/app/views/public/tournament_pages/show.html.erb index 7ae53d5..b54bf23 100644 --- a/backend/app/views/public/tournament_pages/show.html.erb +++ b/backend/app/views/public/tournament_pages/show.html.erb @@ -64,7 +64,11 @@ <%= t("live.index.badge_live") %> <% end %> - <%= link_to t("tournaments.page.watch_live"), public_live_path(session), class: "btn-watch" %> + <% watch = tournament_public_watch_target(session) %> + <% if watch %> + <% url, label, html_opts = watch %> + <%= link_to label, url, { class: "btn-watch" }.merge(html_opts) %> + <% end %> <% end %> diff --git a/backend/app/views/public/tournaments/_tab_calendario.html.erb b/backend/app/views/public/tournaments/_tab_calendario.html.erb index f54ff8d..a784d92 100644 --- a/backend/app/views/public/tournaments/_tab_calendario.html.erb +++ b/backend/app/views/public/tournaments/_tab_calendario.html.erb @@ -8,88 +8,105 @@ <% if @matches.any? %>

<%= t("tournaments.hub.calendar_edit_hint") %>

-
- "> - - - - - - - - - - - - - - <% @matches.each do |match| %> - <% form_id = "match-edit-#{match.id}" %> - <% courts = (@tournament.court_list + [match.court]).compact_blank.uniq %> - <%= form_with url: public_tournament_match_path(@tournament, match), method: :patch, html: { id: form_id, class: "visually-hidden" } do %> - <%= hidden_field_tag :tab, @tab, form: form_id %> - <% end %> - - <% if @writable %> - - - - - - - - - <% else %> - - - - - - - - - <% end %> - +

+ + <% end %> - -
<%= t("tournaments.hub.datetime") %><%= t("tournaments.hub.court") %><%= t("tournaments.hub.round") %><%= t("tournaments.hub.home") %><%= t("tournaments.hub.away") %><%= t("tournaments.hub.result") %>
- <%= datetime_local_field_tag "match[scheduled_at]", tournament_datetime_local(match.scheduled_at), - id: "match_at_#{match.id}", form: form_id, required: true, class: "tournament-match-form__datetime" %> - - <%= select_tag "match[court]", options_for_select(courts, match.court), - id: "match_court_#{match.id}", form: form_id, include_blank: true, class: "tournament-match-form__court" %> - <%= tournament_phase_label(match) %><%= render "public/tournaments/match_side_select", match: match, side: :home, form_id: form_id %> - <%= button_to public_swap_tournament_match_path(@tournament, match), method: :post, - class: "tournament-match-swap__btn", - title: t("tournaments.hub.swap_sides"), - form: { class: "tournament-match-swap__form", data: { turbo: false } } do %> - - <%= t("tournaments.hub.swap_sides") %> - <% end %> - <%= render "public/tournaments/match_side_select", match: match, side: :away, form_id: form_id %> - <% if match.played? || match.result_status.to_s.start_with?("walkover") %> - <%= match.home_score %>–<%= match.away_score %> - <% else %> - - <%= number_field_tag :home_score, match.home_score, id: "home_score_#{match.id}", form: form_id, data: { swap_score: "home" } %> - - <%= number_field_tag :away_score, match.away_score, id: "away_score_#{match.id}", form: form_id, data: { swap_score: "away" } %> - - <% end %> - - <%= submit_tag t("tournaments.hub.save_match"), form: form_id, class: "btn btn-secondary tournament-match-form__save" %> - <% if match.deletable? %> - <%= button_to t("matches.index.delete"), public_tournament_match_path(@tournament, match), - method: :delete, class: "btn btn-secondary tournament-match-form__save", - form: { class: "tournament-match-delete-form", data: { turbo_confirm: match.matchup_label } } %> - <% end %> - <%= match.scheduled_at ? l(match.scheduled_at, format: :short) : "—" %><%= match.court || "—" %><%= tournament_phase_label(match) %><%= tournament_team_chip(match.home_participant, name: match.home_display_name) %><%= tournament_team_chip(match.away_participant, name: match.away_display_name) %> +
"> + <% @matches.each do |match| %> + <% form_id = "match-edit-#{match.id}" %> + <% courts = (@tournament.court_list + [match.court]).compact_blank.uniq %> + <%= form_with url: public_tournament_match_path(@tournament, match), method: :patch, html: { id: form_id, class: "visually-hidden" } do %> + <%= hidden_field_tag :tab, @tab, form: form_id %> + <% end %> +
+ <% if @writable %> +
+ + <%= datetime_local_field_tag "match[scheduled_at]", tournament_datetime_local(match.scheduled_at), + id: "match_at_#{match.id}", form: form_id, required: true, class: "tournament-match-form__datetime" %> +
+
+ + <%= select_tag "match[court]", options_for_select(courts, match.court), + id: "match_court_#{match.id}", form: form_id, include_blank: true, class: "tournament-match-form__court" %> +
+
+ <%= t("tournaments.hub.round") %> +

<%= tournament_phase_label(match) %>

+
+
+
+ <%= t("tournaments.hub.home") %> +
<%= render "public/tournaments/match_side_select", match: match, side: :home, form_id: form_id %>
+
+
+ <%= button_to public_swap_tournament_match_path(@tournament, match), method: :post, + class: "tournament-match-swap__btn", + title: t("tournaments.hub.swap_sides"), + form: { class: "tournament-match-swap__form", data: { turbo: false } } do %> + + <%= t("tournaments.hub.swap_sides") %> + <% end %> +
+
+ <%= t("tournaments.hub.away") %> +
<%= render "public/tournaments/match_side_select", match: match, side: :away, form_id: form_id %>
+
+
+ <%= t("tournaments.hub.result") %> + <% if match.played? || match.result_status.to_s.start_with?("walkover") %> +

<%= match.home_score %>–<%= match.away_score %>

+ <% else %> + + <%= number_field_tag :home_score, match.home_score, id: "home_score_#{match.id}", form: form_id, data: { swap_score: "home" } %> + + <%= number_field_tag :away_score, match.away_score, id: "away_score_#{match.id}", form: form_id, data: { swap_score: "away" } %> + + <% end %> +
+
+
+ <%= submit_tag t("tournaments.hub.save_match"), form: form_id, class: "btn btn-secondary tournament-match-form__save" %> + <% if match.deletable? %> + <%= button_to t("matches.index.delete"), public_tournament_match_path(@tournament, match), + method: :delete, class: "btn btn-secondary tournament-match-form__save", + form: { class: "tournament-match-delete-form", data: { turbo_confirm: match.matchup_label } } %> + <% end %> +
+ <% else %> +
+ <%= t("tournaments.hub.datetime") %> +

<%= match.scheduled_at ? l(match.scheduled_at, format: :short) : "—" %>

+
+
+ <%= t("tournaments.hub.court") %> +

<%= match.court || "—" %>

+
+
+ <%= t("tournaments.hub.round") %> +

<%= tournament_phase_label(match) %>

+
+
+
+ <%= t("tournaments.hub.home") %> + <%= tournament_team_chip(match.home_participant, name: match.home_display_name) %> +
+ +
+ <%= t("tournaments.hub.away") %> + <%= tournament_team_chip(match.away_participant, name: match.away_display_name) %> +
+
+ <%= t("tournaments.hub.result") %> +

<% if match.home_score.present? %> <%= match.home_score %>–<%= match.away_score %> <% else %> — <% end %> -

+ + <% end %>
<% else %>

<%= t("tournaments.hub.no_matches") %>

diff --git a/backend/public/marketing.css b/backend/public/marketing.css index d016ad1..c2c4d36 100644 --- a/backend/public/marketing.css +++ b/backend/public/marketing.css @@ -1183,6 +1183,93 @@ body.invite-editor--resizing { .tournament-hub-hint { color: #888; margin: 0 0 12px; } .tournament-table-wrap { overflow-x: auto; margin-top: 8px; } +.tournament-match-list { + display: flex; + flex-direction: column; + gap: 12px; + margin-top: 8px; + min-width: 0; + overflow-x: clip; +} +.tournament-match-row { + display: grid; + grid-template-columns: minmax(0, 1.4fr) minmax(0, 0.9fr) minmax(0, 0.7fr); + gap: 10px 12px; + align-items: end; + min-width: 0; + padding: 14px; + border: 1px solid #2a2a36; + border-radius: 12px; + background: #14141c; +} +.tournament-match-row__field, +.tournament-match-row__side, +.tournament-match-row__score { + min-width: 0; +} +.tournament-match-row__field label, +.tournament-match-row__label { + display: block; + margin-bottom: 6px; + color: #9a9aa8; + font-size: 0.75rem; + letter-spacing: 0.02em; +} +.tournament-match-row__phase { + margin: 0; + padding: 8px 0; + color: #e8e8ee; + font-size: 0.95rem; +} +.tournament-match-row__sides { + grid-column: 1 / -1; + display: grid; + grid-template-columns: minmax(0, 1fr) 44px minmax(0, 1fr) auto; + gap: 8px 10px; + align-items: end; + min-width: 0; +} +.tournament-match-row__actions { + grid-column: 1 / -1; + justify-content: flex-end; +} +.tournament-match-row .tournament-match-form__datetime, +.tournament-match-row .tournament-match-form__court, +.tournament-match-row .tournament-match-form__team { + width: 100%; + max-width: 100%; + min-width: 0; + margin-bottom: 0; + padding: 8px 10px; + font-size: 0.9rem; + box-sizing: border-box; +} +@media (max-width: 799px) { + .tournament-match-row { + grid-template-columns: 1fr 1fr; + } + .tournament-match-row__field--phase { + grid-column: 1 / -1; + } + .tournament-match-row__sides { + grid-template-columns: minmax(0, 1fr) 44px minmax(0, 1fr); + } + .tournament-match-row__score { + grid-column: 1 / -1; + } +} +@media (max-width: 560px) { + .tournament-match-row, + .tournament-match-row__sides { + grid-template-columns: minmax(0, 1fr); + } + .tournament-match-row__field--phase { + grid-column: auto; + } + .tournament-match-swap-col { + justify-self: center; + } +} .tournament-index-actions { display: flex; flex-wrap: wrap; @@ -1205,19 +1292,19 @@ body.invite-editor--resizing { background: #12121a; flex-shrink: 0; } -.tournament-match-table .tournament-match-form__datetime { +table.tournament-match-table .tournament-match-form__datetime { min-width: 190px; margin-bottom: 0; padding: 8px 10px; font-size: 0.9rem; } -.tournament-match-table .tournament-match-form__court { +table.tournament-match-table .tournament-match-form__court { min-width: 120px; margin-bottom: 0; padding: 8px 10px; font-size: 0.9rem; } -.tournament-match-table .tournament-match-form__team { +table.tournament-match-table .tournament-match-form__team { min-width: 160px; max-width: 220px; margin-bottom: 0; @@ -1260,6 +1347,8 @@ body.invite-editor--resizing { padding: 4px 10px; font-size: 0.8rem; margin: 0; + flex-shrink: 0; + white-space: nowrap; } .tournament-match-swap-col { width: 44px; diff --git a/backend/public/tournament-calendar.js b/backend/public/tournament-calendar.js index 1d31242..75394a2 100644 --- a/backend/public/tournament-calendar.js +++ b/backend/public/tournament-calendar.js @@ -10,7 +10,7 @@ event.stopImmediatePropagation(); if (form.dataset.swapBusy === "1") return; - var row = form.closest("tr"); + var row = form.closest(".tournament-match-row") || form.closest("tr"); if (!row) return; form.dataset.swapBusy = "1"; diff --git a/backend/spec/requests/public/tournaments_spec.rb b/backend/spec/requests/public/tournaments_spec.rb index 033273f..a0b0523 100644 --- a/backend/spec/requests/public/tournaments_spec.rb +++ b/backend/spec/requests/public/tournaments_spec.rb @@ -366,6 +366,7 @@ RSpec.describe "Tournaments", type: :request do get public_tournament_path(tournament, tab: "calendario") expect(response.body).to include("tournament-match-swap__btn") + expect(response.body).to include("tournament-match-row") expect(response.body).to include(public_swap_tournament_match_path(tournament, match)) expect(response.body).to include("tournament-calendar.js") @@ -782,6 +783,50 @@ RSpec.describe "Tournaments", type: :request do expect(response.body).to include(public_tournament_page_path(tournament.slug)) end + it "marca In diretta una gara YouTube o in collegamento, non In attesa" do + assign_plan!("premium_full") + login! + tournament = Tournaments::Create.call( + club: club, + attrs: { + name: "Open Diretta YouTube", + sport_key: "basket", + starts_on: Date.current, + ends_on: Date.current, + format_kind: "free", + courts: "Campo A" + } + ) + home = tournament.participants.create!(name: "Squadra 1") + away = tournament.participants.create!(name: "Squadra 3") + match = Tournaments::ScheduleMatch.call( + tournament: tournament, + attrs: { + home_participant_id: home.id, + away_participant_id: away.id, + court: "Campo A", + scheduled_at: 30.minutes.ago + } + ) + post public_publish_tournament_path(tournament) + + session = StreamSession.create!( + match: match, user: coach, platform: "youtube", status: "connecting", + privacy_status: "unlisted", youtube_broadcast_id: "ytLiveBoard1" + ) + get public_tournament_page_path(tournament.slug) + expect(response.body).to include("tournament-live-status--live") + expect(response.body).to include("In diretta") + expect(response.body).not_to include("tournament-live-status--waiting") + expect(response.body).to include("Guarda su YouTube") + expect(response.body).to include(session.youtube_watch_url) + + get public_tournament_path(tournament, tab: "dirette") + expect(response.body).to include("tournament-live-status--live") + expect(response.body).to include("In diretta") + expect(response.body).not_to include("tournament-live-status--waiting_operator") + end + it "marca Terminata una gara con risultato anche se l'orario è ancora futuro" do assign_plan!("premium_full") login! diff --git a/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/streaming/overlay/CompactScoreboardElement.kt b/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/streaming/overlay/CompactScoreboardElement.kt index 8de8e32..eb044ea 100644 --- a/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/streaming/overlay/CompactScoreboardElement.kt +++ b/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/streaming/overlay/CompactScoreboardElement.kt @@ -114,34 +114,38 @@ class CompactScoreboardElement : OverlayElement { val accentColor = if (isHome) compact.homeAccentColor else compact.awayAccentColor val logoUrl = if (isHome) compact.homeLogoUrl else compact.awayLogoUrl val logo = OverlayLogoCache.get(logoUrl) - val rawName = if (isHome) compact.homeTeamName else compact.awayTeamName - val name = ellipsize(rawName.trim(), labelPaint, maxWidth * 0.55f) + val rawName = (if (isHome) compact.homeTeamName else compact.awayTeamName).trim() + val barHalfH = logoSize * 0.45f + val textY = centerY + labelPaint.textSize * 0.35f if (isHome) { var x = rectLeft accentBarPaint.color = accentColor - canvas.drawRect(x, centerY - logoSize * 0.45f, x + barW, centerY + logoSize * 0.45f, accentBarPaint) + canvas.drawRect(x, centerY - barHalfH, x + barW, centerY + barHalfH, accentBarPaint) x += barW + gap if (logo != null) { val dest = RectF(x, centerY - logoSize / 2f, x + logoSize, centerY + logoSize / 2f) canvas.drawBitmap(logo, null, dest, logoPaint) x += logoSize + gap } - canvas.drawText(name, x, centerY + labelPaint.textSize * 0.35f, labelPaint) + val name = ellipsize(rawName, labelPaint, (rectRight - x).coerceAtMost(maxWidth)) + canvas.drawText(name, x, textY, labelPaint) } else { + // Specchio del lato casa: barra, logo, poi nome a sinistra del logo. var x = rectRight accentBarPaint.color = accentColor - canvas.drawRect(x - barW, centerY - logoSize * 0.45f, x, centerY + logoSize * 0.45f, accentBarPaint) + canvas.drawRect(x - barW, centerY - barHalfH, x, centerY + barHalfH, accentBarPaint) x -= barW + gap - labelPaint.textAlign = Paint.Align.RIGHT - canvas.drawText(name, x, centerY + labelPaint.textSize * 0.35f, labelPaint) - labelPaint.textAlign = Paint.Align.LEFT - x -= gap if (logo != null) { x -= logoSize val dest = RectF(x, centerY - logoSize / 2f, x + logoSize, centerY + logoSize / 2f) canvas.drawBitmap(logo, null, dest, logoPaint) + x -= gap } + val name = ellipsize(rawName, labelPaint, (x - rectLeft).coerceAtMost(maxWidth)) + labelPaint.textAlign = Paint.Align.RIGHT + canvas.drawText(name, x, textY, labelPaint) + labelPaint.textAlign = Paint.Align.LEFT } } diff --git a/native/ios/MatchLiveTv/Streaming/Overlay/CompactScoreboardElement.swift b/native/ios/MatchLiveTv/Streaming/Overlay/CompactScoreboardElement.swift index 72449cd..b4ece2f 100644 --- a/native/ios/MatchLiveTv/Streaming/Overlay/CompactScoreboardElement.swift +++ b/native/ios/MatchLiveTv/Streaming/Overlay/CompactScoreboardElement.swift @@ -67,32 +67,37 @@ struct CompactScoreboardElement: OverlayElement { let accent = isHome ? compact.homeAccentColor : compact.awayAccentColor let logoUrl = isHome ? compact.homeLogoUrl : compact.awayLogoUrl let logo = OverlayLogoCache.get(logoUrl) - let rawName = isHome ? compact.homeTeamName : compact.awayTeamName - let name = ellipsize(rawName.trimmingCharacters(in: .whitespacesAndNewlines), font: labelFont, maxWidth: maxWidth * 0.55) + let rawName = (isHome ? compact.homeTeamName : compact.awayTeamName) + .trimmingCharacters(in: .whitespacesAndNewlines) let attrs: [NSAttributedString.Key: Any] = [.font: labelFont, .foregroundColor: UIColor.white] + let textY = centerY + labelFont.pointSize * 0.35 - labelFont.lineHeight + let barHalfH = CGFloat(logoSize) * 0.45 if isHome { var x = rectLeft context.setFillColor(accent.cgColor) - context.fill(CGRect(x: x, y: centerY - CGFloat(logoSize) * 0.45, width: CGFloat(barW), height: CGFloat(logoSize) * 0.9)) + context.fill(CGRect(x: x, y: centerY - barHalfH, width: CGFloat(barW), height: CGFloat(logoSize) * 0.9)) x += CGFloat(barW + gap) if let logo, let cg = logo.cgImage { context.draw(cg, in: CGRect(x: x, y: centerY - CGFloat(logoSize) / 2, width: CGFloat(logoSize), height: CGFloat(logoSize))) x += CGFloat(logoSize + gap) } - (name as NSString).draw(at: CGPoint(x: x, y: centerY + labelFont.pointSize * 0.35 - labelFont.lineHeight), withAttributes: attrs) + let name = ellipsize(rawName, font: labelFont, maxWidth: min(rectRight - x, maxWidth)) + (name as NSString).draw(at: CGPoint(x: x, y: textY), withAttributes: attrs) } else { + // Specchio del lato casa: barra, logo, poi nome a sinistra del logo. var x = rectRight context.setFillColor(accent.cgColor) - context.fill(CGRect(x: x - CGFloat(barW), y: centerY - CGFloat(logoSize) * 0.45, width: CGFloat(barW), height: CGFloat(logoSize) * 0.9)) + context.fill(CGRect(x: x - CGFloat(barW), y: centerY - barHalfH, width: CGFloat(barW), height: CGFloat(logoSize) * 0.9)) x -= CGFloat(barW + gap) - let nameSize = (name as NSString).size(withAttributes: attrs) - (name as NSString).draw(at: CGPoint(x: x - nameSize.width, y: centerY + labelFont.pointSize * 0.35 - labelFont.lineHeight), withAttributes: attrs) - x -= CGFloat(gap) if let logo, let cg = logo.cgImage { x -= CGFloat(logoSize) context.draw(cg, in: CGRect(x: x, y: centerY - CGFloat(logoSize) / 2, width: CGFloat(logoSize), height: CGFloat(logoSize))) + x -= CGFloat(gap) } + let name = ellipsize(rawName, font: labelFont, maxWidth: min(x - rectLeft, maxWidth)) + let nameSize = (name as NSString).size(withAttributes: attrs) + (name as NSString).draw(at: CGPoint(x: x - nameSize.width, y: textY), withAttributes: attrs) } }