Skip to content

Commit

Permalink
Non-emoji icons (streamlit#8307)
Browse files Browse the repository at this point in the history
Add the ability to use nonemoji icons (from material outlined font) in alert elements, toast, and as a favicon.
  • Loading branch information
kajarenc authored Apr 26, 2024
1 parent c9de002 commit 7b9f046
Show file tree
Hide file tree
Showing 106 changed files with 546 additions and 51 deletions.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions e2e_playwright/lazy_loaded_modules.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
# Internal modules:
"streamlit.emojis",
"streamlit.external",
"streamlit.material_icon_names",
"streamlit.proto.openmetrics_data_model_pb2",
"streamlit.vendor.pympler",
# Requires `server.fileWatcherType` to be configured with `none` or `poll`:
Expand Down
2 changes: 1 addition & 1 deletion e2e_playwright/lazy_loaded_modules_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,6 @@
def test_lazy_loaded_modules_are_not_imported(app: Page):
"""Test that lazy loaded modules are not imported when the page is loaded."""
markdown_elements = app.get_by_test_id("stMarkdown")
expect(markdown_elements).to_have_count(18)
expect(markdown_elements).to_have_count(19)
for element in markdown_elements.all():
expect(element).to_have_text(re.compile(r".*not loaded.*"))
9 changes: 9 additions & 0 deletions e2e_playwright/st_alert.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,12 @@

st.error(text, icon="🚨")
st.success(text)

st.error("This is an error with non emoji icon", icon=":material/running_with_errors:")

st.warning("This is a warning with non emoji icon", icon=":material/warning:")
st.info("This is an info message with non emoji icon", icon=":material/info:")
st.success(
"This is a success message with non emoji icon",
icon=":material/celebration:",
)
7 changes: 6 additions & 1 deletion e2e_playwright/st_alert_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
def test_alerts_rendering(themed_app: Page, assert_snapshot: ImageCompareFunction):
"""Test that alerts render correctly using snapshot testing."""
alert_elements = themed_app.get_by_test_id("stAlert")
expect(alert_elements).to_have_count(16)
expect(alert_elements).to_have_count(20)

# The first 4 alerts are super basic, no need to screenshot test those
expect(alert_elements.nth(0)).to_have_text("This is an error")
Expand All @@ -43,3 +43,8 @@ def test_alerts_rendering(themed_app: Page, assert_snapshot: ImageCompareFunctio

assert_snapshot(alert_elements.nth(14), name="st_alert-error_long_code")
assert_snapshot(alert_elements.nth(15), name="st_alert-success_long_code")

assert_snapshot(alert_elements.nth(16), name="st_alert-error_non_emoji_icon")
assert_snapshot(alert_elements.nth(17), name="st_alert-warning_non_emoji_icon")
assert_snapshot(alert_elements.nth(18), name="st_alert-info_non_emoji_icon")
assert_snapshot(alert_elements.nth(19), name="st_alert-success_non_emoji_icon")
6 changes: 6 additions & 0 deletions e2e_playwright/st_chat_message.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,12 @@
"Another message with the same blue avatar."
)

with st.chat_message("user", avatar=":material/airline_seat_recline_extra:"):
st.write("Hello from USER, non-emoji icon.")

with st.chat_message("AI", avatar=":material/photo_album:"):
st.write("Hello from AI, non-emoji icon.")

query = "This is a hardcoded user message"
sources = "example sources"
llm_response = "some response"
Expand Down
4 changes: 2 additions & 2 deletions e2e_playwright/st_chat_message_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,12 @@ def test_renders_chat_messages_correctly_1(
"""Test if the chat messages render correctly"""
# Wait a bit more to allow all images to load:
chat_message_elements = themed_app.get_by_test_id("stChatMessage")
expect(chat_message_elements).to_have_count(12)
expect(chat_message_elements).to_have_count(14)

# rerun to populate session state chat message
rerun_app(themed_app)

expect(chat_message_elements).to_have_count(14)
expect(chat_message_elements).to_have_count(16)
for i, element in enumerate(chat_message_elements.all()):
element.scroll_into_view_if_needed()
expect(element).to_be_in_viewport()
Expand Down
2 changes: 2 additions & 0 deletions e2e_playwright/st_toast.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,5 @@
"Random toast message that is a really really really really really really really long message, going way past the 3 line limit",
icon="🦄",
)

st.toast("Your edited image was saved!", icon=":material/cabin:")
42 changes: 29 additions & 13 deletions e2e_playwright/st_toast_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,11 @@ def test_default_toast_rendering(
themed_app.wait_for_timeout(250)

toasts = themed_app.get_by_test_id("stToast")
expect(toasts).to_have_count(2)
toasts.nth(1).hover()
expect(toasts).to_have_count(3)
toasts.nth(2).hover()

expect(toasts.nth(1)).to_have_text("🐶This is a default toast messageClose")
assert_snapshot(toasts.nth(1), name="toast-default")
expect(toasts.nth(2)).to_contain_text("🐶This is a default toast message")
assert_snapshot(toasts.nth(2), name="toast-default")


def test_collapsed_toast_rendering(
Expand All @@ -42,13 +42,13 @@ def test_collapsed_toast_rendering(
themed_app.wait_for_timeout(250)

toasts = themed_app.get_by_test_id("stToast")
expect(toasts).to_have_count(2)
toasts.nth(0).hover()
expect(toasts).to_have_count(3)
toasts.nth(1).hover()

expect(toasts.nth(0)).to_have_text(
"🦄Random toast message that is a really really really really really really really long message, going way pastview moreClose"
expect(toasts.nth(1)).to_contain_text(
"🦄Random toast message that is a really really really really really really really long message, going wayview moreClose"
)
assert_snapshot(toasts.nth(0), name="toast-collapsed")
assert_snapshot(toasts.nth(1), name="toast-collapsed")


def test_expanded_toast_rendering(
Expand All @@ -60,14 +60,30 @@ def test_expanded_toast_rendering(
themed_app.wait_for_timeout(250)

toasts = themed_app.get_by_test_id("stToast")
expect(toasts).to_have_count(2)
toasts.nth(0).hover()
expect(toasts).to_have_count(3)
toasts.nth(1).hover()

expand = themed_app.get_by_text("view more")
expect(expand).to_have_count(1)
expand.click()

expect(toasts.nth(0)).to_have_text(
expect(toasts.nth(1)).to_contain_text(
"🦄Random toast message that is a really really really really really really really long message, going way past the 3 line limitview lessClose"
)
assert_snapshot(toasts.nth(0), name="toast-expanded")
assert_snapshot(toasts.nth(1), name="toast-expanded")


def test_toast_with_material_icon_rendering(
themed_app: Page, assert_snapshot: ImageCompareFunction
):
"""Test that toasts with material icons are correctly rendered."""
themed_app.keyboard.press("r")
wait_for_app_loaded(themed_app)
themed_app.wait_for_timeout(250)

toasts = themed_app.get_by_test_id("stToast")
expect(toasts).to_have_count(3)
toasts.nth(0).hover()

expect(toasts.nth(0)).to_contain_text("cabinYour edited image was saved!Close")
assert_snapshot(toasts.nth(0), name="toast-material-icon")
42 changes: 42 additions & 0 deletions frontend/app/src/assets/css/icon-fonts.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/**
* Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2024)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

@font-face {
font-family: "Material Symbols Outlined";
font-style: normal;
font-weight: 400;
font-display: block;
/* IMPORTANT: Always use a relative path! */
src: url("../fonts/MaterialSymbols/MaterialSymbols-Outlined.woff2")
format("woff2");
}

.material-symbols-outlined {
font-family: "Material Symbols Outlined";
font-weight: normal;
font-style: normal;
font-size: 24px;
line-height: 1;
letter-spacing: normal;
text-transform: none;
display: inline-block;
white-space: nowrap;
word-wrap: normal;
direction: ltr;
font-feature-settings: "liga";
-webkit-font-feature-settings: "liga";
-webkit-font-smoothing: antialiased;
}
1 change: 1 addition & 0 deletions frontend/app/src/assets/css/theme.scss
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@
@import "variables";
@import "fonts";
@import "reboot";
@import "icon-fonts";
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ describe("Alert element", () => {
expect(
screen.getByTestId("stNotificationContentError")
).toBeInTheDocument()
expect(screen.queryByTestId("stAlertEmojiIcon")).not.toBeInTheDocument()
expect(screen.queryByTestId("stAlertDynamicIcon")).not.toBeInTheDocument()
expect(screen.getByText("#what in the world?")).toBeInTheDocument()
})

Expand All @@ -60,7 +60,7 @@ describe("Alert element", () => {
expect(
screen.getByTestId("stNotificationContentWarning")
).toBeInTheDocument()
expect(screen.queryByTestId("stAlertEmojiIcon")).not.toBeInTheDocument()
expect(screen.queryByTestId("stAlertDynamicIcon")).not.toBeInTheDocument()
expect(screen.getByText("test")).toBeInTheDocument()
})

Expand All @@ -74,7 +74,7 @@ describe("Alert element", () => {
expect(
screen.getByTestId("stNotificationContentSuccess")
).toBeInTheDocument()
expect(screen.queryByTestId("stAlertEmojiIcon")).not.toBeInTheDocument()
expect(screen.queryByTestId("stAlertDynamicIcon")).not.toBeInTheDocument()
expect(
screen.getByText("But our princess was in another castle!")
).toBeInTheDocument()
Expand All @@ -88,7 +88,7 @@ describe("Alert element", () => {
render(<AlertElement {...props} />)
expect(screen.getByTestId("stAlert")).toBeInTheDocument()
expect(screen.getByTestId("stNotificationContentInfo")).toBeInTheDocument()
expect(screen.queryByTestId("stAlertEmojiIcon")).not.toBeInTheDocument()
expect(screen.queryByTestId("stAlertDynamicIcon")).not.toBeInTheDocument()
expect(screen.getByText("It's dangerous to go alone.")).toBeInTheDocument()
})

Expand All @@ -101,7 +101,7 @@ describe("Alert element", () => {
render(<AlertElement {...props} />)
expect(screen.getByTestId("stAlert")).toBeInTheDocument()
expect(screen.getByTestId("stNotificationContentInfo")).toBeInTheDocument()
expect(screen.getByTestId("stAlertEmojiIcon")).toHaveTextContent("👉🏻")
expect(screen.getByTestId("stAlertDynamicIcon")).toHaveTextContent("👉🏻")
expect(screen.getByText("It's dangerous to go alone.")).toBeInTheDocument()
})
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import React, { ReactElement } from "react"

import { Alert as AlertProto } from "@streamlit/lib/src/proto"
import StreamlitMarkdown from "@streamlit/lib/src/components/shared/StreamlitMarkdown"
import { EmojiIcon } from "@streamlit/lib/src/components/shared/Icon"
import { DynamicIcon } from "@streamlit/lib/src/components/shared/Icon"
import AlertContainer, {
Kind,
} from "@streamlit/lib/src/components/shared/AlertContainer"
Expand Down Expand Up @@ -65,10 +65,13 @@ export default function AlertElement({
<AlertContainer width={width} kind={kind}>
<StyledAlertContent>
{icon && (
<EmojiIcon testid="stAlertEmojiIcon" size="lg">
{icon}
</EmojiIcon>
<DynamicIcon
iconValue={icon}
size="lg"
testid="stAlertDynamicIcon"
/>
)}

<StreamlitMarkdown
source={body}
allowHTML={false}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@
*/

import styled from "@emotion/styled"
import { StyledEmojiIcon } from "@streamlit/lib/src/components/shared/Icon/styled-components"
import {
StyledIcon,
StyledEmojiIcon,
} from "@streamlit/lib/src/components/shared/Icon/styled-components"
import { StyledMaterialIcon } from "@streamlit/lib/src/components/shared/Icon/Material/styled-components"

export const StyledAlertContent = styled.div(({ theme }) => ({
display: "flex",
Expand All @@ -27,6 +31,16 @@ export const StyledAlertContent = styled.div(({ theme }) => ({
top: "2px",
},

[StyledIcon as any]: {
position: "relative",
top: "2px",
},

[StyledMaterialIcon as any]: {
position: "relative",
top: "2px",
},

".stCodeBlock code": {
paddingRight: "1rem",
},
Expand Down
12 changes: 11 additions & 1 deletion frontend/lib/src/components/elements/ChatMessage/ChatMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { useTheme } from "@emotion/react"
import { Face, SmartToy } from "@emotion-icons/material-outlined"

import { Block as BlockProto } from "@streamlit/lib/src/proto"
import Icon from "@streamlit/lib/src/components/shared/Icon"
import Icon, { DynamicIcon } from "@streamlit/lib/src/components/shared/Icon"
import { EmotionTheme } from "@streamlit/lib/src/theme"
import { StreamlitEndpoints } from "@streamlit/lib/src/StreamlitEndpoints"

Expand Down Expand Up @@ -72,6 +72,16 @@ function ChatMessageAvatar(props: ChatMessageAvatarProps): ReactElement {
<Icon content={SmartToy} size="lg" />
</StyledAvatarIcon>
)
} else if (avatar.startsWith(":material")) {
return (
<StyledAvatarBackground data-testid="chatAvatarIcon-custom">
<DynamicIcon
size="lg"
iconValue={avatar}
color={theme.colors.bodyText}
/>
</StyledAvatarBackground>
)
}
}
}
Expand Down
20 changes: 20 additions & 0 deletions frontend/lib/src/components/elements/Favicon/Favicon.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,15 @@ const SATELLITE_TWEMOJI_URL =
const CRESCENT_MOON_TWEMOJI_URL =
"https://cdn.jsdelivr.net/gh/twitter/[email protected]/assets/72x72/1f319.png"

const FLAG_MATERIAL_ICON_URL =
"https://fonts.gstatic.com/s/i/short-term/release/materialsymbolsoutlined/flag/default/24px.svg"

const SMART_DISPLAY_MATERIAL_ICON_URL =
"https://fonts.gstatic.com/s/i/short-term/release/materialsymbolsoutlined/smart_display/default/24px.svg"

const ACCESSIBILITY_NEW_MATERIAL_ICON_URL =
"https://fonts.gstatic.com/s/i/short-term/release/materialsymbolsoutlined/accessibility_new/default/24px.svg"

test("is set up with the default favicon", () => {
expect(getFaviconHref()).toBe("http://localhost/default.png")
})
Expand All @@ -59,6 +68,17 @@ describe("Favicon element", () => {
expect(getFaviconHref()).toBe(SATELLITE_TWEMOJI_URL)
})

it("handles material icon correctly", () => {
handleFavicon(":material/flag:", jest.fn(), endpoints)
expect(getFaviconHref()).toBe(FLAG_MATERIAL_ICON_URL)

handleFavicon(":material/smart_display:", jest.fn(), endpoints)
expect(getFaviconHref()).toBe(SMART_DISPLAY_MATERIAL_ICON_URL)

handleFavicon(":material/accessibility_new:", jest.fn(), endpoints)
expect(getFaviconHref()).toBe(ACCESSIBILITY_NEW_MATERIAL_ICON_URL)
})

it("handles emoji shortcodes containing a dash correctly", () => {
handleFavicon(":crescent-moon:", jest.fn(), endpoints)
expect(getFaviconHref()).toBe(CRESCENT_MOON_TWEMOJI_URL)
Expand Down
16 changes: 15 additions & 1 deletion frontend/lib/src/components/elements/Favicon/Favicon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,18 @@ import { grabTheRightIcon } from "@streamlit/lib/src/vendor/twemoji"
import { IGuestToHostMessage } from "@streamlit/lib/src/hostComm/types"
import { StreamlitEndpoints } from "@streamlit/lib/src/StreamlitEndpoints"

function iconToUrl(icon: string): string {
const iconRegexp = /^:(.+)\/(.+):$/
const matchResult = icon.match(iconRegexp)
if (matchResult === null) {
// If the icon is invalid, return just an empty string
return ""
}

const iconUrl = `https://fonts.gstatic.com/s/i/short-term/release/materialsymbolsoutlined/${matchResult[2]}/default/24px.svg`
return iconUrl
}

/**
* Set the provided url/emoji as the page favicon.
*
Expand All @@ -34,12 +46,14 @@ export function handleFavicon(
const emoji = extractEmoji(favicon)
let imageUrl

if (emoji) {
if (emoji && !favicon.startsWith(":material")) {
// Find the corresponding Twitter emoji on the CDN.
const codepoint = grabTheRightIcon(emoji)
const emojiUrl = `https://cdn.jsdelivr.net/gh/twitter/[email protected]/assets/72x72/${codepoint}.png`

imageUrl = emojiUrl
} else if (favicon.startsWith(":material")) {
imageUrl = iconToUrl(favicon)
} else {
imageUrl = endpoints.buildMediaURL(favicon)
}
Expand Down
Loading

0 comments on commit 7b9f046

Please sign in to comment.