From 14ef2b445adcec770defe1abf83af9d22ccf39d8 Mon Sep 17 00:00:00 2001 From: Ekdohibs Date: Tue, 31 May 2016 17:30:11 +0200 Subject: Add colored text (not only colored chat). Add documentation, move files to a proper place and avoid memory leaks. Make it work with most kind of texts, and allow backgrounds too. --- src/client/CMakeLists.txt | 1 - src/client/guiChatConsole.cpp | 664 ------------------------------------------ src/client/guiChatConsole.h | 138 --------- 3 files changed, 803 deletions(-) delete mode 100644 src/client/guiChatConsole.cpp delete mode 100644 src/client/guiChatConsole.h (limited to 'src/client') diff --git a/src/client/CMakeLists.txt b/src/client/CMakeLists.txt index bcf114760..a1ec37fe3 100644 --- a/src/client/CMakeLists.txt +++ b/src/client/CMakeLists.txt @@ -1,6 +1,5 @@ set(client_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/clientlauncher.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/guiChatConsole.cpp ${CMAKE_CURRENT_SOURCE_DIR}/tile.cpp PARENT_SCOPE ) diff --git a/src/client/guiChatConsole.cpp b/src/client/guiChatConsole.cpp deleted file mode 100644 index d8837556a..000000000 --- a/src/client/guiChatConsole.cpp +++ /dev/null @@ -1,664 +0,0 @@ -/* -Minetest -Copyright (C) 2013 celeron55, Perttu Ahola - -This program is free software; you can redistribute it and/or modify -it under the terms of the GNU Lesser General Public License as published by -the Free Software Foundation; either version 2.1 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Lesser General Public License for more details. - -You should have received a copy of the GNU Lesser General Public License along -with this program; if not, write to the Free Software Foundation, Inc., -51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -*/ - -#include "guiChatConsole.h" -#include "chat.h" -#include "client.h" -#include "debug.h" -#include "gettime.h" -#include "keycode.h" -#include "settings.h" -#include "porting.h" -#include "client/tile.h" -#include "fontengine.h" -#include "log.h" -#include "gettext.h" -#include - -#if USE_FREETYPE - #include "xCGUITTFont.h" -#endif - -inline u32 clamp_u8(s32 value) -{ - return (u32) MYMIN(MYMAX(value, 0), 255); -} - - -GUIChatConsole::GUIChatConsole( - gui::IGUIEnvironment* env, - gui::IGUIElement* parent, - s32 id, - ChatBackend* backend, - Client* client, - IMenuManager* menumgr -): - IGUIElement(gui::EGUIET_ELEMENT, env, parent, id, - core::rect(0,0,100,100)), - m_chat_backend(backend), - m_client(client), - m_menumgr(menumgr), - m_screensize(v2u32(0,0)), - m_animate_time_old(0), - m_open(false), - m_close_on_enter(false), - m_height(0), - m_desired_height(0), - m_desired_height_fraction(0.0), - m_height_speed(5.0), - m_open_inhibited(0), - m_cursor_blink(0.0), - m_cursor_blink_speed(0.0), - m_cursor_height(0.0), - m_background(NULL), - m_background_color(255, 0, 0, 0), - m_font(NULL), - m_fontsize(0, 0) -{ - m_animate_time_old = getTimeMs(); - - // load background settings - s32 console_alpha = g_settings->getS32("console_alpha"); - m_background_color.setAlpha(clamp_u8(console_alpha)); - - // load the background texture depending on settings - ITextureSource *tsrc = client->getTextureSource(); - if (tsrc->isKnownSourceImage("background_chat.jpg")) { - m_background = tsrc->getTexture("background_chat.jpg"); - m_background_color.setRed(255); - m_background_color.setGreen(255); - m_background_color.setBlue(255); - } else { - v3f console_color = g_settings->getV3F("console_color"); - m_background_color.setRed(clamp_u8(myround(console_color.X))); - m_background_color.setGreen(clamp_u8(myround(console_color.Y))); - m_background_color.setBlue(clamp_u8(myround(console_color.Z))); - } - - m_font = g_fontengine->getFont(FONT_SIZE_UNSPECIFIED, FM_Mono); - - if (m_font == NULL) - { - errorstream << "GUIChatConsole: Unable to load mono font "; - } - else - { - core::dimension2d dim = m_font->getDimension(L"M"); - m_fontsize = v2u32(dim.Width, dim.Height); - m_font->grab(); - } - m_fontsize.X = MYMAX(m_fontsize.X, 1); - m_fontsize.Y = MYMAX(m_fontsize.Y, 1); - - // set default cursor options - setCursor(true, true, 2.0, 0.1); -} - -GUIChatConsole::~GUIChatConsole() -{ - if (m_font) - m_font->drop(); -} - -void GUIChatConsole::openConsole(f32 height) -{ - m_open = true; - m_desired_height_fraction = height; - m_desired_height = height * m_screensize.Y; - reformatConsole(); - m_animate_time_old = getTimeMs(); - IGUIElement::setVisible(true); - Environment->setFocus(this); - m_menumgr->createdMenu(this); -} - -bool GUIChatConsole::isOpen() const -{ - return m_open; -} - -bool GUIChatConsole::isOpenInhibited() const -{ - return m_open_inhibited > 0; -} - -void GUIChatConsole::closeConsole() -{ - m_open = false; - Environment->removeFocus(this); - m_menumgr->deletingMenu(this); -} - -void GUIChatConsole::closeConsoleAtOnce() -{ - closeConsole(); - m_height = 0; - recalculateConsolePosition(); -} - -f32 GUIChatConsole::getDesiredHeight() const -{ - return m_desired_height_fraction; -} - -void GUIChatConsole::replaceAndAddToHistory(std::wstring line) -{ - ChatPrompt& prompt = m_chat_backend->getPrompt(); - prompt.addToHistory(prompt.getLine()); - prompt.replace(line); -} - - -void GUIChatConsole::setCursor( - bool visible, bool blinking, f32 blink_speed, f32 relative_height) -{ - if (visible) - { - if (blinking) - { - // leave m_cursor_blink unchanged - m_cursor_blink_speed = blink_speed; - } - else - { - m_cursor_blink = 0x8000; // on - m_cursor_blink_speed = 0.0; - } - } - else - { - m_cursor_blink = 0; // off - m_cursor_blink_speed = 0.0; - } - m_cursor_height = relative_height; -} - -void GUIChatConsole::draw() -{ - if(!IsVisible) - return; - - video::IVideoDriver* driver = Environment->getVideoDriver(); - - // Check screen size - v2u32 screensize = driver->getScreenSize(); - if (screensize != m_screensize) - { - // screen size has changed - // scale current console height to new window size - if (m_screensize.Y != 0) - m_height = m_height * screensize.Y / m_screensize.Y; - m_desired_height = m_desired_height_fraction * m_screensize.Y; - m_screensize = screensize; - reformatConsole(); - } - - // Animation - u32 now = getTimeMs(); - animate(now - m_animate_time_old); - m_animate_time_old = now; - - // Draw console elements if visible - if (m_height > 0) - { - drawBackground(); - drawText(); - drawPrompt(); - } - - gui::IGUIElement::draw(); -} - -void GUIChatConsole::reformatConsole() -{ - s32 cols = m_screensize.X / m_fontsize.X - 2; // make room for a margin (looks better) - s32 rows = m_desired_height / m_fontsize.Y - 1; // make room for the input prompt - if (cols <= 0 || rows <= 0) - cols = rows = 0; - m_chat_backend->reformat(cols, rows); -} - -void GUIChatConsole::recalculateConsolePosition() -{ - core::rect rect(0, 0, m_screensize.X, m_height); - DesiredRect = rect; - recalculateAbsolutePosition(false); -} - -void GUIChatConsole::animate(u32 msec) -{ - // animate the console height - s32 goal = m_open ? m_desired_height : 0; - - // Set invisible if close animation finished (reset by openConsole) - // This function (animate()) is never called once its visibility becomes false so do not - // actually set visible to false before the inhibited period is over - if (!m_open && m_height == 0 && m_open_inhibited == 0) - IGUIElement::setVisible(false); - - if (m_height != goal) - { - s32 max_change = msec * m_screensize.Y * (m_height_speed / 1000.0); - if (max_change == 0) - max_change = 1; - - if (m_height < goal) - { - // increase height - if (m_height + max_change < goal) - m_height += max_change; - else - m_height = goal; - } - else - { - // decrease height - if (m_height > goal + max_change) - m_height -= max_change; - else - m_height = goal; - } - - recalculateConsolePosition(); - } - - // blink the cursor - if (m_cursor_blink_speed != 0.0) - { - u32 blink_increase = 0x10000 * msec * (m_cursor_blink_speed / 1000.0); - if (blink_increase == 0) - blink_increase = 1; - m_cursor_blink = ((m_cursor_blink + blink_increase) & 0xffff); - } - - // decrease open inhibit counter - if (m_open_inhibited > msec) - m_open_inhibited -= msec; - else - m_open_inhibited = 0; -} - -void GUIChatConsole::drawBackground() -{ - video::IVideoDriver* driver = Environment->getVideoDriver(); - if (m_background != NULL) - { - core::rect sourcerect(0, -m_height, m_screensize.X, 0); - driver->draw2DImage( - m_background, - v2s32(0, 0), - sourcerect, - &AbsoluteClippingRect, - m_background_color, - false); - } - else - { - driver->draw2DRectangle( - m_background_color, - core::rect(0, 0, m_screensize.X, m_height), - &AbsoluteClippingRect); - } -} - -void GUIChatConsole::drawText() -{ - if (m_font == NULL) - return; - - ChatBuffer& buf = m_chat_backend->getConsoleBuffer(); - for (u32 row = 0; row < buf.getRows(); ++row) - { - const ChatFormattedLine& line = buf.getFormattedLine(row); - if (line.fragments.empty()) - continue; - - s32 line_height = m_fontsize.Y; - s32 y = row * line_height + m_height - m_desired_height; - if (y + line_height < 0) - continue; - - for (u32 i = 0; i < line.fragments.size(); ++i) - { - const ChatFormattedFragment& fragment = line.fragments[i]; - s32 x = (fragment.column + 1) * m_fontsize.X; - core::rect destrect( - x, y, x + m_fontsize.X * fragment.text.size(), y + m_fontsize.Y); - - - #if USE_FREETYPE - // Draw colored text if FreeType is enabled - irr::gui::CGUITTFont *tmp = static_cast(m_font); - tmp->draw( - fragment.text.c_str(), - destrect, - fragment.text.getColors(), - false, - false, - &AbsoluteClippingRect); - #else - // Otherwise use standard text - m_font->draw( - fragment.text.c_str(), - destrect, - video::SColor(255, 255, 255, 255), - false, - false, - &AbsoluteClippingRect); - #endif - } - } -} - -void GUIChatConsole::drawPrompt() -{ - if (m_font == NULL) - return; - - u32 row = m_chat_backend->getConsoleBuffer().getRows(); - s32 line_height = m_fontsize.Y; - s32 y = row * line_height + m_height - m_desired_height; - - ChatPrompt& prompt = m_chat_backend->getPrompt(); - std::wstring prompt_text = prompt.getVisiblePortion(); - - // FIXME Draw string at once, not character by character - // That will only work with the cursor once we have a monospace font - for (u32 i = 0; i < prompt_text.size(); ++i) - { - wchar_t ws[2] = {prompt_text[i], 0}; - s32 x = (1 + i) * m_fontsize.X; - core::rect destrect( - x, y, x + m_fontsize.X, y + m_fontsize.Y); - m_font->draw( - ws, - destrect, - video::SColor(255, 255, 255, 255), - false, - false, - &AbsoluteClippingRect); - } - - // Draw the cursor during on periods - if ((m_cursor_blink & 0x8000) != 0) - { - s32 cursor_pos = prompt.getVisibleCursorPosition(); - if (cursor_pos >= 0) - { - s32 cursor_len = prompt.getCursorLength(); - video::IVideoDriver* driver = Environment->getVideoDriver(); - s32 x = (1 + cursor_pos) * m_fontsize.X; - core::rect destrect( - x, - y + m_fontsize.Y * (1.0 - m_cursor_height), - x + m_fontsize.X * MYMAX(cursor_len, 1), - y + m_fontsize.Y * (cursor_len ? m_cursor_height+1 : 1) - ); - video::SColor cursor_color(255,255,255,255); - driver->draw2DRectangle( - cursor_color, - destrect, - &AbsoluteClippingRect); - } - } - -} - -bool GUIChatConsole::OnEvent(const SEvent& event) -{ - - ChatPrompt &prompt = m_chat_backend->getPrompt(); - - if(event.EventType == EET_KEY_INPUT_EVENT && event.KeyInput.PressedDown) - { - // Key input - if(KeyPress(event.KeyInput) == getKeySetting("keymap_console")) - { - closeConsole(); - - // inhibit open so the_game doesn't reopen immediately - m_open_inhibited = 50; - m_close_on_enter = false; - return true; - } - else if(event.KeyInput.Key == KEY_ESCAPE) - { - closeConsoleAtOnce(); - m_close_on_enter = false; - // inhibit open so the_game doesn't reopen immediately - m_open_inhibited = 1; // so the ESCAPE button doesn't open the "pause menu" - return true; - } - else if(event.KeyInput.Key == KEY_PRIOR) - { - m_chat_backend->scrollPageUp(); - return true; - } - else if(event.KeyInput.Key == KEY_NEXT) - { - m_chat_backend->scrollPageDown(); - return true; - } - else if(event.KeyInput.Key == KEY_RETURN) - { - prompt.addToHistory(prompt.getLine()); - std::wstring text = prompt.replace(L""); - m_client->typeChatMessage(text); - if (m_close_on_enter) { - closeConsoleAtOnce(); - m_close_on_enter = false; - } - return true; - } - else if(event.KeyInput.Key == KEY_UP) - { - // Up pressed - // Move back in history - prompt.historyPrev(); - return true; - } - else if(event.KeyInput.Key == KEY_DOWN) - { - // Down pressed - // Move forward in history - prompt.historyNext(); - return true; - } - else if(event.KeyInput.Key == KEY_LEFT || event.KeyInput.Key == KEY_RIGHT) - { - // Left/right pressed - // Move/select character/word to the left depending on control and shift keys - ChatPrompt::CursorOp op = event.KeyInput.Shift ? - ChatPrompt::CURSOROP_SELECT : - ChatPrompt::CURSOROP_MOVE; - ChatPrompt::CursorOpDir dir = event.KeyInput.Key == KEY_LEFT ? - ChatPrompt::CURSOROP_DIR_LEFT : - ChatPrompt::CURSOROP_DIR_RIGHT; - ChatPrompt::CursorOpScope scope = event.KeyInput.Control ? - ChatPrompt::CURSOROP_SCOPE_WORD : - ChatPrompt::CURSOROP_SCOPE_CHARACTER; - prompt.cursorOperation(op, dir, scope); - return true; - } - else if(event.KeyInput.Key == KEY_HOME) - { - // Home pressed - // move to beginning of line - prompt.cursorOperation( - ChatPrompt::CURSOROP_MOVE, - ChatPrompt::CURSOROP_DIR_LEFT, - ChatPrompt::CURSOROP_SCOPE_LINE); - return true; - } - else if(event.KeyInput.Key == KEY_END) - { - // End pressed - // move to end of line - prompt.cursorOperation( - ChatPrompt::CURSOROP_MOVE, - ChatPrompt::CURSOROP_DIR_RIGHT, - ChatPrompt::CURSOROP_SCOPE_LINE); - return true; - } - else if(event.KeyInput.Key == KEY_BACK) - { - // Backspace or Ctrl-Backspace pressed - // delete character / word to the left - ChatPrompt::CursorOpScope scope = - event.KeyInput.Control ? - ChatPrompt::CURSOROP_SCOPE_WORD : - ChatPrompt::CURSOROP_SCOPE_CHARACTER; - prompt.cursorOperation( - ChatPrompt::CURSOROP_DELETE, - ChatPrompt::CURSOROP_DIR_LEFT, - scope); - return true; - } - else if(event.KeyInput.Key == KEY_DELETE) - { - // Delete or Ctrl-Delete pressed - // delete character / word to the right - ChatPrompt::CursorOpScope scope = - event.KeyInput.Control ? - ChatPrompt::CURSOROP_SCOPE_WORD : - ChatPrompt::CURSOROP_SCOPE_CHARACTER; - prompt.cursorOperation( - ChatPrompt::CURSOROP_DELETE, - ChatPrompt::CURSOROP_DIR_RIGHT, - scope); - return true; - } - else if(event.KeyInput.Key == KEY_KEY_A && event.KeyInput.Control) - { - // Ctrl-A pressed - // Select all text - prompt.cursorOperation( - ChatPrompt::CURSOROP_SELECT, - ChatPrompt::CURSOROP_DIR_LEFT, // Ignored - ChatPrompt::CURSOROP_SCOPE_LINE); - return true; - } - else if(event.KeyInput.Key == KEY_KEY_C && event.KeyInput.Control) - { - // Ctrl-C pressed - // Copy text to clipboard - if (prompt.getCursorLength() <= 0) - return true; - std::wstring wselected = prompt.getSelection(); - std::string selected(wselected.begin(), wselected.end()); - Environment->getOSOperator()->copyToClipboard(selected.c_str()); - return true; - } - else if(event.KeyInput.Key == KEY_KEY_V && event.KeyInput.Control) - { - // Ctrl-V pressed - // paste text from clipboard - if (prompt.getCursorLength() > 0) { - // Delete selected section of text - prompt.cursorOperation( - ChatPrompt::CURSOROP_DELETE, - ChatPrompt::CURSOROP_DIR_LEFT, // Ignored - ChatPrompt::CURSOROP_SCOPE_SELECTION); - } - IOSOperator *os_operator = Environment->getOSOperator(); - const c8 *text = os_operator->getTextFromClipboard(); - if (!text) - return true; - std::basic_string str((const unsigned char*)text); - prompt.input(std::wstring(str.begin(), str.end())); - return true; - } - else if(event.KeyInput.Key == KEY_KEY_X && event.KeyInput.Control) - { - // Ctrl-X pressed - // Cut text to clipboard - if (prompt.getCursorLength() <= 0) - return true; - std::wstring wselected = prompt.getSelection(); - std::string selected(wselected.begin(), wselected.end()); - Environment->getOSOperator()->copyToClipboard(selected.c_str()); - prompt.cursorOperation( - ChatPrompt::CURSOROP_DELETE, - ChatPrompt::CURSOROP_DIR_LEFT, // Ignored - ChatPrompt::CURSOROP_SCOPE_SELECTION); - return true; - } - else if(event.KeyInput.Key == KEY_KEY_U && event.KeyInput.Control) - { - // Ctrl-U pressed - // kill line to left end - prompt.cursorOperation( - ChatPrompt::CURSOROP_DELETE, - ChatPrompt::CURSOROP_DIR_LEFT, - ChatPrompt::CURSOROP_SCOPE_LINE); - return true; - } - else if(event.KeyInput.Key == KEY_KEY_K && event.KeyInput.Control) - { - // Ctrl-K pressed - // kill line to right end - prompt.cursorOperation( - ChatPrompt::CURSOROP_DELETE, - ChatPrompt::CURSOROP_DIR_RIGHT, - ChatPrompt::CURSOROP_SCOPE_LINE); - return true; - } - else if(event.KeyInput.Key == KEY_TAB) - { - // Tab or Shift-Tab pressed - // Nick completion - std::list names = m_client->getConnectedPlayerNames(); - bool backwards = event.KeyInput.Shift; - prompt.nickCompletion(names, backwards); - return true; - } - else if(event.KeyInput.Char != 0 && !event.KeyInput.Control) - { - #if (defined(linux) || defined(__linux)) - wchar_t wc = L'_'; - mbtowc( &wc, (char *) &event.KeyInput.Char, sizeof(event.KeyInput.Char) ); - prompt.input(wc); - #else - prompt.input(event.KeyInput.Char); - #endif - return true; - } - } - else if(event.EventType == EET_MOUSE_INPUT_EVENT) - { - if(event.MouseInput.Event == EMIE_MOUSE_WHEEL) - { - s32 rows = myround(-3.0 * event.MouseInput.Wheel); - m_chat_backend->scroll(rows); - } - } - - return Parent ? Parent->OnEvent(event) : false; -} - -void GUIChatConsole::setVisible(bool visible) -{ - m_open = visible; - IGUIElement::setVisible(visible); - if (!visible) { - m_height = 0; - recalculateConsolePosition(); - } -} - diff --git a/src/client/guiChatConsole.h b/src/client/guiChatConsole.h deleted file mode 100644 index 3013a1d31..000000000 --- a/src/client/guiChatConsole.h +++ /dev/null @@ -1,138 +0,0 @@ -/* -Minetest -Copyright (C) 2013 celeron55, Perttu Ahola - -This program is free software; you can redistribute it and/or modify -it under the terms of the GNU Lesser General Public License as published by -the Free Software Foundation; either version 2.1 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Lesser General Public License for more details. - -You should have received a copy of the GNU Lesser General Public License along -with this program; if not, write to the Free Software Foundation, Inc., -51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -*/ - -#ifndef GUICHATCONSOLE_HEADER -#define GUICHATCONSOLE_HEADER - -#include "irrlichttypes_extrabloated.h" -#include "modalMenu.h" -#include "chat.h" -#include "config.h" - -class Client; - -class GUIChatConsole : public gui::IGUIElement -{ -public: - GUIChatConsole(gui::IGUIEnvironment* env, - gui::IGUIElement* parent, - s32 id, - ChatBackend* backend, - Client* client, - IMenuManager* menumgr); - virtual ~GUIChatConsole(); - - // Open the console (height = desired fraction of screen size) - // This doesn't open immediately but initiates an animation. - // You should call isOpenInhibited() before this. - void openConsole(f32 height); - - bool isOpen() const; - - // Check if the console should not be opened at the moment - // This is to avoid reopening the console immediately after closing - bool isOpenInhibited() const; - // Close the console, equivalent to openConsole(0). - // This doesn't close immediately but initiates an animation. - void closeConsole(); - // Close the console immediately, without animation. - void closeConsoleAtOnce(); - // Set whether to close the console after the user presses enter. - void setCloseOnEnter(bool close) { m_close_on_enter = close; } - - // Return the desired height (fraction of screen size) - // Zero if the console is closed or getting closed - f32 getDesiredHeight() const; - - // Replace actual line when adding the actual to the history (if there is any) - void replaceAndAddToHistory(std::wstring line); - - // Change how the cursor looks - void setCursor( - bool visible, - bool blinking = false, - f32 blink_speed = 1.0, - f32 relative_height = 1.0); - - // Irrlicht draw method - virtual void draw(); - - bool canTakeFocus(gui::IGUIElement* element) { return false; } - - virtual bool OnEvent(const SEvent& event); - - virtual void setVisible(bool visible); - -private: - void reformatConsole(); - void recalculateConsolePosition(); - - // These methods are called by draw - void animate(u32 msec); - void drawBackground(); - void drawText(); - void drawPrompt(); - -private: - ChatBackend* m_chat_backend; - Client* m_client; - IMenuManager* m_menumgr; - - // current screen size - v2u32 m_screensize; - - // used to compute how much time passed since last animate() - u32 m_animate_time_old; - - // should the console be opened or closed? - bool m_open; - // should it close after you press enter? - bool m_close_on_enter; - // current console height [pixels] - s32 m_height; - // desired height [pixels] - f32 m_desired_height; - // desired height [screen height fraction] - f32 m_desired_height_fraction; - // console open/close animation speed [screen height fraction / second] - f32 m_height_speed; - // if nonzero, opening the console is inhibited [milliseconds] - u32 m_open_inhibited; - - // cursor blink frame (16-bit value) - // cursor is off during [0,32767] and on during [32768,65535] - u32 m_cursor_blink; - // cursor blink speed [on/off toggles / second] - f32 m_cursor_blink_speed; - // cursor height [line height] - f32 m_cursor_height; - - // background texture - video::ITexture* m_background; - // background color (including alpha) - video::SColor m_background_color; - - // font - gui::IGUIFont* m_font; - v2u32 m_fontsize; -}; - - -#endif - -- cgit v1.2.3