UX: Lots of wizard canvas rendering improvements

* Make sure bold body font loads before rendering otherwise
  the 1/20 post count on the timeline will use wrong font
* Fix pill button rendering on homepage preview, now all
  the widths resize based on the font size/width and all
  buttons depend on the X position of the button beforehand
* Fix the table header labels overlapping by giving them more
  room based on col position
* Moving hardcoded label text into I18n
This commit is contained in:
Martin Brennan 2024-12-06 16:58:54 +10:00
parent 9c6e5440d8
commit 7c92402e15
No known key found for this signature in database
GPG Key ID: BD981EFEEC8F5675
9 changed files with 135 additions and 71 deletions

View File

@ -83,7 +83,7 @@ export default class HomepagePreview extends PreviewBaseComponent {
]
);
ctx.font = `Bold ${bodyFontSize * 1.3}em '${font}'`;
ctx.font = `700 ${bodyFontSize * 1.3}em '${font}'`;
ctx.fillStyle = colors.primary;
ctx.textAlign = "center";
ctx.fillText(category.name, boxStartX + boxWidth / 2, boxStartY + 25);
@ -166,7 +166,7 @@ export default class HomepagePreview extends PreviewBaseComponent {
// Categories
this.categories().forEach((category, idx) => {
const textPos = y + categoryHeight * 0.35;
ctx.font = `Bold ${bodyFontSize * 1.1}em '${font}'`;
ctx.font = `700 ${bodyFontSize * 1.1}em '${font}'`;
ctx.fillStyle = colors.primary;
ctx.fillText(category.name, cols[0], textPos);
@ -262,7 +262,7 @@ export default class HomepagePreview extends PreviewBaseComponent {
// Categories
this.categories().forEach((category, idx) => {
const textPos = y + categoryHeight * 0.35;
ctx.font = `Bold ${bodyFontSize * 1.1}em '${font}'`;
ctx.font = `700 ${bodyFontSize * 1.1}em '${font}'`;
ctx.fillStyle = colors.primary;
ctx.fillText(category.name, cols[0], textPos);
@ -310,7 +310,7 @@ export default class HomepagePreview extends PreviewBaseComponent {
);
ctx.fillText(title, cols[3], textPos);
ctx.font = `Bold ${bodyFontSize}em '${font}'`;
ctx.font = `700 ${bodyFontSize}em '${font}'`;
ctx.fillText(Math.floor(Math.random() * 90) + 10, cols[4], textPos);
ctx.font = `${bodyFontSize}em '${font}'`;
ctx.fillText(`1h`, cols[4], textPos + topicHeight * 0.4);
@ -318,7 +318,7 @@ export default class HomepagePreview extends PreviewBaseComponent {
ctx.beginPath();
ctx.fillStyle = category.color;
const badgeSize = topicHeight * 0.1;
ctx.font = `Bold ${bodyFontSize * 0.5}em '${font}'`;
ctx.font = `700 ${bodyFontSize * 0.5}em '${font}'`;
ctx.rect(
cols[3] + margin * 0.25,
y + topicHeight * 0.65,
@ -383,17 +383,33 @@ export default class HomepagePreview extends PreviewBaseComponent {
ctx.stroke();
};
const cols = [0.02, 0.66, 0.8, 0.87, 0.93].map((c) => c * width);
const cols = [0.02, 0.66, 0.75, 0.83, 0.9].map((c) => c * width);
// Headings
const headingY = height * 0.33;
ctx.fillStyle = textColor;
ctx.font = `${bodyFontSize * 0.9}em '${font}'`;
ctx.fillText("Topic", cols[0], headingY);
ctx.fillText("Replies", cols[2], headingY);
ctx.fillText("Views", cols[3], headingY);
ctx.fillText("Activity", cols[4], headingY);
ctx.fillText(
i18n("wizard.homepage_preview.table_headers.topic"),
cols[0],
headingY
);
ctx.fillText(
i18n("wizard.homepage_preview.table_headers.replies"),
cols[2],
headingY
);
ctx.fillText(
i18n("wizard.homepage_preview.table_headers.views"),
cols[3],
headingY
);
ctx.fillText(
i18n("wizard.homepage_preview.table_headers.activity"),
cols[4],
headingY
);
// Topics
let y = headingY + rowHeight / 2.6;
@ -412,7 +428,7 @@ export default class HomepagePreview extends PreviewBaseComponent {
ctx.beginPath();
ctx.fillStyle = category.color;
const badgeSize = rowHeight * 0.15;
ctx.font = `Bold ${bodyFontSize * 0.75}em '${font}'`;
ctx.font = `700 ${bodyFontSize * 0.75}em '${font}'`;
ctx.rect(cols[0] + 4, y + rowHeight * 0.6, badgeSize, badgeSize);
ctx.fill();

View File

@ -116,7 +116,7 @@ export default class PreviewBase extends Component {
});
});
Promise.all(
return Promise.all(
fontFaces.map((fontFace) =>
fontFace.load().then((loadedFont) => {
document.fonts.add(loadedFont);
@ -135,7 +135,7 @@ export default class PreviewBase extends Component {
this.loadingFontVariants = false;
});
} else if (this.loadedFonts.has(font.id)) {
this.triggerRepaint();
return Promise.resolve(this.triggerRepaint());
}
}
@ -162,8 +162,14 @@ export default class PreviewBase extends Component {
reload() {
Promise.all([this.loadFonts(), this.loadImages()]).then(() => {
this.loaded = true;
this.triggerRepaint();
// NOTE: This must be done otherwise the "bold" variant of the body font
// will not be loaded for some reason before rendering the canvas.
//
// The header font does not suffer from this issue.
this.loadFontVariants(this.wizard.font).then(() => {
this.loaded = true;
this.triggerRepaint();
});
});
}
@ -283,17 +289,16 @@ export default class PreviewBase extends Component {
avatarSize,
avatarSize
);
// accounts for hard-set color variables in solarized themes
ctx.fillStyle =
colors.primary_low_mid ||
darkLightDiff(colors.primary, colors.secondary, 45, 55);
const pathScale = this.headerHeight / 1200;
// search icon SVG path
const searchIcon = new Path2D(
"M505 442.7L405.3 343c-4.5-4.5-10.6-7-17-7H372c27.6-35.3 44-79.7 44-128C416 93.1 322.9 0 208 0S0 93.1 0 208s93.1 208 208 208c48.3 0 92.7-16.4 128-44v16.3c0 6.4 2.5 12.5 7 17l99.7 99.7c9.4 9.4 24.6 9.4 33.9 0l28.3-28.3c9.4-9.4 9.4-24.6.1-34zM208 336c-70.7 0-128-57.2-128-128 0-70.7 57.2-128 128-128 70.7 0 128 57.2 128 128 0 70.7-57.2 128-128 128z"
);
// hamburger icon
const hamburgerIcon = new Path2D(
"M16 132h416c8.837 0 16-7.163 16-16V76c0-8.837-7.163-16-16-16H16C7.163 60 0 67.163 0 76v40c0 8.837 7.163 16 16 16zm0 160h416c8.837 0 16-7.163 16-16v-40c0-8.837-7.163-16-16-16H16c-8.837 0-16 7.163-16 16v40c0 8.837 7.163 16 16 16zm0 160h416c8.837 0 16-7.163 16-16v-40c0-8.837-7.163-16-16-16H16c-8.837 0-16 7.163-16 16v40c0 8.837 7.163 16 16 16z"
);
@ -321,100 +326,100 @@ export default class PreviewBase extends Component {
const { ctx } = this;
const categoriesSize = headerHeight * 2;
const badgeHeight = categoriesSize * 0.25;
const badgeHeight = headerHeight * 2 * 0.25;
const headerMargin = headerHeight * 0.2;
const fontSize = Math.round(badgeHeight * 0.5);
ctx.font = `${fontSize}px '${font}'`;
const allCategoriesText = i18n(
"wizard.homepage_preview.nav_buttons.all_categories"
);
const categoriesWidth = ctx.measureText(allCategoriesText).width;
const categoriesBoxWidth = categoriesWidth + headerMargin * 2;
// Box around "all categories >"
ctx.beginPath();
ctx.strokeStyle = colors.primary;
ctx.lineWidth = 0.5;
ctx.rect(
headerMargin,
headerHeight + headerMargin,
categoriesSize,
categoriesBoxWidth,
badgeHeight
);
ctx.stroke();
const fontSize = Math.round(badgeHeight * 0.5);
ctx.font = `${fontSize}px '${font}'`;
ctx.fillStyle = colors.primary;
ctx.fillText(
"all categories",
allCategoriesText,
headerMargin * 1.5,
headerHeight + headerMargin * 1.4 + fontSize
);
// Caret (>) at the end of "all categories" box
const pathScale = badgeHeight / 1000;
// caret icon
const caretIcon = new Path2D(
"M0 384.662V127.338c0-17.818 21.543-26.741 34.142-14.142l128.662 128.662c7.81 7.81 7.81 20.474 0 28.284L34.142 398.804C21.543 411.404 0 402.48 0 384.662z"
);
ctx.save();
ctx.translate(
categoriesSize - headerMargin / 4,
categoriesBoxWidth,
headerHeight + headerMargin + badgeHeight / 4
);
ctx.scale(pathScale, pathScale);
ctx.fill(caretIcon);
ctx.restore();
const text = opts.categories ? "Categories" : "Latest";
// First top menu item
const firstTopMenuItemText = opts.categories
? i18n("wizard.homepage_preview.nav_buttons.categories")
: i18n("wizard.homepage_preview.nav_buttons.latest");
const newText = i18n("wizard.homepage_preview.nav_buttons.new");
const unreadText = i18n("wizard.homepage_preview.nav_buttons.unread");
const topText = i18n("wizard.homepage_preview.nav_buttons.top");
const hotText = i18n("wizard.homepage_preview.nav_buttons.hot");
const activeWidth = categoriesSize * (opts.categories ? 0.8 : 0.55);
ctx.beginPath();
ctx.fillStyle = colors.tertiary;
ctx.rect(
headerMargin * 2 + categoriesSize,
categoriesBoxWidth + headerMargin * 2,
headerHeight + headerMargin,
activeWidth,
ctx.measureText(firstTopMenuItemText).width + headerMargin * 2,
badgeHeight
);
ctx.fill();
ctx.font = `${fontSize}px '${font}'`;
ctx.fillStyle = colors.secondary;
let x = headerMargin * 3.0 + categoriesSize;
const pillButtonTextY = headerHeight + headerMargin * 1.4 + fontSize;
const firstTopMenuItemX = headerMargin * 3.0 + categoriesBoxWidth;
ctx.fillText(
text,
x - headerMargin * 0.1,
headerHeight + headerMargin * 1.5 + fontSize
firstTopMenuItemText,
firstTopMenuItemX,
pillButtonTextY,
ctx.measureText(firstTopMenuItemText).width
);
ctx.fillStyle = colors.primary;
x += categoriesSize * (opts.categories ? 0.8 : 0.6);
ctx.fillText("New", x, headerHeight + headerMargin * 1.5 + fontSize);
x += categoriesSize * 0.4;
ctx.fillText("Unread", x, headerHeight + headerMargin * 1.5 + fontSize);
const newTextX =
firstTopMenuItemX +
ctx.measureText(firstTopMenuItemText).width +
headerMargin * 2.0;
ctx.fillText(newText, newTextX, pillButtonTextY);
x += categoriesSize * 0.6;
ctx.fillText("Top", x, headerHeight + headerMargin * 1.5 + fontSize);
}
const unreadTextX =
newTextX + ctx.measureText(newText).width + headerMargin * 2.0;
ctx.fillText(unreadText, unreadTextX, pillButtonTextY);
resizeTextLinesToFitRect(
textLines,
rectWidth,
ctx,
fontSize,
font,
renderCallback
) {
const maxLengthLine = textLines.reduce((a, b) =>
a.length > b.length ? a : b
);
const topTextX =
unreadTextX + ctx.measureText(unreadText).width + headerMargin * 2.0;
ctx.fillText(topText, topTextX, pillButtonTextY);
let fontSizeDecreaseMultiplier = 1;
while (ctx.measureText(maxLengthLine).width > rectWidth) {
fontSizeDecreaseMultiplier -= 0.1;
ctx.font = `${fontSize * fontSizeDecreaseMultiplier}em '${font}'`;
}
for (let i = 0; i < textLines.length; i++) {
renderCallback(textLines[i], i);
}
const hotTextX =
topTextX + ctx.measureText(topText).width + headerMargin * 2.0;
ctx.fillText(hotText, hotTextX, pillButtonTextY);
}
}

View File

@ -2,7 +2,11 @@ import { action } from "@ember/object";
import { observes } from "@ember-decorators/object";
import { bind } from "discourse-common/utils/decorators";
import { i18n } from "discourse-i18n";
import { chooseDarker, darkLightDiff } from "../../../lib/preview";
import {
chooseDarker,
darkLightDiff,
resizeTextLinesToFitRect,
} from "../../../lib/preview";
import HomepagePreview from "./-homepage-preview";
import PreviewBaseComponent from "./-preview-base";
@ -133,7 +137,7 @@ export default class Index extends PreviewBaseComponent {
// Topic title
ctx.beginPath();
ctx.fillStyle = colors.primary;
ctx.font = `bold ${titleFontSize}em '${headingFont}'`;
ctx.font = `700 ${titleFontSize}em '${headingFont}'`;
ctx.fillText(i18n("wizard.previews.topic_title"), margin, height * 0.3);
// Topic OP text
@ -144,7 +148,7 @@ export default class Index extends PreviewBaseComponent {
const topicOp = i18n("wizard.homepage_preview.topic_ops.what_books");
const topicOpLines = topicOp.split("\n");
this.resizeTextLinesToFitRect(
resizeTextLinesToFitRect(
topicOpLines,
timelineX - leftHandTextGutter,
ctx,
@ -216,13 +220,14 @@ export default class Index extends PreviewBaseComponent {
// Timeline post count
const postCountY = height * 0.3 + margin + 10;
ctx.font = `Bold ${bodyFontSize}em ${font}`;
ctx.beginPath();
ctx.font = `700 ${bodyFontSize}em '${font}'`;
ctx.fillStyle = colors.primary;
ctx.fillText("1 / 20", timelineX + margin / 2, postCountY);
// Timeline post date
ctx.beginPath();
ctx.font = `${bodyFontSize * 0.9}em ${font}`;
ctx.font = `${bodyFontSize * 0.9}em '${font}'`;
ctx.fillStyle = darkLightDiff(colors.primary, colors.secondary, 70, 65);
ctx.fillText(
"Nov 22",

View File

@ -138,3 +138,26 @@ export function drawHeader(ctx, colors, width, headerHeight) {
ctx.fill();
ctx.restore();
}
export function resizeTextLinesToFitRect(
textLines,
rectWidth,
ctx,
fontSize,
font,
renderCallback
) {
const maxLengthLine = textLines.reduce((a, b) =>
a.length > b.length ? a : b
);
let fontSizeDecreaseMultiplier = 1;
while (ctx.measureText(maxLengthLine).width > rectWidth) {
fontSizeDecreaseMultiplier -= 0.1;
ctx.font = `${fontSize * fontSizeDecreaseMultiplier}em '${font}'`;
}
for (let i = 0; i < textLines.length; i++) {
renderCallback(textLines[i], i);
}
}

View File

@ -72,7 +72,7 @@ class SiteSetting < ActiveRecord::Base
end
def self.top_menu_items
top_menu.split("|").map { |menu_item| TopMenuItem.new(menu_item) }
top_menu_map.map { |menu_item| TopMenuItem.new(menu_item) }
end
def self.homepage

View File

@ -7390,6 +7390,14 @@ en:
homepage_preview: "Homepage preview"
homepage_preview:
nav_buttons:
all_categories: "all categories"
new: "New"
unread: "Unread"
top: "Top"
latest: "Latest"
hot: "Hot"
categories: "Categories"
topic_titles:
what_books: "What books are you reading?"
what_movies: "What movies have you seen recently?"
@ -7409,3 +7417,8 @@ en:
icebreakers: "Icebreakers"
news: "News"
site_feedback: "Site Feedback"
table_headers:
topic: "Topic"
replies: "Replies"
views: "Views"
activity: "Activity"

View File

@ -5423,6 +5423,8 @@ en:
choices:
latest:
label: "Latest Topics"
hot:
label: "Hot Topics"
categories_only:
label: "Categories Only"
categories_with_featured_topics:

View File

@ -222,7 +222,7 @@ module Stylesheet
)
contents << <<~CSS
@font-face {
font-family: #{font[:name]};
font-family: '#{font[:name]}';
src: #{src};
font-weight: #{variant[:weight]};
}

View File

@ -191,7 +191,7 @@ class Wizard
current =
(
if SiteSetting.top_menu.starts_with?("categories")
if SiteSetting.top_menu_map.first == "categories"
SiteSetting.desktop_category_page_style
else
"latest"
@ -214,8 +214,8 @@ class Wizard
updater.update_setting(:base_font, updater.fields[:body_font])
updater.update_setting(:heading_font, updater.fields[:heading_font])
top_menu = SiteSetting.top_menu.split("|")
if updater.fields[:homepage_style] == "latest" && top_menu[0] != "latest"
top_menu = SiteSetting.top_menu_map
if updater.fields[:homepage_style] == "latest" && top_menu.first != "latest"
top_menu.delete("latest")
top_menu.insert(0, "latest")
elsif updater.fields[:homepage_style] != "latest"