diff --git a/benches/packet.rs b/benches/packet.rs index 690d76d75..8de29f113 100644 --- a/benches/packet.rs +++ b/benches/packet.rs @@ -9,7 +9,7 @@ use valence::protocol::byte_angle::ByteAngle; use valence::protocol::decode::PacketDecoder; use valence::protocol::encode::PacketEncoder; use valence::protocol::var_int::VarInt; -use valence::text::TextFormat; +use valence::text::IntoText; use valence_core::protocol::encode::{PacketWriter, WritePacket}; use valence_entity::packet::EntitySpawnS2c; use valence_instance::packet::ChunkDataS2c; diff --git a/crates/valence_client/src/lib.rs b/crates/valence_client/src/lib.rs index 72197e73a..a6b6fc678 100644 --- a/crates/valence_client/src/lib.rs +++ b/crates/valence_client/src/lib.rs @@ -50,7 +50,7 @@ use valence_core::protocol::global_pos::GlobalPos; use valence_core::protocol::packet::sound::{PlaySoundS2c, Sound, SoundCategory}; use valence_core::protocol::var_int::VarInt; use valence_core::protocol::{Encode, Packet}; -use valence_core::text::Text; +use valence_core::text::{IntoText, Text}; use valence_core::uuid::UniqueId; use valence_entity::packet::{ EntitiesDestroyS2c, EntitySetHeadYawS2c, EntitySpawnS2c, EntityStatusS2c, @@ -331,10 +331,10 @@ impl Client { /// Kills the client and shows `message` on the death screen. If an entity /// killed the player, you should supply it as `killer`. - pub fn kill(&mut self, message: impl Into) { + pub fn kill<'a>(&mut self, message: impl IntoText<'a>) { self.write_packet(&DeathMessageS2c { player_id: VarInt(0), - message: message.into().into(), + message: message.into_cow_text(), }); } diff --git a/crates/valence_client/src/message.rs b/crates/valence_client/src/message.rs index f2bff1c49..c7b91ff69 100644 --- a/crates/valence_client/src/message.rs +++ b/crates/valence_client/src/message.rs @@ -4,7 +4,7 @@ use bevy_app::prelude::*; use bevy_ecs::prelude::*; use valence_core::protocol::encode::WritePacket; use valence_core::protocol::packet::chat::{ChatMessageC2s, GameMessageS2c}; -use valence_core::text::Text; +use valence_core::text::IntoText; use crate::event_loop::{EventLoopPreUpdate, PacketEvent}; @@ -15,22 +15,22 @@ pub(super) fn build(app: &mut App) { pub trait SendMessage { /// Sends a system message visible in the chat. - fn send_chat_message(&mut self, msg: impl Into); + fn send_chat_message<'a>(&mut self, msg: impl IntoText<'a>); /// Displays a message in the player's action bar (text above the hotbar). - fn send_action_bar_message(&mut self, msg: impl Into); + fn send_action_bar_message<'a>(&mut self, msg: impl IntoText<'a>); } impl SendMessage for T { - fn send_chat_message(&mut self, msg: impl Into) { + fn send_chat_message<'a>(&mut self, msg: impl IntoText<'a>) { self.write_packet(&GameMessageS2c { - chat: msg.into().into(), + chat: msg.into_cow_text(), overlay: false, }); } - fn send_action_bar_message(&mut self, msg: impl Into) { + fn send_action_bar_message<'a>(&mut self, msg: impl IntoText<'a>) { self.write_packet(&GameMessageS2c { - chat: msg.into().into(), + chat: msg.into_cow_text(), overlay: true, }); } diff --git a/crates/valence_client/src/title.rs b/crates/valence_client/src/title.rs index 1510efacc..c1767dbe9 100644 --- a/crates/valence_client/src/title.rs +++ b/crates/valence_client/src/title.rs @@ -1,4 +1,5 @@ use valence_core::protocol::{packet_id, Decode, Encode}; +use valence_core::text::IntoText; use super::*; @@ -9,11 +10,11 @@ pub trait SetTitle { /// which may also include a subtitle underneath it. The title can be /// configured to fade in and out using /// [`set_title_times`](Self::set_title_times). - fn set_title(&mut self, text: impl Into); + fn set_title<'a>(&mut self, text: impl IntoText<'a>); - fn set_subtitle(&mut self, text: impl Into); + fn set_subtitle<'a>(&mut self, text: impl IntoText<'a>); - fn set_action_bar(&mut self, text: impl Into); + fn set_action_bar<'a>(&mut self, text: impl IntoText<'a>); /// - `fade_in`: Ticks to spend fading in. /// - `stay`: Ticks to keep the title displayed. @@ -26,21 +27,21 @@ pub trait SetTitle { } impl SetTitle for T { - fn set_title(&mut self, text: impl Into) { + fn set_title<'a>(&mut self, text: impl IntoText<'a>) { self.write_packet(&TitleS2c { - title_text: text.into().into(), + title_text: text.into_cow_text(), }); } - fn set_subtitle(&mut self, text: impl Into) { + fn set_subtitle<'a>(&mut self, text: impl IntoText<'a>) { self.write_packet(&SubtitleS2c { - subtitle_text: text.into().into(), + subtitle_text: text.into_cow_text(), }); } - fn set_action_bar(&mut self, text: impl Into) { + fn set_action_bar<'a>(&mut self, text: impl IntoText<'a>) { self.write_packet(&OverlayMessageS2c { - action_bar_text: text.into().into(), + action_bar_text: text.into_cow_text(), }); } diff --git a/crates/valence_core/src/protocol.rs b/crates/valence_core/src/protocol.rs index 4b986b128..a1e27f3f2 100644 --- a/crates/valence_core/src/protocol.rs +++ b/crates/valence_core/src/protocol.rs @@ -280,7 +280,7 @@ mod tests { use crate::item::{ItemKind, ItemStack}; use crate::protocol::var_int::VarInt; use crate::protocol::var_long::VarLong; - use crate::text::{Text, TextFormat}; + use crate::text::{IntoText, Text}; #[cfg(feature = "encryption")] const CRYPT_KEY: [u8; 16] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; diff --git a/crates/valence_core/src/text.rs b/crates/valence_core/src/text.rs index 8b26a9df5..394264974 100644 --- a/crates/valence_core/src/text.rs +++ b/crates/valence_core/src/text.rs @@ -2,17 +2,24 @@ use std::borrow::Cow; use std::io::Write; +use std::ops::{Deref, DerefMut}; use std::{fmt, ops}; use anyhow::Context; use serde::de::Visitor; -use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; +use serde::{de, Deserialize, Deserializer, Serialize}; use uuid::Uuid; use valence_nbt::Value; use crate::ident::Ident; use crate::protocol::{Decode, Encode}; +pub mod color; +mod into_text; + +pub use color::Color; +pub use into_text::IntoText; + /// Represents formatted text in Minecraft's JSON text format. /// /// Text is used in various places such as chat, window titles, @@ -24,9 +31,9 @@ use crate::protocol::{Decode, Encode}; /// /// # Examples /// -/// With [`TextFormat`] in scope, you can write the following: +/// With [`IntoText`] in scope, you can write the following: /// ``` -/// use valence_core::text::{Color, Text, TextFormat}; +/// use valence_core::text::{Color, IntoText, Text}; /// /// let txt = "The text is ".into_text() /// + "Red".color(Color::RED) @@ -47,112 +54,53 @@ use crate::protocol::{Decode, Encode}; #[serde(transparent)] pub struct Text(Box); -impl<'de> Deserialize<'de> for Text { - fn deserialize>(deserializer: D) -> Result { - struct TextVisitor; - - impl<'de> Visitor<'de> for TextVisitor { - type Value = Text; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - write!(formatter, "a text component data type") - } - - fn visit_bool(self, v: bool) -> Result { - Ok(Text::text(v.to_string())) - } - - fn visit_i64(self, v: i64) -> Result { - Ok(Text::text(v.to_string())) - } - - fn visit_u64(self, v: u64) -> Result { - Ok(Text::text(v.to_string())) - } - - fn visit_f64(self, v: f64) -> Result { - Ok(Text::text(v.to_string())) - } - - fn visit_str(self, v: &str) -> Result { - Ok(Text::text(v.to_string())) - } - - fn visit_string(self, v: String) -> Result { - Ok(Text::text(v)) - } - - fn visit_seq>(self, mut seq: A) -> Result { - let Some(mut res) = seq.next_element()? else { - return Ok(Text::default()) - }; - - while let Some(child) = seq.next_element::()? { - res += child; - } - - Ok(res) - } - - fn visit_map>(self, map: A) -> Result { - use de::value::MapAccessDeserializer; - - Ok(Text(Box::new(TextInner::deserialize( - MapAccessDeserializer::new(map), - )?))) - } - } - - deserializer.deserialize_any(TextVisitor) - } -} - +/// Text data and formatting. #[derive(Clone, PartialEq, Default, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -struct TextInner { +pub struct TextInner { #[serde(flatten)] - content: TextContent, + pub content: TextContent, #[serde(default, skip_serializing_if = "Option::is_none")] - color: Option, + pub color: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - font: Option>, + pub font: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - bold: Option, + pub bold: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - italic: Option, + pub italic: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - underlined: Option, + pub underlined: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - strikethrough: Option, + pub strikethrough: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - obfuscated: Option, + pub obfuscated: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - insertion: Option>, + pub insertion: Option>, #[serde(default, skip_serializing_if = "Option::is_none")] - click_event: Option, + pub click_event: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - hover_event: Option, + pub hover_event: Option, #[serde(default, skip_serializing_if = "Vec::is_empty")] - extra: Vec, + pub extra: Vec, } +/// The text content of a Text object. #[derive(Clone, PartialEq, Debug, Serialize, Deserialize)] #[serde(untagged)] -enum TextContent { - Text { - text: Cow<'static, str>, - }, +pub enum TextContent { + /// Normal text + Text { text: Cow<'static, str> }, /// A piece of text that will be translated on the client based on the /// client language. If no corresponding translation can be found, the /// identifier itself is used as the translated text. @@ -166,9 +114,7 @@ enum TextContent { with: Vec, }, /// Displays a score holder's current score in an objective. - ScoreboardValue { - score: ScoreboardValueContent, - }, + ScoreboardValue { score: ScoreboardValueContent }, /// Displays the name of one or more entities found by a [`selector`]. /// /// [`selector`]: https://minecraft.fandom.com/wiki/Target_selectors @@ -211,7 +157,7 @@ enum TextContent { }, /// Displays NBT values from command storage. StorageNbt { - storage: Ident, + storage: Ident>, nbt: Cow<'static, str>, #[serde(default, skip_serializing_if = "Option::is_none")] interpret: Option, @@ -220,62 +166,87 @@ enum TextContent { }, } +/// Scoreboard value. #[derive(Clone, PartialEq, Debug, Serialize, Deserialize)] -struct ScoreboardValueContent { +pub struct ScoreboardValueContent { /// The name of the score holder whose score should be displayed. This /// can be a [`selector`] or an explicit name. /// /// [`selector`]: https://minecraft.fandom.com/wiki/Target_selectors - name: Cow<'static, str>, + pub name: Cow<'static, str>, /// The internal name of the objective to display the player's score in. - objective: Cow<'static, str>, + pub objective: Cow<'static, str>, /// If present, this value is displayed regardless of what the score /// would have been. #[serde(default, skip_serializing_if = "Option::is_none")] - value: Option>, -} - -/// Text color -#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] -pub struct Color { - /// Red channel - pub r: u8, - /// Green channel - pub g: u8, - /// Blue channel - pub b: u8, + pub value: Option>, } +/// Action to take on click of the text. #[derive(Clone, PartialEq, Debug, Serialize, Deserialize)] #[serde(tag = "action", content = "value", rename_all = "snake_case")] -enum ClickEvent { +pub enum ClickEvent { + /// Opens an URL OpenUrl(Cow<'static, str>), /// Only usable by internal servers for security reasons. OpenFile(Cow<'static, str>), + /// Sends a chat command. Doesn't actually have to be a command, can be a + /// normal chat message. RunCommand(Cow<'static, str>), + /// Replaces the contents of the chat box with the text, not necessarily a + /// command. SuggestCommand(Cow<'static, str>), + /// Only usable within written books. Changes the page of the book. Indexing + /// starts at 1. ChangePage(i32), + /// Copies the given text to clipboard CopyToClipboard(Cow<'static, str>), } +/// Action to take when mouse-hovering on the text. #[derive(Clone, PartialEq, Debug, Serialize, Deserialize)] #[serde(tag = "action", content = "contents", rename_all = "snake_case")] #[allow(clippy::enum_variant_names)] -enum HoverEvent { +pub enum HoverEvent { + /// Displays a tooltip with the given text. ShowText(Text), + /// Shows an item. ShowItem { - id: Ident, + /// Resource identifier of the item + id: Ident>, + /// Number of the items in the stack count: Option, - // TODO: tag + /// NBT information about the item (sNBT format) + tag: Cow<'static, str>, // TODO replace with newtype for sNBT? }, + /// Shows an entity. ShowEntity { - name: Text, - #[serde(rename = "type")] - kind: Ident, + /// The entity's UUID id: Uuid, + /// Resource identifier of the entity + #[serde(rename = "type")] + #[serde(default, skip_serializing_if = "Option::is_none")] + kind: Option>>, + /// Optional custom name for the entity + #[serde(default, skip_serializing_if = "Option::is_none")] + name: Option, }, } +/// The font of the text. +#[derive(Clone, Copy, PartialEq, Debug, Serialize, Deserialize)] +pub enum Font { + /// The default font. + #[serde(rename = "minecraft:default")] + Default, + /// Unicode font. + #[serde(rename = "minecraft:uniform")] + Uniform, + /// Enchanting table font. + #[serde(rename = "minecraft:alt")] + Alt, +} + #[allow(clippy::self_named_constructors)] impl Text { /// Constructs a new plain text object. @@ -379,7 +350,7 @@ impl Text { /// Creates a text component for a command storage NBT tag. pub fn storage_nbt( - storage: impl Into>, + storage: impl Into>>, nbt: impl Into>, interpret: Option, separator: Option, @@ -569,15 +540,21 @@ impl Text { strikethrough: Option, underlined: Option, italic: Option, - color: Option, + color: Option, } impl Modifiers { // Writes all active modifiers to a String as `§` fn write(&self, output: &mut String) { if let Some(color) = self.color { + let code = match color { + Color::Rgb(rgb) => rgb.to_named_lossy().hex_digit(), + Color::Named(normal) => normal.hex_digit(), + Color::Reset => return, + }; + output.push('§'); - output.push(color); + output.push(code); } if let Some(true) = self.obfuscated { output.push_str("§k"); @@ -616,25 +593,7 @@ impl Text { strikethrough: this.0.strikethrough, underlined: this.0.underlined, italic: this.0.italic, - color: this.0.color.map(|color| match color.to_legacy() { - Color::BLACK => '0', - Color::DARK_BLUE => '1', - Color::DARK_GREEN => '2', - Color::DARK_AQUA => '3', - Color::DARK_RED => '4', - Color::DARK_PURPLE => '5', - Color::GOLD => '6', - Color::GRAY => '7', - Color::DARK_GRAY => '8', - Color::BLUE => '9', - Color::GREEN => 'a', - Color::AQUA => 'b', - Color::RED => 'c', - Color::LIGHT_PURPLE => 'd', - Color::YELLOW => 'e', - Color::WHITE => 'f', - _ => unreachable!(), - }), + color: this.0.color, }; // If any modifiers were removed @@ -647,6 +606,7 @@ impl Text { ] .iter() .any(|m| *m == Some(false)) + || this.0.color == Some(Color::Reset) { // Reset and print sum of old and new modifiers result.push_str("§r"); @@ -675,201 +635,21 @@ impl Text { } } -/// Provides the methods necessary for working with [`Text`] objects. -/// -/// This trait exists to allow using `Into` types without having to first -/// convert the type into [`Text`]. A blanket implementation exists for all -/// `Into` types, including [`Text`] itself. -pub trait TextFormat: Into { - /// Converts this type into a [`Text`] object. - fn into_text(self) -> Text { - self.into() - } - - fn color(self, color: Color) -> Text { - let mut t = self.into(); - t.0.color = Some(color); - t - } - - fn clear_color(self) -> Text { - let mut t = self.into(); - t.0.color = None; - t - } - - fn font(self, font: impl Into>) -> Text { - let mut t = self.into(); - t.0.font = Some(font.into()); - t - } - - fn clear_font(self) -> Text { - let mut t = self.into(); - t.0.font = None; - t - } - - fn bold(self) -> Text { - let mut t = self.into(); - t.0.bold = Some(true); - t - } - - fn not_bold(self) -> Text { - let mut t = self.into(); - t.0.bold = Some(false); - t - } - - fn clear_bold(self) -> Text { - let mut t = self.into(); - t.0.bold = None; - t - } - - fn italic(self) -> Text { - let mut t = self.into(); - t.0.italic = Some(true); - t - } - - fn not_italic(self) -> Text { - let mut t = self.into(); - t.0.italic = Some(false); - t - } - - fn clear_italic(self) -> Text { - let mut t = self.into(); - t.0.italic = None; - t - } - - fn underlined(self) -> Text { - let mut t = self.into(); - t.0.underlined = Some(true); - t - } +impl Deref for Text { + type Target = TextInner; - fn not_underlined(self) -> Text { - let mut t = self.into(); - t.0.underlined = Some(false); - t - } - - fn clear_underlined(self) -> Text { - let mut t = self.into(); - t.0.underlined = None; - t - } - - fn strikethrough(self) -> Text { - let mut t = self.into(); - t.0.strikethrough = Some(true); - t - } - - fn not_strikethrough(self) -> Text { - let mut t = self.into(); - t.0.strikethrough = Some(false); - t - } - - fn clear_strikethrough(self) -> Text { - let mut t = self.into(); - t.0.strikethrough = None; - t - } - - fn obfuscated(self) -> Text { - let mut t = self.into(); - t.0.obfuscated = Some(true); - t - } - - fn not_obfuscated(self) -> Text { - let mut t = self.into(); - t.0.obfuscated = Some(false); - t - } - - fn clear_obfuscated(self) -> Text { - let mut t = self.into(); - t.0.obfuscated = None; - t - } - - fn insertion(self, insertion: impl Into>) -> Text { - let mut t = self.into(); - t.0.insertion = Some(insertion.into()); - t - } - - fn clear_insertion(self) -> Text { - let mut t = self.into(); - t.0.insertion = None; - t - } - - fn on_click_open_url(self, url: impl Into>) -> Text { - let mut t = self.into(); - t.0.click_event = Some(ClickEvent::OpenUrl(url.into())); - t - } - - fn on_click_run_command(self, command: impl Into>) -> Text { - let mut t = self.into(); - t.0.click_event = Some(ClickEvent::RunCommand(command.into())); - t - } - - fn on_click_suggest_command(self, command: impl Into>) -> Text { - let mut t = self.into(); - t.0.click_event = Some(ClickEvent::SuggestCommand(command.into())); - t - } - - fn on_click_change_page(self, page: impl Into) -> Text { - let mut t = self.into(); - t.0.click_event = Some(ClickEvent::ChangePage(page.into())); - t - } - - fn on_click_copy_to_clipboard(self, text: impl Into>) -> Text { - let mut t = self.into(); - t.0.click_event = Some(ClickEvent::CopyToClipboard(text.into())); - t - } - - fn clear_click_event(self) -> Text { - let mut t = self.into(); - t.0.click_event = None; - t - } - - fn on_hover_show_text(self, text: impl Into) -> Text { - let mut t = self.into(); - t.0.hover_event = Some(HoverEvent::ShowText(text.into())); - t - } - - fn clear_hover_event(self) -> Text { - let mut t = self.into(); - t.0.hover_event = None; - t + fn deref(&self) -> &Self::Target { + &self.0 } +} - fn add_child(self, text: impl Into) -> Text { - let mut t = self.into(); - t.0.extra.push(text.into()); - t +impl DerefMut for Text { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 } } -impl> TextFormat for T {} - -impl> ops::Add for Text { +impl> ops::Add for Text { type Output = Self; fn add(self, rhs: T) -> Self::Output { @@ -877,63 +657,9 @@ impl> ops::Add for Text { } } -impl> ops::AddAssign for Text { +impl> ops::AddAssign for Text { fn add_assign(&mut self, rhs: T) { - self.0.extra.push(rhs.into()); - } -} - -impl From for Text { - fn from(c: char) -> Self { - Text::text(String::from(c)) - } -} - -impl From for Text { - fn from(s: String) -> Self { - Text::text(s) - } -} - -impl From<&'static str> for Text { - fn from(s: &'static str) -> Self { - Text::text(s) - } -} - -impl From> for Text { - fn from(s: Cow<'static, str>) -> Self { - Text::text(s) - } -} - -impl From for Text { - fn from(value: i32) -> Self { - Text::text(value.to_string()) - } -} - -impl From for Text { - fn from(value: i64) -> Self { - Text::text(value.to_string()) - } -} - -impl From for Text { - fn from(value: u64) -> Self { - Text::text(value.to_string()) - } -} - -impl From for Text { - fn from(value: f64) -> Self { - Text::text(value.to_string()) - } -} - -impl From for Text { - fn from(value: bool) -> Self { - Text::text(value.to_string()) + self.extra.push(rhs.into_text()); } } @@ -970,6 +696,12 @@ impl fmt::Display for Text { } } +impl Default for TextContent { + fn default() -> Self { + Self::Text { text: "".into() } + } +} + impl Encode for Text { fn encode(&self, w: impl Write) -> anyhow::Result<()> { serde_json::to_string(self)?.encode(w) @@ -987,124 +719,63 @@ impl Decode<'_> for Text { } } -impl Default for TextContent { - fn default() -> Self { - Self::Text { text: "".into() } - } -} +impl<'de> Deserialize<'de> for Text { + fn deserialize>(deserializer: D) -> Result { + struct TextVisitor; -impl Color { - pub const AQUA: Color = Color::new(85, 255, 255); - pub const BLACK: Color = Color::new(0, 0, 0); - pub const BLUE: Color = Color::new(85, 85, 255); - pub const DARK_AQUA: Color = Color::new(0, 170, 170); - pub const DARK_BLUE: Color = Color::new(0, 0, 170); - pub const DARK_GRAY: Color = Color::new(85, 85, 85); - pub const DARK_GREEN: Color = Color::new(0, 170, 0); - pub const DARK_PURPLE: Color = Color::new(170, 0, 170); - pub const DARK_RED: Color = Color::new(170, 0, 0); - pub const GOLD: Color = Color::new(255, 170, 0); - pub const GRAY: Color = Color::new(170, 170, 170); - pub const GREEN: Color = Color::new(85, 255, 85); - pub const LIGHT_PURPLE: Color = Color::new(255, 85, 255); - pub const RED: Color = Color::new(255, 85, 85); - pub const WHITE: Color = Color::new(255, 255, 255); - pub const YELLOW: Color = Color::new(255, 255, 85); - - /// Constructs a new color from red, green, and blue components. - pub const fn new(r: u8, g: u8, b: u8) -> Self { - Self { r, g, b } - } + impl<'de> Visitor<'de> for TextVisitor { + type Value = Text; - // Returns the closest legacy color - pub fn to_legacy(self) -> Self { - [ - Self::AQUA, - Self::BLACK, - Self::BLUE, - Self::DARK_AQUA, - Self::DARK_BLUE, - Self::DARK_GRAY, - Self::DARK_GREEN, - Self::DARK_PURPLE, - Self::DARK_RED, - Self::GOLD, - Self::GRAY, - Self::GREEN, - Self::LIGHT_PURPLE, - Self::RED, - Self::WHITE, - Self::YELLOW, - ] - .into_iter() - .min_by_key(|legacy| { - (legacy.r as i32 - self.r as i32).pow(2) - + (legacy.g as i32 - self.g as i32).pow(2) - + (legacy.b as i32 - self.b as i32).pow(2) - }) - .unwrap() - } -} + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + write!(formatter, "a text component data type") + } -impl Serialize for Color { - fn serialize(&self, serializer: S) -> Result { - format!("#{:02x}{:02x}{:02x}", self.r, self.g, self.b).serialize(serializer) - } -} + fn visit_bool(self, v: bool) -> Result { + Ok(Text::text(v.to_string())) + } -impl<'de> Deserialize<'de> for Color { - fn deserialize>(deserializer: D) -> Result { - deserializer.deserialize_str(ColorVisitor) - } -} + fn visit_i64(self, v: i64) -> Result { + Ok(Text::text(v.to_string())) + } -struct ColorVisitor; + fn visit_u64(self, v: u64) -> Result { + Ok(Text::text(v.to_string())) + } -impl<'de> Visitor<'de> for ColorVisitor { - type Value = Color; + fn visit_f64(self, v: f64) -> Result { + Ok(Text::text(v.to_string())) + } - fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "a hex color of the form #rrggbb") - } + fn visit_str(self, v: &str) -> Result { + Ok(Text::text(v.to_string())) + } - fn visit_str(self, s: &str) -> Result { - color_from_str(s).ok_or_else(|| E::custom("invalid hex color")) - } -} + fn visit_string(self, v: String) -> Result { + Ok(Text::text(v)) + } + + fn visit_seq>(self, mut seq: A) -> Result { + let Some(mut res) = seq.next_element()? else { + return Ok(Text::default()); + }; -fn color_from_str(s: &str) -> Option { - let to_num = |d| match d { - b'0'..=b'9' => Some(d - b'0'), - b'a'..=b'f' => Some(d - b'a' + 0xa), - b'A'..=b'F' => Some(d - b'A' + 0xa), - _ => None, - }; - - match s.as_bytes() { - [b'#', r0, r1, g0, g1, b0, b1] => Some(Color { - r: to_num(*r0)? << 4 | to_num(*r1)?, - g: to_num(*g0)? << 4 | to_num(*g1)?, - b: to_num(*b0)? << 4 | to_num(*b1)?, - }), - _ => match s { - "aqua" => Some(Color::AQUA), - "black" => Some(Color::BLACK), - "blue" => Some(Color::BLUE), - "dark_aqua" => Some(Color::DARK_AQUA), - "dark_blue" => Some(Color::DARK_BLUE), - "dark_gray" => Some(Color::DARK_GRAY), - "dark_green" => Some(Color::DARK_GREEN), - "dark_purple" => Some(Color::DARK_PURPLE), - "dark_red" => Some(Color::DARK_RED), - "gold" => Some(Color::GOLD), - "gray" => Some(Color::GRAY), - "green" => Some(Color::GREEN), - "light_purple" => Some(Color::LIGHT_PURPLE), - "red" => Some(Color::RED), - "white" => Some(Color::WHITE), - "yellow" => Some(Color::YELLOW), - _ => None, - }, + while let Some(child) = seq.next_element::()? { + res += child; + } + + Ok(res) + } + + fn visit_map>(self, map: A) -> Result { + use de::value::MapAccessDeserializer; + + Ok(Text(Box::new(TextInner::deserialize( + MapAccessDeserializer::new(map), + )?))) + } + } + + deserializer.deserialize_any(TextVisitor) } } @@ -1134,20 +805,6 @@ mod tests { assert_eq!(before.to_string(), after.to_string()); } - #[test] - fn text_color() { - assert_eq!( - color_from_str("#aBcDeF"), - Some(Color::new(0xab, 0xcd, 0xef)) - ); - assert_eq!(color_from_str("#fFfFfF"), Some(Color::new(255, 255, 255))); - assert_eq!(color_from_str("#00000000"), None); - assert_eq!(color_from_str("#000000"), Some(Color::BLACK)); - assert_eq!(color_from_str("#"), None); - assert_eq!(color_from_str("red"), Some(Color::RED)); - assert_eq!(color_from_str("blue"), Some(Color::BLUE)); - } - #[test] fn non_object_data_types() { let input = r#"["foo", true, false, 1.9E10, 9999]"#; @@ -1160,7 +817,7 @@ mod tests { fn translate() { let txt = Text::translate( translation_key::CHAT_TYPE_ADVANCEMENT_TASK, - ["arg1".into(), "arg2".into()], + ["arg1".into_text(), "arg2".into_text()], ); let serialized = serde_json::to_string(&txt).unwrap(); let deserialized: Text = serde_json::from_str(&serialized).unwrap(); @@ -1191,7 +848,7 @@ mod tests { let deserialized: Text = serde_json::from_str(&serialized).unwrap(); assert_eq!( serialized, - r##"{"selector":"foo","separator":{"text":"bar","color":"#ff5555","bold":true}}"## + r##"{"selector":"foo","separator":{"text":"bar","color":"red","bold":true}}"## ); assert_eq!(txt, deserialized); } @@ -1207,7 +864,7 @@ mod tests { #[test] fn block_nbt() { - let txt = Text::block_nbt("foo", "bar", Some(true), Some("baz".into())); + let txt = Text::block_nbt("foo", "bar", Some(true), Some("baz".into_text())); let serialized = serde_json::to_string(&txt).unwrap(); let deserialized: Text = serde_json::from_str(&serialized).unwrap(); let expected = r#"{"block":"foo","nbt":"bar","interpret":true,"separator":{"text":"baz"}}"#; @@ -1217,7 +874,7 @@ mod tests { #[test] fn entity_nbt() { - let txt = Text::entity_nbt("foo", "bar", Some(true), Some("baz".into())); + let txt = Text::entity_nbt("foo", "bar", Some(true), Some("baz".into_text())); let serialized = serde_json::to_string(&txt).unwrap(); let deserialized: Text = serde_json::from_str(&serialized).unwrap(); let expected = @@ -1228,7 +885,7 @@ mod tests { #[test] fn storage_nbt() { - let txt = Text::storage_nbt(ident!("foo"), "bar", Some(true), Some("baz".into())); + let txt = Text::storage_nbt(ident!("foo"), "bar", Some(true), Some("baz".into_text())); let serialized = serde_json::to_string(&txt).unwrap(); let deserialized: Text = serde_json::from_str(&serialized).unwrap(); let expected = r#"{"storage":"minecraft:foo","nbt":"bar","interpret":true,"separator":{"text":"baz"}}"#; @@ -1244,21 +901,21 @@ mod tests { .strikethrough() .underlined() .obfuscated() - .color(Color { r: 0, g: 255, b: 0 }) + .color(Color::GREEN) + "Lightly formatted red text\n" .not_bold() .not_strikethrough() .not_obfuscated() - .color(Color { r: 255, g: 0, b: 0 }) + .color(Color::RED) + "Not formatted blue text" .not_italic() .not_underlined() - .color(Color { r: 0, g: 0, b: 255 }); + .color(Color::BLUE); assert_eq!( text.to_legacy_lossy(), - "§2§k§l§m§n§oHeavily formatted green text\n§r§4§n§oLightly formatted red \ - text\n§r§1Not formatted blue text" + "§a§k§l§m§n§oHeavily formatted green text\n§r§c§n§oLightly formatted red \ + text\n§r§9Not formatted blue text" ); } } diff --git a/crates/valence_core/src/text/color.rs b/crates/valence_core/src/text/color.rs new file mode 100644 index 000000000..c05831587 --- /dev/null +++ b/crates/valence_core/src/text/color.rs @@ -0,0 +1,368 @@ +//! [`Color`] and related data structures. + +use std::fmt; +use std::hash::Hash; + +use serde::de::Visitor; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use thiserror::Error; + +/// Text color +#[derive(Default, Debug, PartialOrd, Eq, Ord, Clone, Copy)] +pub enum Color { + /// The default color for the text will be used, which varies by context + /// (in some cases, it's white; in others, it's black; in still others, it + /// is a shade of gray that isn't normally used on text). + #[default] + Reset, + /// RGB Color + Rgb(RgbColor), + /// One of the 16 named Minecraft colors + Named(NamedColor), +} + +/// RGB Color +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] +pub struct RgbColor { + /// Red channel + pub r: u8, + /// Green channel + pub g: u8, + /// Blue channel + pub b: u8, +} + +/// Named Minecraft color +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] +pub enum NamedColor { + /// Hex digit: `0`, name: `black` + Black = 0, + /// Hex digit: `1`, name: `dark_blue` + DarkBlue, + /// Hex digit: `2`, name: `dark_green` + DarkGreen, + /// Hex digit: `3`, name: `dark_aqua` + DarkAqua, + /// Hex digit: `4`, name: `dark_red` + DarkRed, + /// Hex digit: `5`, name: `dark_purple` + DarkPurple, + /// Hex digit: `6`, name: `gold` + Gold, + /// Hex digit: `7`, name: `gray` + Gray, + /// Hex digit: `8`, name: `dark_gray` + DarkGray, + /// Hex digit: `9`, name: `blue` + Blue, + /// Hex digit: `a`, name: `green` + Green, + /// Hex digit: `b`, name: `aqua` + Aqua, + /// Hex digit: `c`, name: `red` + Red, + /// Hex digit: `d`, name: `light_purple` + LightPurple, + /// Hex digit: `e`, name: `yellow` + Yellow, + /// Hex digit: `f`, name: `white` + White, +} + +/// Color parsing error +#[derive(Debug, Error, PartialEq, PartialOrd, Clone, Copy, Hash, Eq, Ord)] +#[error("invalid color name or hex code")] +pub struct ColorError; + +impl Color { + pub const RESET: Self = Self::Reset; + pub const AQUA: Self = Self::Named(NamedColor::Aqua); + pub const BLACK: Self = Self::Named(NamedColor::Black); + pub const BLUE: Self = Self::Named(NamedColor::Blue); + pub const DARK_AQUA: Self = Self::Named(NamedColor::DarkAqua); + pub const DARK_BLUE: Self = Self::Named(NamedColor::DarkBlue); + pub const DARK_GRAY: Self = Self::Named(NamedColor::DarkGray); + pub const DARK_GREEN: Self = Self::Named(NamedColor::DarkGreen); + pub const DARK_PURPLE: Self = Self::Named(NamedColor::DarkPurple); + pub const DARK_RED: Self = Self::Named(NamedColor::DarkRed); + pub const GOLD: Self = Self::Named(NamedColor::Gold); + pub const GRAY: Self = Self::Named(NamedColor::Gray); + pub const GREEN: Self = Self::Named(NamedColor::Green); + pub const LIGHT_PURPLE: Self = Self::Named(NamedColor::LightPurple); + pub const RED: Self = Self::Named(NamedColor::Red); + pub const WHITE: Self = Self::Named(NamedColor::White); + pub const YELLOW: Self = Self::Named(NamedColor::Yellow); + + /// Constructs a new RGB color + pub const fn rgb(r: u8, g: u8, b: u8) -> Self { + Self::Rgb(RgbColor::new(r, g, b)) + } +} + +impl RgbColor { + /// Constructs a new color from red, green, and blue components. + pub const fn new(r: u8, g: u8, b: u8) -> Self { + Self { r, g, b } + } + /// Converts the RGB color to the closest [`NamedColor`] equivalent (lossy). + pub fn to_named_lossy(self) -> NamedColor { + // calculates the squared distance between 2 colors + fn squared_distance(c1: RgbColor, c2: RgbColor) -> i32 { + (c1.r as i32 - c2.r as i32).pow(2) + + (c1.g as i32 - c2.g as i32).pow(2) + + (c1.b as i32 - c2.b as i32).pow(2) + } + + [ + NamedColor::Aqua, + NamedColor::Black, + NamedColor::Blue, + NamedColor::DarkAqua, + NamedColor::DarkBlue, + NamedColor::DarkGray, + NamedColor::DarkGreen, + NamedColor::DarkPurple, + NamedColor::DarkRed, + NamedColor::Gold, + NamedColor::Gray, + NamedColor::Green, + NamedColor::LightPurple, + NamedColor::Red, + NamedColor::White, + NamedColor::Yellow, + ] + .into_iter() + .min_by_key(|&named| squared_distance(named.into(), self)) + .unwrap() + } +} + +impl NamedColor { + /// Returns the corresponding hex digit of the color. + pub const fn hex_digit(self) -> char { + b"0123456789abcdef"[self as usize] as char + } + /// Returns the identifier of the color. + pub const fn name(self) -> &'static str { + [ + "black", + "dark_blue", + "dark_green", + "dark_aqua", + "dark_red", + "dark_purple", + "gold", + "gray", + "dark_gray", + "blue", + "green", + "aqua", + "red", + "light_purple", + "yellow", + "white", + ][self as usize] + } +} + +impl PartialEq for Color { + fn eq(&self, other: &Self) -> bool { + match (*self, *other) { + (Self::Reset, Self::Reset) => true, + (Self::Rgb(rgb1), Self::Rgb(rgb2)) => rgb1 == rgb2, + (Self::Named(normal1), Self::Named(normal2)) => normal1 == normal2, + (Self::Rgb(rgb), Self::Named(normal)) | (Self::Named(normal), Self::Rgb(rgb)) => { + rgb == RgbColor::from(normal) + } + (Self::Reset, _) | (_, Self::Reset) => false, + } + } +} + +impl Hash for Color { + fn hash(&self, state: &mut H) { + match self { + Self::Reset => state.write_u8(0), + Self::Rgb(rgb) => { + state.write_u8(1); + rgb.hash(state); + } + Self::Named(normal) => { + state.write_u8(1); + RgbColor::from(*normal).hash(state); + } + } + } +} + +impl From for RgbColor { + fn from(value: NamedColor) -> Self { + match value { + NamedColor::Aqua => Self::new(85, 255, 255), + NamedColor::Black => Self::new(0, 0, 0), + NamedColor::Blue => Self::new(85, 85, 255), + NamedColor::DarkAqua => Self::new(0, 170, 170), + NamedColor::DarkBlue => Self::new(0, 0, 170), + NamedColor::DarkGray => Self::new(85, 85, 85), + NamedColor::DarkGreen => Self::new(0, 170, 0), + NamedColor::DarkPurple => Self::new(170, 0, 170), + NamedColor::DarkRed => Self::new(170, 0, 0), + NamedColor::Gold => Self::new(255, 170, 0), + NamedColor::Gray => Self::new(170, 170, 170), + NamedColor::Green => Self::new(85, 255, 85), + NamedColor::LightPurple => Self::new(255, 85, 255), + NamedColor::Red => Self::new(255, 85, 85), + NamedColor::White => Self::new(255, 255, 255), + NamedColor::Yellow => Self::new(255, 255, 85), + } + } +} + +impl From for Color { + fn from(value: RgbColor) -> Self { + Self::Rgb(value) + } +} + +impl From for Color { + fn from(value: NamedColor) -> Self { + Self::Named(value) + } +} + +impl TryFrom<&str> for Color { + type Error = ColorError; + + fn try_from(value: &str) -> Result { + if value.starts_with('#') { + return Ok(Self::Rgb(RgbColor::try_from(value)?)); + } + + if value == "reset" { + return Ok(Self::Reset); + } + + Ok(Self::Named(NamedColor::try_from(value)?)) + } +} + +impl TryFrom<&str> for NamedColor { + type Error = ColorError; + + fn try_from(value: &str) -> Result { + match value { + "black" => Ok(NamedColor::Black), + "dark_blue" => Ok(NamedColor::DarkBlue), + "dark_green" => Ok(NamedColor::DarkGreen), + "dark_aqua" => Ok(NamedColor::DarkAqua), + "dark_red" => Ok(NamedColor::DarkRed), + "dark_purple" => Ok(NamedColor::DarkPurple), + "gold" => Ok(NamedColor::Gold), + "gray" => Ok(NamedColor::Gray), + "dark_gray" => Ok(NamedColor::DarkGray), + "blue" => Ok(NamedColor::Blue), + "green" => Ok(NamedColor::Green), + "aqua" => Ok(NamedColor::Aqua), + "red" => Ok(NamedColor::Red), + "light_purple" => Ok(NamedColor::LightPurple), + "yellow" => Ok(NamedColor::Yellow), + "white" => Ok(NamedColor::White), + _ => Err(ColorError), + } + } +} + +impl TryFrom<&str> for RgbColor { + type Error = ColorError; + + fn try_from(value: &str) -> Result { + let to_num = |d| match d { + b'0'..=b'9' => Ok(d - b'0'), + b'a'..=b'f' => Ok(d - b'a' + 0xa), + b'A'..=b'F' => Ok(d - b'A' + 0xa), + _ => Err(ColorError), + }; + + if let &[b'#', r0, r1, g0, g1, b0, b1] = value.as_bytes() { + Ok(RgbColor { + r: to_num(r0)? << 4 | to_num(r1)?, + g: to_num(g0)? << 4 | to_num(g1)?, + b: to_num(b0)? << 4 | to_num(b1)?, + }) + } else { + Err(ColorError) + } + } +} + +impl Serialize for Color { + fn serialize(&self, serializer: S) -> Result { + format!("{}", self).serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for Color { + fn deserialize>(deserializer: D) -> Result { + deserializer.deserialize_str(ColorVisitor) + } +} + +struct ColorVisitor; + +impl<'de> Visitor<'de> for ColorVisitor { + type Value = Color; + + fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "a hex color (#rrggbb), a normal color or 'reset'") + } + + fn visit_str(self, s: &str) -> Result { + Color::try_from(s).map_err(|_| E::custom("invalid color")) + } +} + +impl fmt::Display for Color { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Color::Reset => write!(f, "reset"), + Color::Rgb(rgb) => rgb.fmt(f), + Color::Named(normal) => normal.fmt(f), + } + } +} + +impl fmt::Display for RgbColor { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "#{:02x}{:02x}{:02x}", self.r, self.g, self.b) + } +} + +impl fmt::Display for NamedColor { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.name()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn colors() { + assert_eq!( + Color::try_from("#aBcDeF"), + Ok(RgbColor::new(0xab, 0xcd, 0xef).into()) + ); + assert_eq!( + Color::try_from("#fFfFfF"), + Ok(RgbColor::new(255, 255, 255).into()) + ); + assert_eq!(Color::try_from("#000000"), Ok(NamedColor::Black.into())); + assert_eq!(Color::try_from("red"), Ok(NamedColor::Red.into())); + assert_eq!(Color::try_from("blue"), Ok(NamedColor::Blue.into())); + assert!(Color::try_from("#ffTf00").is_err()); + assert!(Color::try_from("#ffš00").is_err()); + assert!(Color::try_from("#00000000").is_err()); + assert!(Color::try_from("#").is_err()); + } +} diff --git a/crates/valence_core/src/text/into_text.rs b/crates/valence_core/src/text/into_text.rs new file mode 100644 index 000000000..c13b679a7 --- /dev/null +++ b/crates/valence_core/src/text/into_text.rs @@ -0,0 +1,377 @@ +//! Provides the [`IntoText`] trait and implementations. + +use std::borrow::Cow; + +use super::{ClickEvent, Color, Font, HoverEvent, Text}; + +/// Trait for any data that can be converted to a [`Text`] object. +/// +/// Also conveniently provides many useful methods for modifying a [`Text`] +/// object. +/// +/// # Usage +/// +/// ``` +/// # use valence_core::text::{IntoText, color::NamedColor}; +/// let mut my_text = "".into_text(); +/// my_text = my_text.color(NamedColor::Red).bold(); +/// my_text = my_text.add_child("CRABBBBB".obfuscated()); +pub trait IntoText<'a>: Sized { + /// Converts to a [`Text`] object, either owned or borrowed. + fn into_cow_text(self) -> Cow<'a, Text>; + + /// Converts to an owned [`Text`] object. + fn into_text(self) -> Text { + self.into_cow_text().into_owned() + } + + /// Sets the color of the text. + fn color(self, color: impl Into) -> Text { + let mut value = self.into_text(); + value.color = Some(color.into()); + value + } + /// Clears the color of the text. Color of parent [`Text`] object will be + /// used. + fn clear_color(self) -> Text { + let mut value = self.into_text(); + value.color = None; + value + } + + /// Sets the font of the text. + fn font(self, font: Font) -> Text { + let mut value = self.into_text(); + value.font = Some(font); + value + } + /// Clears the font of the text. Font of parent [`Text`] object will be + /// used. + fn clear_font(self) -> Text { + let mut value = self.into_text(); + value.font = None; + value + } + + /// Makes the text bold. + fn bold(self) -> Text { + let mut value = self.into_text(); + value.bold = Some(true); + value + } + /// Makes the text not bold. + fn not_bold(self) -> Text { + let mut value = self.into_text(); + value.bold = Some(false); + value + } + /// Clears the `bold` property of the text. Property of the parent [`Text`] + /// object will be used. + fn clear_bold(self) -> Text { + let mut value = self.into_text(); + value.bold = None; + value + } + + /// Makes the text italic. + fn italic(self) -> Text { + let mut value = self.into_text(); + value.italic = Some(true); + value + } + /// Makes the text not italic. + fn not_italic(self) -> Text { + let mut value = self.into_text(); + value.italic = Some(false); + value + } + /// Clears the `italic` property of the text. Property of the parent + /// [`Text`] object will be used. + fn clear_italic(self) -> Text { + let mut value = self.into_text(); + value.italic = None; + value + } + + /// Makes the text underlined. + fn underlined(self) -> Text { + let mut value = self.into_text(); + value.underlined = Some(true); + value + } + /// Makes the text not underlined. + fn not_underlined(self) -> Text { + let mut value = self.into_text(); + value.underlined = Some(false); + value + } + /// Clears the `underlined` property of the text. Property of the parent + /// [`Text`] object will be used. + fn clear_underlined(self) -> Text { + let mut value = self.into_text(); + value.underlined = None; + value + } + + /// Adds a strikethrough effect to the text. + fn strikethrough(self) -> Text { + let mut value = self.into_text(); + value.strikethrough = Some(true); + value + } + /// Removes the strikethrough effect from the text. + fn not_strikethrough(self) -> Text { + let mut value = self.into_text(); + value.strikethrough = Some(false); + value + } + /// Clears the `strikethrough` property of the text. Property of the parent + /// [`Text`] object will be used. + fn clear_strikethrough(self) -> Text { + let mut value = self.into_text(); + value.strikethrough = None; + value + } + + /// Makes the text obfuscated. + fn obfuscated(self) -> Text { + let mut value = self.into_text(); + value.obfuscated = Some(true); + value + } + /// Makes the text not obfuscated. + fn not_obfuscated(self) -> Text { + let mut value = self.into_text(); + value.obfuscated = Some(false); + value + } + /// Clears the `obfuscated` property of the text. Property of the parent + /// [`Text`] object will be used. + fn clear_obfuscated(self) -> Text { + let mut value = self.into_text(); + value.obfuscated = None; + value + } + + /// Adds an `insertion` property to the text. When shift-clicked, the given + /// text will be inserted into chat box for the client. + fn insertion(self, insertion: impl Into>) -> Text { + let mut value = self.into_text(); + value.insertion = Some(insertion.into()); + value + } + /// Clears the `insertion` property of the text. Property of the parent + /// [`Text`] object will be used. + fn clear_insertion(self) -> Text { + let mut value = self.into_text(); + value.insertion = None; + value + } + + /// On click, opens the given URL. Has to be `http` or `https` protocol. + fn on_click_open_url(self, url: impl Into>) -> Text { + let mut value = self.into_text(); + value.click_event = Some(ClickEvent::OpenUrl(url.into())); + value + } + /// On click, sends a command. Doesn't actually have to be a command, can be + /// a simple chat message. + fn on_click_run_command(self, command: impl Into>) -> Text { + let mut value = self.into_text(); + value.click_event = Some(ClickEvent::RunCommand(command.into())); + value + } + /// On click, copies the given text to the chat box. + fn on_click_suggest_command(self, command: impl Into>) -> Text { + let mut value = self.into_text(); + value.click_event = Some(ClickEvent::SuggestCommand(command.into())); + value + } + /// On click, turns the page of the opened book to the given number. + /// Indexing starts at `1`. + fn on_click_change_page(self, page: impl Into) -> Text { + let mut value = self.into_text(); + value.click_event = Some(ClickEvent::ChangePage(page.into())); + value + } + /// On click, copies the given text to clipboard. + fn on_click_copy_to_clipboard(self, text: impl Into>) -> Text { + let mut value = self.into_text(); + value.click_event = Some(ClickEvent::CopyToClipboard(text.into())); + value + } + /// Clears the `click_event` property of the text. Property of the parent + /// [`Text`] object will be used. + fn clear_click_event(self) -> Text { + let mut value = self.into_text(); + value.click_event = None; + value + } + + /// On mouse hover, shows the given text in a tooltip. + fn on_hover_show_text(self, text: impl IntoText<'static>) -> Text { + let mut value = self.into_text(); + value.hover_event = Some(HoverEvent::ShowText(text.into_text())); + value + } + /// Clears the `hover_event` property of the text. Property of the parent + /// [`Text`] object will be used. + fn clear_hover_event(self) -> Text { + let mut value = self.into_text(); + value.hover_event = None; + value + } + + /// Adds a child [`Text`] object. + fn add_child(self, text: impl IntoText<'static>) -> Text { + let mut value = self.into_text(); + value.extra.push(text.into_text()); + value + } +} + +impl<'a> IntoText<'a> for Text { + fn into_cow_text(self) -> Cow<'a, Text> { + Cow::Owned(self) + } +} +impl<'a> IntoText<'a> for &'a Text { + fn into_cow_text(self) -> Cow<'a, Text> { + Cow::Borrowed(self) + } +} +impl<'a> From<&'a Text> for Text { + fn from(value: &'a Text) -> Self { + value.clone() + } +} + +impl<'a> IntoText<'a> for Cow<'a, Text> { + fn into_cow_text(self) -> Cow<'a, Text> { + self + } +} +impl<'a> From> for Text { + fn from(value: Cow<'a, Text>) -> Self { + value.into_owned() + } +} +impl<'a, 'b> IntoText<'a> for &'a Cow<'b, Text> { + fn into_cow_text(self) -> Cow<'a, Text> { + self.clone() + } +} +impl<'a, 'b> From<&'a Cow<'b, Text>> for Text { + fn from(value: &'a Cow<'b, Text>) -> Self { + value.clone().into_owned() + } +} + +impl<'a> IntoText<'a> for String { + fn into_cow_text(self) -> Cow<'a, Text> { + Cow::Owned(Text::text(self)) + } +} +impl From for Text { + fn from(value: String) -> Self { + value.into_text() + } +} +impl<'a, 'b> IntoText<'b> for &'a String { + fn into_cow_text(self) -> Cow<'b, Text> { + Cow::Owned(Text::text(self.clone())) + } +} +impl<'a> From<&'a String> for Text { + fn from(value: &'a String) -> Self { + value.into_text() + } +} + +impl<'a> IntoText<'a> for Cow<'static, str> { + fn into_cow_text(self) -> Cow<'a, Text> { + Cow::Owned(Text::text(self)) + } +} +impl From> for Text { + fn from(value: Cow<'static, str>) -> Self { + value.into_text() + } +} +impl<'a> IntoText<'static> for &'a Cow<'static, str> { + fn into_cow_text(self) -> Cow<'static, Text> { + Cow::Owned(Text::text(self.clone())) + } +} +impl<'a> From<&'a Cow<'static, str>> for Text { + fn from(value: &'a Cow<'static, str>) -> Self { + value.into_text() + } +} + +impl<'a> IntoText<'a> for &'static str { + fn into_cow_text(self) -> Cow<'a, Text> { + Cow::Owned(Text::text(self)) + } +} +impl From<&'static str> for Text { + fn from(value: &'static str) -> Self { + value.into_text() + } +} + +impl<'a, 'b, T: IntoText<'a>, const N: usize> IntoText<'b> for [T; N] { + fn into_cow_text(self) -> Cow<'b, Text> { + let mut txt = Text::text(""); + + for child in self { + txt = txt.add_child(child.into_cow_text().into_owned()); + } + + Cow::Owned(txt) + } +} + +impl<'a, 'b, 'c, T: IntoText<'a> + Clone, const N: usize> IntoText<'c> for &'b [T; N] { + fn into_cow_text(self) -> Cow<'c, Text> { + let mut txt = Text::text(""); + + for child in self { + txt = txt.add_child(child.clone().into_cow_text().into_owned()); + } + + Cow::Owned(txt) + } +} + +macro_rules! impl_primitives { + ($($primitive:ty),+) => { + $( + impl<'a> IntoText<'a> for $primitive { + fn into_cow_text(self) -> Cow<'a, Text> { + Cow::Owned(Text::text(self.to_string())) + } + } + )+ + }; +} +impl_primitives! {char, bool, f32, f64, isize, usize, i8, i16, i32, i64, i128, u8, u16, u32, u64, u128} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn intotext_trait() { + fn is_borrowed<'a>(value: impl IntoText<'a>) -> bool { + matches!(value.into_cow_text(), Cow::Borrowed(..)) + } + + assert!(is_borrowed(&"this should be borrowed".into_text())); + assert!(is_borrowed(&"this should be borrowed too".bold())); + assert!(!is_borrowed("this should be owned?".bold())); + assert!(!is_borrowed("this should be owned")); + assert!(!is_borrowed(465)); + assert!(!is_borrowed(false)); + } +} diff --git a/crates/valence_inventory/src/lib.rs b/crates/valence_inventory/src/lib.rs index 4f274f6f3..1739e759f 100644 --- a/crates/valence_inventory/src/lib.rs +++ b/crates/valence_inventory/src/lib.rs @@ -38,7 +38,7 @@ use valence_core::game_mode::GameMode; use valence_core::item::ItemStack; use valence_core::protocol::encode::WritePacket; use valence_core::protocol::var_int::VarInt; -use valence_core::text::Text; +use valence_core::text::{IntoText, Text}; pub mod packet; mod validate; @@ -98,9 +98,9 @@ impl Inventory { Self::with_title(kind, "Inventory") } - pub fn with_title(kind: InventoryKind, title: impl Into) -> Self { + pub fn with_title<'a>(kind: InventoryKind, title: impl IntoText<'a>) -> Self { Inventory { - title: title.into(), + title: title.into_cow_text().into_owned(), kind, slots: vec![None; kind.slot_count()].into(), changed: 0, @@ -264,16 +264,16 @@ impl Inventory { /// inv.set_title("Box of Holding"); /// ``` #[inline] - pub fn set_title(&mut self, title: impl Into) { + pub fn set_title<'a>(&mut self, title: impl IntoText<'a>) { let _ = self.replace_title(title); } /// Replace the text displayed on the inventory's title bar, and returns the /// old text. #[must_use] - pub fn replace_title(&mut self, title: impl Into) -> Text { + pub fn replace_title<'a>(&mut self, title: impl IntoText<'a>) -> Text { // TODO: set title modified flag - std::mem::replace(&mut self.title, title.into()) + std::mem::replace(&mut self.title, title.into_cow_text().into_owned()) } pub(crate) fn slot_slice(&self) -> &[Option] { diff --git a/crates/valence_network/src/connect.rs b/crates/valence_network/src/connect.rs index 178e84781..9affed059 100644 --- a/crates/valence_network/src/connect.rs +++ b/crates/valence_network/src/connect.rs @@ -25,7 +25,7 @@ use valence_core::protocol::encode::PacketEncoder; use valence_core::protocol::raw::RawBytes; use valence_core::protocol::var_int::VarInt; use valence_core::protocol::Decode; -use valence_core::text::Text; +use valence_core::text::{Color, Text}; use valence_core::{ident, translation_key, PROTOCOL_VERSION}; use crate::legacy_ping::try_handle_legacy_ping; @@ -189,11 +189,27 @@ async fn handle_status( online_players, max_players, player_sample, - description, + mut description, favicon_png, version_name, protocol, } => { + // For pre-1.16 clients, replace all webcolors with their closest + // normal colors Because webcolor support was only + // added at 1.16. + if handshake.protocol_version < 735 { + fn fallback_webcolors(txt: &mut Text) { + if let Some(Color::Rgb(ref color)) = txt.color { + txt.color = Some(Color::Named(color.to_named_lossy())); + } + for child in &mut txt.extra { + fallback_webcolors(child); + } + } + + fallback_webcolors(&mut description); + } + let mut json = json!({ "version": { "name": version_name, diff --git a/crates/valence_network/src/lib.rs b/crates/valence_network/src/lib.rs index 0a1e447df..20c971388 100644 --- a/crates/valence_network/src/lib.rs +++ b/crates/valence_network/src/lib.rs @@ -47,7 +47,7 @@ use tokio::time; use tracing::error; use uuid::Uuid; use valence_client::{ClientBundle, ClientBundleArgs, Properties, SpawnClientsSet}; -use valence_core::text::Text; +use valence_core::text::{IntoText, Text}; use valence_core::{Server, MINECRAFT_VERSION, PROTOCOL_VERSION}; pub struct NetworkPlugin; @@ -345,7 +345,7 @@ pub trait NetworkCallbacks: Send + Sync + 'static { online_players: shared.player_count().load(Ordering::Relaxed) as i32, max_players: shared.max_players() as i32, player_sample: vec![], - description: "A Valence Server".into(), + description: "A Valence Server".into_text(), favicon_png: &[], version_name: MINECRAFT_VERSION.to_owned(), protocol: PROTOCOL_VERSION, @@ -467,7 +467,7 @@ pub trait NetworkCallbacks: Send + Sync + 'static { })) } else { // TODO: use correct translation key. - Err("Server Full".into()) + Err("Server Full".into_text()) } } diff --git a/crates/valence_player_list/src/lib.rs b/crates/valence_player_list/src/lib.rs index de1db6b3d..0d4f1aa73 100644 --- a/crates/valence_player_list/src/lib.rs +++ b/crates/valence_player_list/src/lib.rs @@ -30,7 +30,7 @@ use valence_client::{Client, Ping, Properties, Username}; use valence_core::despawn::Despawned; use valence_core::game_mode::GameMode; use valence_core::protocol::encode::{PacketWriter, WritePacket}; -use valence_core::text::Text; +use valence_core::text::{IntoText, Text}; use valence_core::uuid::UniqueId; use valence_core::Server; use valence_instance::WriteUpdatePacketsToInstancesSet; @@ -99,8 +99,8 @@ impl PlayerList { &self.footer } - pub fn set_header(&mut self, txt: impl Into) { - let txt = txt.into(); + pub fn set_header<'a>(&mut self, txt: impl IntoText<'a>) { + let txt = txt.into_cow_text().into_owned(); if txt != self.header { self.changed_header_or_footer = true; @@ -109,8 +109,8 @@ impl PlayerList { self.header = txt; } - pub fn set_footer(&mut self, txt: impl Into) { - let txt = txt.into(); + pub fn set_footer<'a>(&mut self, txt: impl IntoText<'a>) { + let txt = txt.into_cow_text().into_owned(); if txt != self.footer { self.changed_header_or_footer = true; diff --git a/examples/player_list.rs b/examples/player_list.rs index 55407cb2d..853f55dbd 100644 --- a/examples/player_list.rs +++ b/examples/player_list.rs @@ -74,7 +74,7 @@ fn init_clients( fn override_display_name(mut clients: Query<&mut DisplayName, (Added, With)>) { for mut display_name in &mut clients { - display_name.0 = Some("ඞ".color(Color::new(255, 87, 66))); + display_name.0 = Some("ඞ".color(Color::rgb(255, 87, 66))); } } @@ -94,7 +94,7 @@ fn update_player_list( for (_, uuid, mut display_name) in &mut entries { if uuid.0 == PLAYER_UUID_1 { let mut rng = rand::thread_rng(); - let color = Color::new(rng.gen(), rng.gen(), rng.gen()); + let color = Color::rgb(rng.gen(), rng.gen(), rng.gen()); let new_name = display_name.0.clone().unwrap_or_default().color(color); display_name.0 = Some(new_name); diff --git a/examples/server_list_ping.rs b/examples/server_list_ping.rs index 017475ff7..eca06a8a5 100644 --- a/examples/server_list_ping.rs +++ b/examples/server_list_ping.rs @@ -41,7 +41,7 @@ impl NetworkCallbacks for MyCallbacks { id: Uuid::from_u128(12345), }], description: "Your IP address is ".into_text() - + remote_addr.to_string().color(Color::DARK_GRAY), + + remote_addr.to_string().color(Color::rgb(50, 50, 250)), favicon_png: include_bytes!("../assets/logo-64x64.png"), version_name: ("Valence ".color(Color::GOLD) + MINECRAFT_VERSION.color(Color::RED)) .to_legacy_lossy(), diff --git a/src/lib.rs b/src/lib.rs index 579b5e97b..87f810878 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -99,7 +99,7 @@ pub mod prelude { pub use valence_core::ident; // Export the `ident!` macro. pub use valence_core::item::{ItemKind, ItemStack}; pub use valence_core::particle::Particle; - pub use valence_core::text::{Color, Text, TextFormat}; + pub use valence_core::text::{Color, IntoText, Text}; pub use valence_core::uuid::UniqueId; pub use valence_core::{translation_key, CoreSettings, Server}; pub use valence_dimension::{DimensionType, DimensionTypeRegistry};