diff --git a/src/kiwano-physics/Fixture.cpp b/src/kiwano-physics/Fixture.cpp
index fd2d5588..51e20a2b 100644
--- a/src/kiwano-physics/Fixture.cpp
+++ b/src/kiwano-physics/Fixture.cpp
@@ -80,7 +80,7 @@ FixturePtr Fixture::CreateRect(Body* body, Param const& param, Size const& size,
     b2Vec2 b2offset = world->Stage2World(offset);
 
     b2PolygonShape shape;
-    shape.SetAsBox(b2size.x / 2, b2size.y / 2, b2offset, rotation);
+    shape.SetAsBox(b2size.x / 2, b2size.y / 2, b2offset, math::Degree2Radian(rotation));
 
     return CreateFixture(body, &shape, param);
 }
diff --git a/src/kiwano-physics/World.cpp b/src/kiwano-physics/World.cpp
index 0b0f4cbc..0f7d8a2a 100644
--- a/src/kiwano-physics/World.cpp
+++ b/src/kiwano-physics/World.cpp
@@ -242,10 +242,23 @@ ContactList World::GetContactList()
 
 void World::Update(Duration dt)
 {
-    world_.Step(dt.Seconds(), vel_iter_, pos_iter_);
 
     b2Body* b2body = world_.GetBodyList();
     while (b2body)
+    {
+        Body* body = static_cast
(b2body->GetUserData());
+        if (body && body->GetType() != Body::Type::Static)
+        {
+            body->UpdateFromActor();
+        }
+
+        b2body = b2body->GetNext();
+    }
+
+    world_.Step(dt.Seconds(), vel_iter_, pos_iter_);
+
+    b2body = world_.GetBodyList();
+    while (b2body)
     {
         Body* body = static_cast(b2body->GetUserData());
         if (body && body->GetType() != Body::Type::Static)
diff --git a/src/kiwano/2d/Actor.cpp b/src/kiwano/2d/Actor.cpp
index 9a8b257e..963efbb4 100644
--- a/src/kiwano/2d/Actor.cpp
+++ b/src/kiwano/2d/Actor.cpp
@@ -260,13 +260,13 @@ void Actor::HandleEvent(Event* evt)
     }
 }
 
-Matrix3x2 const& Actor::GetTransformMatrix() const
+const Matrix3x2& Actor::GetTransformMatrix() const
 {
     UpdateTransform();
     return transform_matrix_;
 }
 
-Matrix3x2 const& Actor::GetTransformInverseMatrix() const
+const Matrix3x2& Actor::GetTransformInverseMatrix() const
 {
     UpdateTransform();
     if (dirty_transform_inverse_)
diff --git a/src/kiwano/2d/Actor.h b/src/kiwano/2d/Actor.h
index bbcb2df2..9e838f19 100644
--- a/src/kiwano/2d/Actor.h
+++ b/src/kiwano/2d/Actor.h
@@ -114,7 +114,7 @@ public:
 
     /// \~chinese
     /// @brief 获取坐标
-    Point const& GetPosition() const;
+    virtual Point GetPosition() const;
 
     /// \~chinese
     /// @brief 获取 x 坐标
@@ -124,6 +124,10 @@ public:
     /// @brief 获取 y 坐标
     float GetPositionY() const;
 
+    /// \~chinese
+    /// @brief 获取大小
+    virtual Size GetSize() const;
+
     /// \~chinese
     /// @brief 获取宽度
     float GetWidth() const;
@@ -132,10 +136,6 @@ public:
     /// @brief 获取高度
     float GetHeight() const;
 
-    /// \~chinese
-    /// @brief 获取大小
-    Size const& GetSize() const;
-
     /// \~chinese
     /// @brief 获取缩放后的宽度
     float GetScaledWidth() const;
@@ -150,7 +150,7 @@ public:
 
     /// \~chinese
     /// @brief 获取锚点
-    Point const& GetAnchor() const;
+    virtual Point GetAnchor() const;
 
     /// \~chinese
     /// @brief 获取 x 方向锚点
@@ -162,7 +162,7 @@ public:
 
     /// \~chinese
     /// @brief 获取透明度
-    float GetOpacity() const;
+    virtual float GetOpacity() const;
 
     /// \~chinese
     /// @brief 获取显示透明度
@@ -170,11 +170,11 @@ public:
 
     /// \~chinese
     /// @brief 获取旋转角度
-    float GetRotation() const;
+    virtual float GetRotation() const;
 
     /// \~chinese
     /// @brief 获取缩放比例
-    Point const& GetScale() const;
+    virtual Point GetScale() const;
 
     /// \~chinese
     /// @brief 获取横向缩放比例
@@ -186,7 +186,7 @@ public:
 
     /// \~chinese
     /// @brief 获取错切角度
-    Point const& GetSkew() const;
+    virtual Point GetSkew() const;
 
     /// \~chinese
     /// @brief 获取横向错切角度
@@ -218,11 +218,11 @@ public:
 
     /// \~chinese
     /// @brief 获取二维变换矩阵
-    Matrix3x2 const& GetTransformMatrix() const;
+    const Matrix3x2& GetTransformMatrix() const;
 
     /// \~chinese
     /// @brief 获取二维变换的逆矩阵
-    Matrix3x2 const& GetTransformInverseMatrix() const;
+    const Matrix3x2& GetTransformInverseMatrix() const;
 
     /// \~chinese
     /// @brief 设置角色是否可见
@@ -284,14 +284,6 @@ public:
     /// @brief 设置锚点位置,默认为 (0, 0), 范围 [0, 1]
     void SetAnchor(float anchorx, float anchory);
 
-    /// \~chinese
-    /// @brief 修改宽度
-    virtual void SetWidth(float width);
-
-    /// \~chinese
-    /// @brief 修改高度
-    virtual void SetHeight(float height);
-
     /// \~chinese
     /// @brief 修改大小
     virtual void SetSize(Size const& size);
@@ -300,6 +292,14 @@ public:
     /// @brief 修改大小
     void SetSize(float width, float height);
 
+    /// \~chinese
+    /// @brief 修改宽度
+    void SetWidth(float width);
+
+    /// \~chinese
+    /// @brief 修改高度
+    void SetHeight(float height);
+
     /// \~chinese
     /// @brief 设置透明度,默认为 1.0, 范围 [0, 1]
     virtual void SetOpacity(float opacity);
@@ -514,7 +514,7 @@ inline int Actor::GetZOrder() const
     return z_order_;
 }
 
-inline Point const& Actor::GetPosition() const
+inline Point Actor::GetPosition() const
 {
     return transform_.position;
 }
@@ -529,7 +529,7 @@ inline float Actor::GetPositionY() const
     return GetPosition().y;
 }
 
-inline Point const& Actor::GetScale() const
+inline Point Actor::GetScale() const
 {
     return transform_.scale;
 }
@@ -544,7 +544,7 @@ inline float Actor::GetScaleY() const
     return GetScale().y;
 }
 
-inline Point const& Actor::GetSkew() const
+inline Point Actor::GetSkew() const
 {
     return transform_.skew;
 }
@@ -574,7 +574,7 @@ inline float Actor::GetHeight() const
     return GetSize().y;
 }
 
-inline Size const& Actor::GetSize() const
+inline Size Actor::GetSize() const
 {
     return size_;
 }
@@ -594,7 +594,7 @@ inline Size Actor::GetScaledSize() const
     return Size{ GetScaledWidth(), GetScaledHeight() };
 }
 
-inline Point const& Actor::GetAnchor() const
+inline Point Actor::GetAnchor() const
 {
     return anchor_;
 }
@@ -693,4 +693,5 @@ inline void Actor::SetSkew(float skewx, float skewy)
 {
     SetSkew(Vec2{ skewx, skewy });
 }
+
 }  // namespace kiwano
diff --git a/src/kiwano/2d/DebugActor.cpp b/src/kiwano/2d/DebugActor.cpp
index 0f8626b2..d33881c5 100644
--- a/src/kiwano/2d/DebugActor.cpp
+++ b/src/kiwano/2d/DebugActor.cpp
@@ -124,7 +124,7 @@ void DebugActor::OnUpdate(Duration dt)
 
     debug_text_.Reset(ss.str(), debug_text_style_);
 
-    Size layout_size = debug_text_.GetLayoutSize();
+    Size layout_size = debug_text_.GetSize();
     if (layout_size.x > GetWidth() - 20)
     {
         SetWidth(20 + layout_size.x);
diff --git a/src/kiwano/2d/TextActor.cpp b/src/kiwano/2d/TextActor.cpp
index da05ffa2..a46715f8 100644
--- a/src/kiwano/2d/TextActor.cpp
+++ b/src/kiwano/2d/TextActor.cpp
@@ -61,34 +61,10 @@ void TextActor::OnRender(RenderContext& ctx)
     }
 }
 
-void TextActor::OnUpdate(Duration dt)
+Size TextActor::GetSize() const
 {
-    UpdateLayout();
-}
-
-void TextActor::UpdateLayout()
-{
-    if (!layout_)
-        return;
-
-    if (layout_->GetDirtyFlag() == TextLayout::DirtyFlag::Updated)
-    {
-        layout_->SetDirtyFlag(TextLayout::DirtyFlag::Clean);
-
-        if (text_.empty())
-        {
-            SetSize(Size());
-        }
-        else
-        {
-            SetSize(layout_->GetLayoutSize());
-        }
-    }
-}
-
-bool TextActor::CheckVisibility(RenderContext& ctx) const
-{
-    return layout_ && layout_->IsValid() && Actor::CheckVisibility(ctx);
+    const_cast(this)->UpdateDirtyLayout();
+    return Actor::GetSize();
 }
 
 void TextActor::SetFillColor(Color const& color)
@@ -124,7 +100,7 @@ void TextActor::SetTextLayout(TextLayoutPtr layout)
     if (layout_ != layout)
     {
         layout_ = layout;
-        UpdateLayout();
+        ForceUpdateLayout();
     }
 }
 
@@ -294,4 +270,39 @@ void TextActor::SetOutlineStrokeStyle(StrokeStylePtr stroke)
     }
 }
 
+void TextActor::Update(Duration dt)
+{
+    this->UpdateDirtyLayout();
+    Actor::Update(dt);
+}
+
+bool TextActor::CheckVisibility(RenderContext& ctx) const
+{
+    return layout_ && layout_->IsValid() && Actor::CheckVisibility(ctx);
+}
+
+void TextActor::UpdateDirtyLayout()
+{
+    KGE_ASSERT(layout_);
+    if (layout_->UpdateWhenDirty())
+    {
+        ForceUpdateLayout();
+    }
+}
+
+void TextActor::ForceUpdateLayout()
+{
+    KGE_ASSERT(layout_);
+
+    layout_->UpdateWhenDirty();
+    if (text_.empty())
+    {
+        SetSize(Size());
+    }
+    else
+    {
+        SetSize(layout_->GetSize());
+    }
+}
+
 }  // namespace kiwano
diff --git a/src/kiwano/2d/TextActor.h b/src/kiwano/2d/TextActor.h
index 581b2028..0027371e 100644
--- a/src/kiwano/2d/TextActor.h
+++ b/src/kiwano/2d/TextActor.h
@@ -67,8 +67,8 @@ public:
     TextLayoutPtr GetLayout() const;
 
     /// \~chinese
-    /// @brief 获取文本布局大小
-    Size GetLayoutSize() const;
+    /// @brief 获取大小
+    Size GetSize() const override;
 
     /// \~chinese
     /// @brief 获取填充画刷
@@ -159,15 +159,20 @@ public:
     void SetTextLayout(TextLayoutPtr layout);
 
     /// \~chinese
-    /// @brief 更新文字布局
+    /// @brief 更新脏文字布局
+    /// @details 仅当文字布局脏时更新
+    void UpdateDirtyLayout();
+
+    /// \~chinese
+    /// @brief 强制更新文字布局
     /// @details 文字布局是懒更新的,手动更新文字布局以更新节点状态
-    void UpdateLayout();
+    void ForceUpdateLayout();
 
     void OnRender(RenderContext& ctx) override;
 
-    void OnUpdate(Duration dt) override;
-
 protected:
+    void Update(Duration dt) override;
+
     bool CheckVisibility(RenderContext& ctx) const override;
 
 private:
@@ -198,15 +203,6 @@ inline TextLayoutPtr TextActor::GetLayout() const
     return layout_;
 }
 
-inline Size TextActor::GetLayoutSize() const
-{
-    if (layout_)
-    {
-        return layout_->GetLayoutSize();
-    }
-    return Size();
-}
-
 inline BrushPtr TextActor::GetFillBrush() const
 {
     return style_.fill_brush;
diff --git a/src/kiwano/render/DirectX/RendererImpl.cpp b/src/kiwano/render/DirectX/RendererImpl.cpp
index a45746ee..6ba754af 100644
--- a/src/kiwano/render/DirectX/RendererImpl.cpp
+++ b/src/kiwano/render/DirectX/RendererImpl.cpp
@@ -610,7 +610,7 @@ void RendererImpl::CreateTextLayout(TextLayout& layout, const String& content, c
     if (content.empty())
     {
         layout.Clear();
-        layout.SetDirtyFlag(TextLayout::DirtyFlag::Updated);
+        layout.SetDirtyFlag(TextLayout::DirtyFlag::Dirty);
         return;
     }
 
@@ -635,7 +635,7 @@ void RendererImpl::CreateTextLayout(TextLayout& layout, const String& content, c
             if (SUCCEEDED(hr))
             {
                 NativePtr::Set(layout, output);
-                layout.SetDirtyFlag(TextLayout::DirtyFlag::Updated);
+                layout.SetDirtyFlag(TextLayout::DirtyFlag::Dirty);
             }
         }
     }
diff --git a/src/kiwano/render/TextLayout.cpp b/src/kiwano/render/TextLayout.cpp
index 8f66e2e3..62b05a3d 100644
--- a/src/kiwano/render/TextLayout.cpp
+++ b/src/kiwano/render/TextLayout.cpp
@@ -45,14 +45,15 @@ TextLayoutPtr TextLayout::Create(const String& content, const TextStyle& style)
 
 TextLayout::TextLayout()
     : dirty_flag_(DirtyFlag::Clean)
+    , line_count_(0)
 {
 }
 
-void TextLayout::Reset(const String& text, const TextStyle& style)
+void TextLayout::Reset(const String& content, const TextStyle& style)
 {
-    if (!text.empty())
+    if (!content.empty())
     {
-        Renderer::GetInstance().CreateTextLayout(*this, text, style);
+        Renderer::GetInstance().CreateTextLayout(*this, content, style);
 
         SetAlignment(style.alignment);
         SetWrapWidth(style.wrap_width);
@@ -63,80 +64,58 @@ void TextLayout::Reset(const String& text, const TextStyle& style)
         SetDefaultOutlineStrokeStyle(style.outline_stroke);
 
         if (style.show_underline)
-            SetUnderline(style.show_underline, { 0, text.length() });
+            SetUnderline(style.show_underline, { 0, content.length() });
 
         if (style.show_strikethrough)
-            SetStrikethrough(style.show_strikethrough, { 0, text.length() });
+            SetStrikethrough(style.show_strikethrough, { 0, content.length() });
     }
     else
     {
         Clear();
     }
+
+    content_ = content;
+    SetDirtyFlag(DirtyFlag::Dirty);
+}
+
+Size TextLayout::GetSize() const
+{
+    const_cast(this)->UpdateWhenDirty();
+    return size_;
 }
 
 uint32_t TextLayout::GetLineCount() const
 {
-#if KGE_RENDER_ENGINE == KGE_RENDER_ENGINE_DIRECTX
-    auto native = NativePtr::Get(this);
-    KGE_ASSERT(native);
-
-    if (native)
-    {
-        DWRITE_TEXT_METRICS metrics;
-        if (SUCCEEDED(native->GetMetrics(&metrics)))
-        {
-            return metrics.lineCount;
-        }
-    }
-#else
-    // not supported
-#endif
-    return 0;
-}
-
-Size TextLayout::GetLayoutSize() const
-{
-#if KGE_RENDER_ENGINE == KGE_RENDER_ENGINE_DIRECTX
-    auto native = NativePtr::Get(this);
-    KGE_ASSERT(native);
-
-    if (native)
-    {
-        DWRITE_TEXT_METRICS metrics;
-        if (SUCCEEDED(native->GetMetrics(&metrics)))
-        {
-            return (metrics.layoutWidth > 0) ? Size(metrics.layoutWidth, metrics.height)
-                                             : Size(metrics.width, metrics.height);
-        }
-    }
-#else
-    // not supported
-#endif
-    return Size();
+    const_cast(this)->UpdateWhenDirty();
+    return line_count_;
 }
 
 void TextLayout::SetFont(FontPtr font, TextRange range)
 {
+    KGE_ASSERT(content_.size() >= (range.start + range.length));
+
 #if KGE_RENDER_ENGINE == KGE_RENDER_ENGINE_DIRECTX
     auto native = NativePtr::Get(this);
     KGE_ASSERT(native);
 
     if (native)
     {
-        ComPtr collection = NativePtr::Get(font);
+        auto collection = NativePtr::Get(font);
 
         HRESULT hr = native->SetFontCollection(collection.Get(), { range.start, range.length });
         KGE_THROW_IF_FAILED(hr, "IDWriteTextLayout::SetFontCollection failed");
-
-        dirty_flag_ = DirtyFlag::Updated;
     }
 #else
     // not supported
 #endif
+
+    SetDirtyFlag(DirtyFlag::Dirty);
 }
 
 void TextLayout::SetFontFamily(String const& family, TextRange range)
 {
+    KGE_ASSERT(content_.size() >= (range.start + range.length));
+
 #if KGE_RENDER_ENGINE == KGE_RENDER_ENGINE_DIRECTX
     auto native = NativePtr::Get(this);
     KGE_ASSERT(native);
@@ -147,16 +126,18 @@ void TextLayout::SetFontFamily(String const& family, TextRange range)
 
         HRESULT hr = native->SetFontFamilyName(font_family.c_str(), { range.start, range.length });
         KGE_THROW_IF_FAILED(hr, "IDWriteTextLayout::SetFontFamilyName failed");
-
-        dirty_flag_ = DirtyFlag::Updated;
     }
 #else
     // not supported
 #endif
+
+    SetDirtyFlag(DirtyFlag::Dirty);
 }
 
 void TextLayout::SetFontSize(float size, TextRange range)
 {
+    KGE_ASSERT(content_.size() >= (range.start + range.length));
+
 #if KGE_RENDER_ENGINE == KGE_RENDER_ENGINE_DIRECTX
     auto native = NativePtr::Get(this);
     KGE_ASSERT(native);
@@ -165,16 +146,18 @@ void TextLayout::SetFontSize(float size, TextRange range)
     {
         HRESULT hr = native->SetFontSize(size, { range.start, range.length });
         KGE_THROW_IF_FAILED(hr, "IDWriteTextLayout::SetFontSize failed");
-
-        dirty_flag_ = DirtyFlag::Updated;
     }
 #else
     // not supported
 #endif
+
+    SetDirtyFlag(DirtyFlag::Dirty);
 }
 
 void TextLayout::SetFontWeight(uint32_t weight, TextRange range)
 {
+    KGE_ASSERT(content_.size() >= (range.start + range.length));
+
 #if KGE_RENDER_ENGINE == KGE_RENDER_ENGINE_DIRECTX
     auto native = NativePtr::Get(this);
     KGE_ASSERT(native);
@@ -185,16 +168,18 @@ void TextLayout::SetFontWeight(uint32_t weight, TextRange range)
 
         HRESULT hr = native->SetFontWeight(font_weight, { range.start, range.length });
         KGE_THROW_IF_FAILED(hr, "IDWriteTextLayout::SetFontWeight failed");
-
-        dirty_flag_ = DirtyFlag::Updated;
     }
 #else
     // not supported
 #endif
+
+    SetDirtyFlag(DirtyFlag::Dirty);
 }
 
 void TextLayout::SetItalic(bool italic, TextRange range)
 {
+    KGE_ASSERT(content_.size() >= (range.start + range.length));
+
 #if KGE_RENDER_ENGINE == KGE_RENDER_ENGINE_DIRECTX
     auto native = NativePtr::Get(this);
     KGE_ASSERT(native);
@@ -205,16 +190,18 @@ void TextLayout::SetItalic(bool italic, TextRange range)
 
         HRESULT hr = native->SetFontStyle(font_style, { range.start, range.length });
         KGE_THROW_IF_FAILED(hr, "IDWriteTextLayout::SetFontStyle failed");
-
-        dirty_flag_ = DirtyFlag::Updated;
     }
 #else
     // not supported
 #endif
+
+    SetDirtyFlag(DirtyFlag::Dirty);
 }
 
 void TextLayout::SetUnderline(bool enable, TextRange range)
 {
+    KGE_ASSERT(content_.size() >= (range.start + range.length));
+
 #if KGE_RENDER_ENGINE == KGE_RENDER_ENGINE_DIRECTX
     auto native = NativePtr::Get(this);
     KGE_ASSERT(native);
@@ -223,16 +210,18 @@ void TextLayout::SetUnderline(bool enable, TextRange range)
     {
         HRESULT hr = native->SetUnderline(enable, { range.start, range.length });
         KGE_THROW_IF_FAILED(hr, "IDWriteTextLayout::SetUnderline failed");
-
-        dirty_flag_ = DirtyFlag::Updated;
     }
 #else
-    return;  // not supported
+    // not supported
 #endif
+
+    SetDirtyFlag(DirtyFlag::Dirty);
 }
 
 void TextLayout::SetStrikethrough(bool enable, TextRange range)
 {
+    KGE_ASSERT(content_.size() >= (range.start + range.length));
+
 #if KGE_RENDER_ENGINE == KGE_RENDER_ENGINE_DIRECTX
     auto native = NativePtr::Get(this);
     KGE_ASSERT(native);
@@ -241,16 +230,18 @@ void TextLayout::SetStrikethrough(bool enable, TextRange range)
     {
         HRESULT hr = native->SetStrikethrough(enable, { range.start, range.length });
         KGE_THROW_IF_FAILED(hr, "IDWriteTextLayout::SetStrikethrough failed");
-
-        dirty_flag_ = DirtyFlag::Updated;
     }
 #else
-    return;  // not supported
+    // not supported
 #endif
+
+    SetDirtyFlag(DirtyFlag::Dirty);
 }
 
 void TextLayout::SetFillBrush(BrushPtr brush, TextRange range)
 {
+    KGE_ASSERT(content_.size() >= (range.start + range.length));
+
 #if KGE_RENDER_ENGINE == KGE_RENDER_ENGINE_DIRECTX
     auto native = NativePtr::Get(this);
     KGE_ASSERT(native);
@@ -260,16 +251,18 @@ void TextLayout::SetFillBrush(BrushPtr brush, TextRange range)
         HRESULT hr =
             native->SetDrawingEffect(NativePtr::Get(brush).Get(), { range.start, range.length });
         KGE_THROW_IF_FAILED(hr, "IDWriteTextLayout::SetDrawingEffect failed");
-
-        dirty_flag_ = DirtyFlag::Updated;
     }
 #else
-    return;  // not supported
+    // not supported
 #endif
+
+    SetDirtyFlag(DirtyFlag::Dirty);
 }
 
 void TextLayout::SetOutlineBrush(BrushPtr brush, TextRange range)
 {
+    KGE_ASSERT(content_.size() >= (range.start + range.length));
+
 #if KGE_RENDER_ENGINE == KGE_RENDER_ENGINE_DIRECTX
     // TODO
     KGE_NOT_USED(range);
@@ -281,6 +274,8 @@ void TextLayout::SetOutlineBrush(BrushPtr brush, TextRange range)
 
 void TextLayout::SetOutlineStrokeStyle(StrokeStylePtr stroke, TextRange range)
 {
+    KGE_ASSERT(content_.size() >= (range.start + range.length));
+
 #if KGE_RENDER_ENGINE == KGE_RENDER_ENGINE_DIRECTX
     // TODO
     KGE_NOT_USED(range);
@@ -317,12 +312,12 @@ void TextLayout::SetAlignment(TextAlign align)
 
         HRESULT hr = native->SetTextAlignment(alignment);
         KGE_THROW_IF_FAILED(hr, "IDWriteTextLayout::SetTextAlignment failed");
-
-        dirty_flag_ = DirtyFlag::Updated;
     }
 #else
     // not supported
 #endif
+
+    SetDirtyFlag(DirtyFlag::Dirty);
 }
 
 void TextLayout::SetWrapWidth(float wrap_width)
@@ -333,34 +328,26 @@ void TextLayout::SetWrapWidth(float wrap_width)
 
     if (native)
     {
-        DWRITE_WORD_WRAPPING wrapping = (wrap_width > 0) ? DWRITE_WORD_WRAPPING_WRAP : DWRITE_WORD_WRAPPING_NO_WRAP;
-
-        HRESULT hr = native->SetWordWrapping(wrapping);
-        if (SUCCEEDED(hr))
+        HRESULT hr = S_OK;
+        if (wrap_width > 0)
         {
-            if (wrap_width > 0)
+            hr = native->SetWordWrapping(DWRITE_WORD_WRAPPING_WRAP);
+            if (SUCCEEDED(hr))
             {
                 hr = native->SetMaxWidth(wrap_width);
             }
-            else
-            {
-                // Fix the layout width when the text does not wrap
-                DWRITE_TEXT_METRICS metrics;
-                hr = native->GetMetrics(&metrics);
-
-                if (SUCCEEDED(hr))
-                {
-                    hr = native->SetMaxWidth(metrics.width);
-                }
-            }
+        }
+        else
+        {
+            hr = native->SetWordWrapping(DWRITE_WORD_WRAPPING_NO_WRAP);
         }
         KGE_THROW_IF_FAILED(hr, "IDWriteTextLayout::SetWordWrapping failed");
-
-        dirty_flag_ = DirtyFlag::Updated;
     }
 #else
-    return;  // not supported
+    // not supported
 #endif
+
+    SetDirtyFlag(DirtyFlag::Dirty);
 }
 
 void TextLayout::SetLineSpacing(float line_spacing)
@@ -383,12 +370,62 @@ void TextLayout::SetLineSpacing(float line_spacing)
             hr = native->SetLineSpacing(DWRITE_LINE_SPACING_METHOD_UNIFORM, spacing, spacing * 0.8f);
         }
         KGE_THROW_IF_FAILED(hr, "IDWriteTextLayout::SetLineSpacing failed");
-
-        dirty_flag_ = DirtyFlag::Updated;
     }
 #else
-    return;  // not supported
+    // not supported
 #endif
+
+    SetDirtyFlag(DirtyFlag::Dirty);
+}
+
+bool TextLayout::UpdateWhenDirty()
+{
+#if KGE_RENDER_ENGINE == KGE_RENDER_ENGINE_DIRECTX
+    if (dirty_flag_ == DirtyFlag::Dirty)
+    {
+        SetDirtyFlag(DirtyFlag::Clean);
+
+        auto native = NativePtr::Get(this);
+        KGE_ASSERT(native);
+
+        HRESULT hr = S_OK;
+
+        DWRITE_TEXT_METRICS metrics;
+        hr = native->GetMetrics(&metrics);
+        if (SUCCEEDED(hr))
+        {
+            if (native->GetWordWrapping() == DWRITE_WORD_WRAPPING_NO_WRAP)
+            {
+                // Fix the layout width when the text does not wrap
+                hr = native->SetMaxWidth(metrics.widthIncludingTrailingWhitespace);
+                if (SUCCEEDED(hr))
+                {
+                    hr = native->GetMetrics(&metrics);
+                }
+            }
+        }
+
+        if (SUCCEEDED(hr))
+        {
+            line_count_ = metrics.lineCount;
+
+            if (metrics.layoutWidth > 0)
+            {
+                size_ = Size(metrics.layoutWidth, metrics.height);
+            }
+            else
+            {
+                size_ = Size(metrics.widthIncludingTrailingWhitespace, metrics.height);
+            }
+        }
+
+        KGE_THROW_IF_FAILED(hr, "IDWriteTextLayout::GetMetrics failed");
+        return true;
+    }
+#else
+    // not supported
+#endif
+    return false;
 }
 
 }  // namespace kiwano
diff --git a/src/kiwano/render/TextLayout.h b/src/kiwano/render/TextLayout.h
index 14abc5d8..21a77ec6 100644
--- a/src/kiwano/render/TextLayout.h
+++ b/src/kiwano/render/TextLayout.h
@@ -67,12 +67,12 @@ public:
     void Reset(const String& content, const TextStyle& style);
 
     /// \~chinese
-    /// @brief 获取文本行数
-    uint32_t GetLineCount() const;
+    /// @brief 获取文本布局大小
+    Size GetSize() const;
 
     /// \~chinese
-    /// @brief 获取文本布局大小
-    Size GetLayoutSize() const;
+    /// @brief 获取文本行数
+    uint32_t GetLineCount() const;
 
     /// \~chinese
     /// @brief 获取默认填充画刷
@@ -183,23 +183,34 @@ public:
     void SetDefaultOutlineStrokeStyle(StrokeStylePtr stroke);
 
     /// \~chinese
-    /// @brief 脏数据标志
+    /// @brief 脏布局标志
     enum class DirtyFlag : uint8_t
     {
-        Clean   = 0,       ///< 干净布局
-        Dirty   = 1 << 0,  ///< 脏布局
-        Updated = 1 << 1,  ///< 已更新
+        Clean = 0,      ///< 干净布局
+        Dirty = 1 << 0  ///< 脏布局
     };
 
+    /// \~chinese
+    /// @brief 获取脏布局标志
     DirtyFlag GetDirtyFlag() const;
 
+    /// \~chinese
+    /// @brief 设置脏布局标志
     void SetDirtyFlag(DirtyFlag flag);
 
+    /// \~chinese
+    /// @brief 更新脏布局
+    /// @return 是否需要更新
+    bool UpdateWhenDirty();
+
 private:
     DirtyFlag      dirty_flag_;
+    uint32_t       line_count_;
+    Size           size_;
     BrushPtr       default_fill_brush_;
     BrushPtr       default_outline_brush_;
     StrokeStylePtr default_outline_stroke_;
+    String         content_;
 };
 
 /** @} */