Files
dbgui/src/frontend/window.cpp

1856 lines
47 KiB
C++

#include "window.h"
#include "frontend.h"
#include <algorithm>
#include "imgui_internal.h"
#include <fstream>
#include <filesystem>
using namespace dbgui;
using namespace dbgui::frontend;
bool Window::draw(Frontend &frontend)
{
switch (this->type)
{
using enum WindowType;
case regs: return std::get<RegWindow>(this->data).draw(frontend); break;
case threads:
return std::get<ThreadWindow>(this->data).draw(frontend);
break;
case frames: return std::get<FrameWindow>(this->data).draw(frontend); break;
case disassembly:
return std::get<DisasmWindow>(this->data).draw(frontend);
break;
case breakpoints:
return std::get<BreakpointWindow>(this->data).draw(frontend);
break;
case source:
return std::get<SourceWindow>(this->data).draw(frontend);
break;
case watch: return std::get<WatchWindow>(this->data).draw(frontend);
default: printf("Unhandled window draw: %u\n", this->type); exit(1);
}
}
Window Window::create_regs(size_t id)
{
auto id_str = std::string{"Registers##"};
id_str.append(std::to_string(id));
return Window{.type = WindowType::regs,
.data = RegWindow{.id = id_str, .open = true}};
}
Window Window::create_threads(size_t id)
{
auto id_str = std::string{"Threads##"};
id_str.append(std::to_string(id));
return Window{.type = WindowType::threads,
.data = ThreadWindow{.id = id_str, .open = true}};
}
Window Window::create_frames(size_t id)
{
auto id_str = std::string{"Frames##"};
id_str.append(std::to_string(id));
return Window{.type = WindowType::frames,
.data = FrameWindow{.id = id_str, .open = true}};
}
Window Window::create_disas(size_t id)
{
auto id_str = std::string{"Disassembly##"};
id_str.append(std::to_string(id));
return Window{.type = WindowType::disassembly,
.data =
DisasmWindow{.id = id_str, .open = true, .first = true}};
}
Window Window::create_bp(size_t id)
{
auto id_str = std::string{"Breakpoints##"};
id_str.append(std::to_string(id));
return Window{.type = WindowType::breakpoints,
.data = BreakpointWindow{.id = id_str, .open = true}};
}
Window Window::create_source(size_t id)
{
auto id_str = std::string{"Source##"};
id_str.append(std::to_string(id));
return Window{.type = WindowType::source,
.data =
SourceWindow{.id = id_str, .open = true, .first = true}};
}
Window Window::create_watch(size_t id)
{
auto id_str = std::string{"Watch##"};
id_str.append(std::to_string(id));
return Window{.type = WindowType::watch,
.data = WatchWindow{.id = id_str, .open = true, .first = true}};
}
bool RegWindow::draw(const Frontend &frontend)
{
//ImGui::SetNextWindowDockID(frontend.dock_id, ImGuiCond_Appearing);
if (!ImGui::Begin(this->id.c_str()))
{
ImGui::End();
return false;
}
if (!frontend.target)
{
ImGui::End();
return false;
}
if (frontend.target->state == TargetState::running)
{
ImGui::PushStyleColor(ImGuiCol_Text,
ImGui::GetStyleColorVec4(ImGuiCol_TextDisabled));
}
if (ImGui::BeginTable("table", 1,
ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg))
{
const auto &sets = frontend.target->reg_sets;
for (size_t set_idx = 0; set_idx < sets.size(); ++set_idx)
{
const auto &set = sets[set_idx];
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
ImGui::Text(set.name.c_str());
char buf[20];
std::snprintf(buf, sizeof(buf), "nested_%lu", set_idx);
if (ImGui::BeginTable(buf, 2,
ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg))
{
for (size_t i = 0; i < set.regs.size(); ++i)
{
const auto &reg = set.regs[i];
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
ImGui::Text(reg.name.c_str());
ImGui::TableNextColumn();
if (reg.bytes.size() == 0)
{
ImGui::Text("<unk>");
continue;
}
// TODO: formatting options
switch (reg.bytes.size())
{
case 1: std::snprintf(buf, sizeof(buf), "%X", reg.bytes[0]); break;
case 2:
std::snprintf(
buf, sizeof(buf), "%X",
*reinterpret_cast<const uint16_t *>(reg.bytes.data()));
break;
case 4:
std::snprintf(
buf, sizeof(buf), "%X",
*reinterpret_cast<const uint32_t *>(reg.bytes.data()));
break;
case 8:
std::snprintf(
buf, sizeof(buf), "%lX",
*reinterpret_cast<const uint64_t *>(reg.bytes.data()));
break;
default: std::snprintf(buf, sizeof(buf), "<val too large>"); break;
}
ImGui::PushID(set_idx * 1000 + i);
ImGui::Text(buf);
if (ImGui::IsItemHovered()
&& ImGui::IsMouseClicked(ImGuiMouseButton_Right))
{
ImGui::OpenPopup("Context");
}
if (ImGui::BeginPopup("Context"))
{
if (ImGui::Selectable("Copy"))
{
ImGui::SetClipboardText(buf);
}
ImGui::EndPopup();
}
ImGui::PopID();
}
ImGui::EndTable();
}
}
ImGui::EndTable();
}
if (frontend.target->state == TargetState::running)
{
ImGui::PopStyleColor();
}
ImGui::End();
return false;
}
bool ThreadWindow::draw(const Frontend &frontend)
{
//ImGui::SetNextWindowDockID(frontend.dock_id, ImGuiCond_Appearing);
if (!ImGui::Begin(this->id.c_str()))
{
ImGui::End();
return false;
}
if (!frontend.target)
{
ImGui::End();
return false;
}
if (frontend.target->state == TargetState::running)
{
ImGui::PushStyleColor(ImGuiCol_Text,
ImGui::GetStyleColorVec4(ImGuiCol_TextDisabled));
}
if (ImGui::BeginTable("table", 3,
ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg
| ImGuiTableFlags_ScrollX))
{
ImGui::TableSetupColumn("ID");
ImGui::TableSetupColumn("Name");
ImGui::TableSetupColumn("Location");
ImGui::TableHeadersRow();
const auto &threads = frontend.target->threads;
char buf[32];
for (size_t idx = 0; idx < threads.size(); ++idx)
{
const auto &thread = threads[idx];
if (!thread)
{
continue;
}
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
if (frontend.target->selected_thread == idx)
{
// TODO: style coloring
const auto col =
ImGui::GetColorU32(ImGui::GetStyleColorVec4(ImGuiCol_Button));
ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0, col);
}
std::snprintf(buf, sizeof(buf), "%lu", thread->id);
ImGui::Text(buf);
ImGui::TableNextColumn();
ImGui::Text(thread->name.c_str());
ImGui::TableNextColumn();
if (thread->cur_display_fn != "")
{
ImGui::Text(thread->cur_display_fn.c_str());
} else
{
std::snprintf(buf, sizeof(buf), "%lX", thread->ip);
ImGui::Text(buf);
}
auto min_pos =
ImGui::TableGetCellBgRect(ImGui::GetCurrentTable(), 0).GetTL();
auto max_pos =
ImGui::TableGetCellBgRect(ImGui::GetCurrentTable(), 2).GetBR();
/*if (idx & 1)
{
ImGui::GetWindowDrawList()->AddRect(min_pos, max_pos,
IM_COL32(255, 0, 0, 255));
} else
{
ImGui::GetWindowDrawList()->AddRect(min_pos, max_pos,
IM_COL32(0, 0, 255, 255));
}*/
// TODO: better scrollbar handling
auto win_pos = ImGui::GetWindowPos();
auto min = ImGui::GetWindowContentRegionMin();
auto max = ImGui::GetWindowContentRegionMax();
min.x += win_pos.x;
min.y += win_pos.y;
max.x += win_pos.x;
max.y += win_pos.y;
auto win = ImGui::GetCurrentWindow();
if (win->ScrollbarY)
{
max.x -= win->ScrollbarSizes.x;
}
ImGui::PushClipRect(min, max, false);
if (frontend.target->selected_thread != idx
&& ImGui::IsMouseClicked(ImGuiMouseButton_Left)
&& ImGui::IsMouseHoveringRect(min_pos, max_pos, true))
{
frontend.target->backend->select_thread(idx);
}
ImGui::PopClipRect();
}
ImGui::EndTable();
}
if (frontend.target->state == TargetState::running)
{
ImGui::PopStyleColor();
}
ImGui::End();
return false;
}
bool FrameWindow::draw(const Frontend &frontend)
{
//ImGui::SetNextWindowDockID(frontend.dock_id, ImGuiCond_Appearing);
if (!ImGui::Begin(this->id.c_str()))
{
ImGui::End();
return false;
}
if (!frontend.target)
{
ImGui::End();
return false;
}
if (frontend.target->state == TargetState::running)
{
ImGui::PushStyleColor(ImGuiCol_Text,
ImGui::GetStyleColorVec4(ImGuiCol_TextDisabled));
}
if (ImGui::BeginTable("table", 1,
ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg
| ImGuiTableFlags_ScrollX))
{
ImGui::TableSetupColumn("Location");
ImGui::TableHeadersRow();
const auto &frames = frontend.target->frames;
char buf[32];
for (size_t idx = 0; idx < frames.size(); ++idx)
{
const auto &frame = frames[idx];
if (!frame)
{
continue;
}
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
if (frontend.target->selected_frame == idx)
{
// TODO: style coloring
const auto col =
ImGui::GetColorU32(ImGui::GetStyleColorVec4(ImGuiCol_Button));
ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0, col);
}
if (frame->display_name != "")
{
ImGui::Text(frame->display_name.c_str());
} else
{
std::snprintf(buf, sizeof(buf), "%lX", frame->ip);
ImGui::Text(buf);
}
auto min_pos =
ImGui::TableGetCellBgRect(ImGui::GetCurrentTable(), 0).GetTL();
auto max_pos =
ImGui::TableGetCellBgRect(ImGui::GetCurrentTable(), 0).GetBR();
max_pos.y += ImGui::GetTextLineHeight();
#if 0
ImGui::GetWindowDrawList()->AddRect(min_pos, max_pos,
IM_COL32(255, 0, 0, 255));
#endif
if (frontend.target->selected_frame != idx
&& ImGui::IsMouseClicked(ImGuiMouseButton_Left)
&& ImGui::IsMouseHoveringRect(min_pos, max_pos, true))
{
printf("Select frame %lu\n", idx);
frontend.target->backend->select_frame(idx);
}
}
ImGui::EndTable();
}
if (frontend.target->state == TargetState::running)
{
ImGui::PopStyleColor();
}
ImGui::End();
return false;
}
bool DisasmWindow::draw(Frontend &frontend)
{
if (!ImGui::Begin(this->id.c_str()))
{
ImGui::End();
return false;
}
if (!frontend.target)
{
this->insts.clear();
this->first = true;
this->ip_unsuccessful = true;
this->disas_unsuccessful = true;
ImGui::End();
return false;
}
if (frontend.target->state == TargetState::stopped
|| frontend.target->state == TargetState::startup)
{
this->ip_unsuccessful = true;
this->disas_unsuccessful = true;
ImGui::End();
return false;
}
if (frontend.target->state == TargetState::running)
{
ImGui::PushStyleColor(ImGuiCol_Text,
ImGui::GetStyleColorVec4(ImGuiCol_TextDisabled));
}
if (first)
{
/*auto found = false;
uint16_t set_idx, reg_idx;
for (size_t i = 0; i < frontend.target->reg_sets.size(); ++i)
{
const auto &set = frontend.target->reg_sets[i];
for (size_t j = 0; j < set.regs.size(); ++j)
{
if (frontend.target->arch == Arch::x86_64 && set.regs[j].name == "rip")
{
found = true;
set_idx = i;
reg_idx = j;
break;
}
}
if (found)
{
break;
}
}
if (!found)
{
ImGui::End();
this->ip_unsuccessful = true;
this->disas_unsuccessful = true;
return false;
}*/
this->ip_src_id = frontend.target->data_node_id++;
this->disas_src_id = frontend.target->data_node_id++;
using namespace data::source;
frontend.target->backend->add_data_node(
Node{.id = this->ip_src_id,
.type = Node::Type::source,
.data = Source{
.type = Source::Type::frame_ip,
}});
frontend.target->backend->add_data_node(
Node{.id = this->disas_src_id,
.type = Node::Type::disassemble,
.data = Disassemble{.src_id = this->ip_src_id}});
first = false;
}
auto pad = ImGui::GetStyle().CellPadding;
pad.x += 10.f;
if (ImGui::BeginTable("table", 3,
ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_ScrollY
| ImGuiTableFlags_RowBg | ImGuiTableFlags_PadOuterX))
{
ImGui::TableSetupScrollFreeze(0, 1);
ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, 10.f);
ImGui::TableSetupColumn("Address");
ImGui::TableSetupColumn("Instruction");
ImGui::TableHeadersRow();
for (size_t idx = 0; idx < insts.size(); ++idx)
{
const auto &inst = insts[idx];
//printf("Inst at %lx with '%s'\n", inst.addr, inst.fmt_str.c_str());
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
ImGui::PushID(idx);
// TODO: draw nice break sign
auto &bps = frontend.target->breakpoints;
const auto bp_it =
std::find_if(bps.begin(), bps.end(),
[inst](const auto &el) { return el.at_addr(inst.addr); });
const auto is_bp = bp_it != bps.end() && !bp_it->removed;
if (is_bp)
{
auto win_pos = ImGui::GetWindowPos();
auto pos = ImGui::GetCursorPos();
pos.x += win_pos.x;
pos.y += win_pos.y - ImGui::GetScrollY();
pos.y += (ImGui::GetTextLineHeight() - 10.f) / 2.f;
auto end_pos = pos;
end_pos.x += 10.f;
end_pos.y += 10.f;
pos.x += 5.f;
pos.y += 5.f;
ImGui::GetWindowDrawList()->AddCircleFilled(pos, 5.f,
IM_COL32(255, 0, 0, 255));
}
// TODO: write custom code to catch mouse clicks in the whole cell, including padding
// to make targeting easier
if (ImGui::InvisibleButton("Break",
ImVec2{10.f, ImGui::GetTextLineHeight()}))
{
if (!is_bp)
{
size_t idx = 0;
for (; idx < bps.size(); ++idx)
{
if (bps[idx].removed)
{
break;
}
}
if (idx == bps.size())
{
bps.push_back(
Target::Breakpoint{.removed = false, .data = inst.addr});
} else
{
bps[idx] = Target::Breakpoint{.removed = false, .data = inst.addr};
}
frontend.target->backend->add_breakpoint(inst.addr, idx);
} else
{
frontend.target->backend->remove_breakpoint(bp_it - bps.begin());
bp_it->removed = true;
}
}
ImGui::PopID();
ImGui::TableNextColumn();
if (!this->ip_unsuccessful && inst.addr == ip)
{
// TODO: color config
const auto col =
ImGui::GetColorU32(ImGui::GetStyleColorVec4(ImGuiCol_Button));
ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0, col);
if (this->ip_changed)
{
ImGui::SetScrollHereY();
}
}
// TODO: make some unified handler for int vars
ImGui::PushID(idx);
ImGui::Text("%lX ", inst.addr);
if (ImGui::IsItemHovered()
&& ImGui::IsMouseClicked(ImGuiMouseButton_Right))
{
ImGui::OpenPopup("Context");
}
if (ImGui::BeginPopup("Context"))
{
if (ImGui::Selectable("Copy"))
{
char buf[32];
std::snprintf(buf, sizeof(buf), "%lX", inst.addr);
ImGui::SetClipboardText(buf);
}
ImGui::EndPopup();
}
ImGui::PopID();
ImGui::TableNextColumn();
const int mnem_space = max_mnem_len - inst.mnem_len + 1;
const int op_space = /*max_op_len - inst.op_len +*/ 1;
ImGui::Text(inst.fmt_str.c_str(), mnem_space, ' ', op_space, ' ');
}
ImGui::EndTable();
}
if (frontend.target->state == TargetState::running)
{
ImGui::PopStyleColor();
}
ImGui::End();
ip_changed = false;
return false;
}
bool BreakpointWindow::draw(Frontend &frontend)
{
if (!ImGui::Begin(this->id.c_str()))
{
ImGui::End();
return false;
}
if (!frontend.target)
{
ImGui::End();
return false;
}
if (ImGui::BeginTable("table", 2,
ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg
| ImGuiTableFlags_SizingFixedFit))
{
ImGui::TableSetupColumn("ID");
ImGui::TableSetupColumn("Location");
ImGui::TableHeadersRow();
auto &bps = frontend.target->breakpoints;
for (size_t idx = 0; idx < bps.size(); ++idx)
{
auto &bp = bps[idx];
if (bp.removed)
{
continue;
}
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
ImGui::PushID(idx);
ImGui::Text("%zu", idx);
ImGui::TableNextColumn();
if (auto addr = std::get_if<uint64_t>(&bp.data))
{
ImGui::Text("%lX", *addr);
} else
{
const auto &loc = std::get<Target::Breakpoint::FileLoc>(bp.data);
ImGui::Text("%s:%u", loc.name.c_str(), loc.line);
}
auto min_pos =
ImGui::TableGetCellBgRect(ImGui::GetCurrentTable(), 0).GetTL();
auto max_pos =
ImGui::TableGetCellBgRect(ImGui::GetCurrentTable(), 1).GetBR();
if (ImGui::IsMouseClicked(ImGuiMouseButton_Right)
&& ImGui::IsMouseHoveringRect(min_pos, max_pos, false))
{
ImGui::OpenPopup("Context", ImGuiPopupFlags_NoOpenOverExistingPopup);
}
if (ImGui::BeginPopup("Context"))
{
if (ImGui::Selectable("Remove"))
{
printf("Removing bp %zu\n", idx);
frontend.target->backend->remove_breakpoint(idx);
bp.removed = true;
}
// TODO: disable/endable?
ImGui::EndPopup();
}
ImGui::PopID();
}
ImGui::EndTable();
}
ImGui::End();
return false;
}
bool SourceWindow::draw(Frontend &frontend)
{
if (!ImGui::Begin(this->id.c_str()))
{
ImGui::End();
return false;
}
if (!frontend.target)
{
this->first = true;
this->file_name = "";
this->lines.clear();
this->file_data.clear();
ImGui::End();
return false;
}
if (frontend.target->state == TargetState::stopped
|| frontend.target->state == TargetState::startup)
{
this->file_name = "";
this->lines.clear();
this->file_data.clear();
ImGui::End();
return false;
}
// TODO: we need to clean up the initializations/sync of the DAG somehow
if (first)
{
/*auto found = false;
uint16_t set_idx, reg_idx;
for (size_t i = 0; i < frontend.target->reg_sets.size(); ++i)
{
const auto &set = frontend.target->reg_sets[i];
for (size_t j = 0; j < set.regs.size(); ++j)
{
if (frontend.target->arch == Arch::x86_64 && set.regs[j].name == "rip")
{
found = true;
set_idx = i;
reg_idx = j;
break;
}
}
if (found)
{
break;
}
}
if (!found)
{
ImGui::End();
this->file_name = "";
this->lines.clear();
this->file_data.clear();
return false;
}*/
this->ip_src_id = frontend.target->data_node_id++;
this->line_entry_src_id = frontend.target->data_node_id++;
using namespace data::source;
frontend.target->backend->add_data_node(
Node{.id = this->ip_src_id,
.type = Node::Type::source,
.data = Source{
.type = Source::Type::frame_ip,
}});
frontend.target->backend->add_data_node(
Node{.id = this->line_entry_src_id,
.type = Node::Type::line_entry,
.data = LineEntry{.src_id = this->ip_src_id}});
first = false;
}
if (!this->file_name.empty() && this->file_data.empty())
{
// parse file
auto file =
std::ifstream{this->file_name, std::ios::binary | std::ios::ate};
if (file.is_open())
{
const auto size = file.tellg();
if (size != 0)
{
this->file_data.resize(size);
file.seekg(std::ios::beg);
file.read(this->file_data.data(), this->file_data.size());
auto view =
std::string_view{this->file_data.data(), this->file_data.size()};
assert(this->lines.empty());
while (true)
{
// TODO: better line handling?
const auto pos = view.find_first_of('\n');
if (pos == std::string_view::npos)
{
this->lines.push_back(view);
break;
}
this->lines.push_back(view.substr(0, pos));
view.remove_prefix(pos + 1);
}
} else
{
printf("File is empty: %s\n", this->file_name.c_str());
// prevent spamming the open call
this->file_data.resize(1);
}
} else
{
printf("Couldn't open file: %s\n", this->file_name.c_str());
// prevent spamming the open call
this->file_data.resize(1);
}
}
if (this->file_name.empty())
{
//auto txt_pos = ImGui::GetWindowPos();
auto txt_pos = ImGui::GetWindowContentRegionMin();
auto max = ImGui::GetWindowContentRegionMax();
txt_pos.x += (max.x - txt_pos.x) * 0.5f;
txt_pos.y += (max.y - txt_pos.y) * 0.5f;
const auto txt = "No source available";
const auto txt_size = ImGui::CalcTextSize(txt);
txt_pos.x -= txt_size.x * 0.5f;
txt_pos.y -= txt_size.y * 0.5f;
ImGui::SetCursorPos(txt_pos);
ImGui::TextDisabled(txt);
}
auto pad = ImGui::GetStyle().CellPadding;
pad.x += 10.f;
// TODO: use ImGuiListClipper to work with larger files
if (ImGui::BeginTable("table", 3,
ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_ScrollY
| ImGuiTableFlags_PadOuterX
| ImGuiTableFlags_ScrollX))
{
ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, 10.f);
ImGui::TableSetupColumn("LineNo");
ImGui::TableSetupColumn("LineContent");
//ImGui::TableHeadersRow();
for (size_t idx = 0; idx < this->lines.size(); ++idx)
{
//printf("Inst at %lx with '%s'\n", inst.addr, inst.fmt_str.c_str());
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
ImGui::PushID(idx);
auto &bps = frontend.target->breakpoints;
const auto bp_it =
std::find_if(bps.begin(), bps.end(), [this, idx](const auto &el) {
return el.at_file_loc(this->file_name, idx + 1);
});
const auto is_bp = bp_it != bps.end() && !bp_it->removed;
if (is_bp)
{
auto win_pos = ImGui::GetWindowPos();
auto pos = ImGui::GetCursorPos();
pos.x += win_pos.x;
pos.y += win_pos.y - ImGui::GetScrollY();
pos.y += (ImGui::GetTextLineHeight() - 10.f) / 2.f;
auto end_pos = pos;
end_pos.x += 10.f;
end_pos.y += 10.f;
pos.x += 5.f;
pos.y += 5.f;
ImGui::GetWindowDrawList()->AddCircleFilled(pos, 5.f,
IM_COL32(255, 0, 0, 255));
}
if (this->line == idx + 1)
{
auto win_pos = ImGui::GetWindowPos();
auto pos = ImGui::GetCursorPos();
pos.x += win_pos.x;
pos.y += win_pos.y - ImGui::GetScrollY();
pos.y += (ImGui::GetTextLineHeight() - 10.f) / 2.f;
auto p2 = pos;
auto p3 = pos;
p2.x += 10.f;
p2.y += 5.f;
p3.y += 10.f;
ImGui::GetWindowDrawList()->AddTriangleFilled(
pos, p2, p3, IM_COL32(227, 197, 103, 255));
ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0,
ImGui::GetColorU32(ImVec4(1.f, 1.f, 1.f, 0.2f)));
}
// TODO: write custom code to catch mouse clicks in the whole cell, including padding
// to make targeting easier
if (ImGui::InvisibleButton("Break",
ImVec2{10.f, ImGui::GetTextLineHeight()}))
{
if (!is_bp)
{
size_t bp_idx = 0;
for (; bp_idx < bps.size(); ++bp_idx)
{
if (bps[bp_idx].removed)
{
break;
}
}
if (frontend.target->backend->add_breakpoint(this->file_name.c_str(),
idx + 1, bp_idx))
{
auto bp =
Target::Breakpoint{.removed = false,
.data = Target::Breakpoint::FileLoc{
.name = this->file_name,
.line = static_cast<uint32_t>(idx + 1)}};
if (bp_idx == bps.size())
{
bps.push_back(bp);
} else
{
bps[bp_idx] = bp;
}
}
} else
{
frontend.target->backend->remove_breakpoint(bp_it - bps.begin());
bp_it->removed = true;
}
}
ImGui::PopID();
ImGui::TableNextColumn();
if (this->line == idx + 1)
{
// TODO: color config
//const auto col =
// ImGui::GetColorU32(ImGui::GetStyleColorVec4(ImGuiCol_Button));
//ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0, col);
if (this->line_changed)
{
ImGui::SetScrollHereY();
}
}
ImGui::Text("%lu ", idx + 1);
ImGui::TableNextColumn();
const auto line = this->lines[idx];
ImGui::Text("%.*s", static_cast<int>(line.size()), line.data());
}
ImGui::EndTable();
}
ImGui::End();
this->line_changed = false;
return false;
}
bool WatchWindow::draw(Frontend &frontend)
{
if (!ImGui::Begin(this->id.c_str()))
{
ImGui::End();
return false;
}
if (!frontend.target)
{
this->first = true;
ImGui::End();
return false;
}
if (frontend.target->state == TargetState::stopped
|| frontend.target->state == TargetState::startup)
{
ImGui::End();
return false;
}
// TODO: we need to clean up the initializations/sync of the DAG somehow
if (first)
{
this->locals_src_id = frontend.target->data_node_id++;
using namespace data::source;
frontend.target->backend->add_data_node(Node{.id = this->locals_src_id,
.type = Node::Type::locals,
.data = std::monostate{}});
first = false;
}
auto expr_path_vec = std::vector<ExprPathPart>{};
// ImGuiTableFlags_SizingFixedFit
if (ImGui::BeginTable("##Variables", 2,
ImGuiTableFlags_RowBg | ImGuiTableFlags_Resizable))
{
ImGui::TableSetupColumn("Name");
ImGui::TableSetupColumn("Value");
ImGui::TableHeadersRow();
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
ImGui::PushID("TTTTTT");
if (ImGui::TreeNodeEx("Locals", ImGuiTreeNodeFlags_SpanFullWidth
| ImGuiTreeNodeFlags_DefaultOpen))
{
auto locals_idx =
frontend.target->data_idx_for_src_id(this->locals_src_id);
if (locals_idx)
{
const auto &local_node = *frontend.target->data_res_nodes[*locals_idx];
const auto &locals_data = local_node.vec_data();
size_t cur_off = 0;
size_t cur_idx = 0;
while (cur_off < locals_data.size())
{
if (cur_off + 2 > locals_data.size())
{
break;
}
auto str_len =
*reinterpret_cast<const uint16_t *>(locals_data.data() + cur_off);
if (cur_off + 2 + str_len > locals_data.size())
{
break;
}
auto name = std::string_view{
reinterpret_cast<const char *>(locals_data.data() + cur_off + 2),
str_len};
auto node_idx = local_node.children[cur_idx];
expr_path_vec.clear();
this->draw_value(frontend,
frontend.target->data_res_nodes[node_idx]->type_id,
name, node_idx, 0, expr_path_vec);
cur_idx += 1;
cur_off += 2 + str_len;
}
}
ImGui::TreePop();
}
ImGui::PopID();
for (size_t i = 0; i < extra_slots.size(); ++i)
{
auto res_idx = frontend.target->data_idx_for_src_id(extra_slots[i].id);
auto no_success = true;
if (res_idx)
{
const auto &node = *frontend.target->data_res_nodes[*res_idx];
if (node.success && node.children.size() == 1)
{
no_success = false;
const auto &child_node =
*frontend.target->data_res_nodes[node.children[0]];
expr_path_vec.clear();
this->draw_value(frontend, child_node.type_id, &extra_slots[i],
node.children[0], 0, expr_path_vec);
if (extra_slots[i].bak == "")
{
frontend.target->backend->remove_data_node(extra_slots[i].id);
extra_slots.erase(extra_slots.begin() + i);
--i;
continue;
}
}
}
if (no_success)
{
auto &slot = extra_slots[i];
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
auto is_editing = false;
if (slot.is_editing)
{
is_editing = true;
if (slot.edit_was_started)
{
slot.edit_was_started = false;
ImGui::SetKeyboardFocusHere();
}
if (ImGui::InputText("##dddd", slot.buf, sizeof(slot.buf),
ImGuiInputTextFlags_AutoSelectAll
| ImGuiInputTextFlags_EnterReturnsTrue)
&& ImGui::IsItemDeactivatedAfterEdit())
{
if (slot.buf[0] != '\0')
{
frontend.target->backend->remove_data_node(slot.id);
using namespace data::source;
frontend.target->backend->add_data_node(Node{
.id = slot.id,
.type = Node::Type::source,
.data =
Source{.type = Source::Type::variable,
.data = Source::Variable{.expr_path = slot.buf}}});
}
slot.bak = slot.buf;
slot.is_editing = false;
}
if (ImGui::IsItemDeactivated())
{
memcpy(slot.buf, slot.bak.data(), slot.bak.size());
slot.buf[slot.bak.size()] = '\0';
slot.is_editing = false;
}
}
if (!is_editing)
{
char tree_id_buf[128];
std::snprintf(tree_id_buf, sizeof(tree_id_buf), "slot%lu", i);
// TODO: better id
ImGui::TreeNodeEx(tree_id_buf,
ImGuiTreeNodeFlags_SpanFullWidth
| ImGuiTreeNodeFlags_Leaf
| ImGuiTreeNodeFlags_NoTreePushOnOpen,
"%s", slot.bak.c_str());
if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)
&& ImGui::IsItemHovered())
{
slot.is_editing = true;
slot.edit_was_started = true;
}
}
ImGui::TableNextColumn();
ImGui::TextDisabled("<not available>");
}
}
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
if (ImGui::InputText("##EXTRASLOT", this->add_slot_buf,
sizeof(this->add_slot_buf),
ImGuiInputTextFlags_AutoSelectAll
| ImGuiInputTextFlags_EnterReturnsTrue)
&& ImGui::IsItemDeactivatedAfterEdit())
{
if (this->add_slot_buf[0] != '\0')
{
auto id = frontend.target->data_node_id++;
using namespace data::source;
frontend.target->backend->add_data_node(Node{
.id = id,
.type = Node::Type::source,
.data =
Source{.type = Source::Type::variable,
.data = Source::Variable{.expr_path = this->add_slot_buf}}});
extra_slots.push_back(ExtraSlot{.id = id, .bak = this->add_slot_buf});
memcpy(extra_slots.back().buf, this->add_slot_buf,
sizeof(this->add_slot_buf));
this->add_slot_buf[0] = '\0';
}
}
ImGui::EndTable();
}
ImGui::End();
return false;
}
void WatchWindow::draw_value(Frontend &frontend,
data::type_info::TypeID type_id,
std::variant<std::string_view, ExtraSlot *> name,
data::result::NodeIdx node_idx, size_t off,
std::vector<ExprPathPart> &expr_path)
{
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
const char *name_begin, *name_end;
if (name.index() == 0)
{
name_begin = std::get<std::string_view>(name).begin();
name_end = std::get<std::string_view>(name).end();
} else
{
name_begin = &*std::get<ExtraSlot *>(name)->bak.begin();
name_end = &*std::get<ExtraSlot *>(name)->bak.end();
}
const auto &node_opt = frontend.target->data_res_nodes[node_idx];
if (!node_opt || !node_opt->success)
{
ImGui::TextUnformatted(name_begin, name_end);
ImGui::TableNextColumn();
ImGui::TextDisabled("<unavailable>");
return;
}
const auto &node = *node_opt;
using namespace data::type_info;
while (type_id.type == Type::alias)
{
type_id =
std::get<TypeID>(frontend.target->types[type_id.idx].member_types);
}
auto tree_open = false;
auto pop_id = false;
if (type_id.type == Type::complex || type_id.type == Type::array)
{
ImGui::PushID(name_begin, name_end);
pop_id = true;
char tree_id_buf[128];
std::snprintf(tree_id_buf, sizeof(tree_id_buf), "%u#off%lu", node_idx, off);
auto is_editing = false;
if (name.index() == 1)
{
auto *slot = std::get<ExtraSlot *>(name);
if (slot->is_editing)
{
is_editing = true;
ImGui::BeginDisabled();
tree_open = ImGui::TreeNodeEx(tree_id_buf, 0, "");
ImGui::EndDisabled();
ImGui::SameLine();
if (slot->edit_was_started)
{
slot->edit_was_started = false;
ImGui::SetKeyboardFocusHere();
}
if (ImGui::InputText("##dddd", slot->buf, sizeof(slot->buf),
ImGuiInputTextFlags_AutoSelectAll
| ImGuiInputTextFlags_EnterReturnsTrue)
&& ImGui::IsItemDeactivatedAfterEdit())
{
if (slot->buf[0] != '\0')
{
frontend.target->backend->remove_data_node(slot->id);
using namespace data::source;
frontend.target->backend->add_data_node(
Node{.id = slot->id,
.type = Node::Type::source,
.data =
Source{.type = Source::Type::variable,
.data = Source::Variable{.expr_path = slot->buf}}});
}
slot->bak = slot->buf;
slot->is_editing = false;
}
if (ImGui::IsItemDeactivated())
{
slot->is_editing = false;
}
}
}
if (!is_editing)
{
tree_open = ImGui::TreeNodeEx(
tree_id_buf,
ImGuiTreeNodeFlags_SpanFullWidth | ImGuiTreeNodeFlags_SpanAvailWidth,
"%.*s", static_cast<int>(name_end - name_begin), name_begin);
if (name.index() == 1
&& ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)
&& ImGui::IsItemHovered())
{
auto *slot = std::get<ExtraSlot *>(name);
slot->is_editing = true;
slot->edit_was_started = true;
}
}
if (tree_open)
{
if (type_id.type == Type::complex)
{
char tree_id_buf[128];
const auto &members = frontend.target->types[type_id.idx].member_vec();
for (size_t i = 0; i < members.size(); ++i)
{
const auto &member = members[i];
if (member.bitfield_size)
{
std::snprintf(tree_id_buf, sizeof(tree_id_buf), "%u#member%lu",
node_idx, i);
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
ImGui::TreeNodeEx(tree_id_buf,
ImGuiTreeNodeFlags_SpanFullWidth
| ImGuiTreeNodeFlags_Leaf
| ImGuiTreeNodeFlags_NoTreePushOnOpen,
"%s", member.name.c_str());
ImGui::TableNextColumn();
auto member_type = member.type_id;
while (member_type.type == Type::alias)
{
member_type = std::get<TypeID>(
frontend.target->types[member_type.idx].member_types);
}
// TODO: handle u128/i128
assert(member_type.type != Type::i128
&& member_type.type != Type::u128);
assert(member_type.is_integral()
|| member_type.type == Type::_enum);
assert(member.bitfield_size <= 8 * 8);
auto byte_off = member.offset / 8;
auto bit_off = member.offset % 8;
auto byte_size = (member.bitfield_size + 7) / 8;
uint64_t value = 0;
uint8_t arr[8] = {};
const auto &data = node.vec_data();
std::copy(data.begin() + off + byte_off,
data.begin() + off + byte_off + byte_size, arr);
value = *reinterpret_cast<uint64_t *>(arr);
assert(bit_off + member.bitfield_size <= 64);
value = value >> bit_off;
value =
value & (0xFFFFFFFF'FFFFFFFF >> (64 - member.bitfield_size));
if (member_type.type == Type::_enum)
{
const auto &members =
frontend.target->types[type_id.idx].member_vec();
auto printed = false;
for (const auto &member : members)
{
if (value == member.enum_val)
{
ImGui::Text("%s", member.name.c_str());
printed = true;
break;
}
}
if (!printed)
{
ImGui::Text("<invalid: %lu>", value);
}
} else
{
ImGui::Text("%ld", value);
}
continue;
}
expr_path.push_back(ExprPathPart{
.ident = std::string_view{name_begin, static_cast<size_t>(
name_end - name_begin)},
.deref = false});
this->draw_value(frontend, member.type_id, member.name, node_idx,
off + member.offset, expr_path);
expr_path.pop_back();
}
} else
{
// array
auto member_ty_id =
std::get<TypeID>(frontend.target->types[type_id.idx].member_types);
auto member_size = member_ty_id.byte_size(frontend.target->types);
size_t el_count =
frontend.target->types[type_id.idx].byte_size / member_size;
expr_path.push_back(ExprPathPart{
.ident = std::string_view{name_begin,
static_cast<size_t>(name_end - name_begin)},
.array = true});
char buf[32];
size_t member_off = 0;
for (size_t i = 0; i < el_count; ++i)
{
std::snprintf(buf, sizeof(buf), "[%lu]", i);
this->draw_value(frontend, member_ty_id, buf, node_idx,
off + member_off, expr_path);
member_off += member_size;
}
expr_path.pop_back();
}
ImGui::TreePop();
}
ImGui::PopID();
} else
{
auto is_editing = false;
if (name.index() == 1)
{
auto *slot = std::get<ExtraSlot *>(name);
if (slot->is_editing)
{
is_editing = true;
if (slot->edit_was_started)
{
slot->edit_was_started = false;
ImGui::SetKeyboardFocusHere();
}
if (ImGui::InputText("##dddd", slot->buf, sizeof(slot->buf),
ImGuiInputTextFlags_AutoSelectAll
| ImGuiInputTextFlags_EnterReturnsTrue)
&& ImGui::IsItemDeactivatedAfterEdit())
{
if (slot->buf[0] != '\0')
{
frontend.target->backend->remove_data_node(slot->id);
using namespace data::source;
frontend.target->backend->add_data_node(
Node{.id = slot->id,
.type = Node::Type::source,
.data =
Source{.type = Source::Type::variable,
.data = Source::Variable{.expr_path = slot->buf}}});
}
slot->bak = slot->buf;
slot->is_editing = false;
}
if (ImGui::IsItemDeactivated())
{
slot->is_editing = false;
}
}
}
auto tree_open = false;
if (!is_editing)
{
char tree_id_buf[128];
std::snprintf(tree_id_buf, sizeof(tree_id_buf), "%u#off%lu", node_idx,
off);
ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_SpanFullWidth;
if (type_id.type != Type::ptr)
{
flags |= ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_NoTreePushOnOpen;
}
// TODO: better id
tree_open =
ImGui::TreeNodeEx(tree_id_buf, flags, "%.*s",
static_cast<int>(name_end - name_begin), name_begin);
//ImGui::TextUnformatted(name_begin, name_end);
if (name.index() == 1
&& ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)
&& ImGui::IsItemHovered())
{
auto *slot = std::get<ExtraSlot *>(name);
slot->is_editing = true;
slot->edit_was_started = true;
}
}
ImGui::TableSetColumnIndex(1);
switch (type_id.type)
{
using enum Type;
case i8:
{
auto val = node.get_primitive<int8_t>(off);
if (std::isprint(val))
{
ImGui::Text("%c", val);
} else
{
ImGui::Text("%d", val);
}
break;
}
case u8: ImGui::Text("%lu", node.get_primitive<uint8_t>(off)); break;
case u16: ImGui::Text("%lu", node.get_primitive<uint16_t>(off)); break;
case u32: ImGui::Text("%lu", node.get_primitive<uint32_t>(off)); break;
case u64: ImGui::Text("%lu", node.get_primitive<uint64_t>(off)); break;
case i16: ImGui::Text("%ld", node.get_primitive<int16_t>(off)); break;
case i32: ImGui::Text("%ld", node.get_primitive<int32_t>(off)); break;
case i64: ImGui::Text("%ld", node.get_primitive<int64_t>(off)); break;
case _bool:
{
auto val = node.get_primitive<uint8_t>(off);
if (val)
{
ImGui::TextUnformatted("true");
} else
{
ImGui::TextUnformatted("false");
}
break;
}
// TODO
case f128:
case i128:
case u128:
{
const auto &data = node.vec_data();
const auto lo = node.get_primitive<uint64_t>(off);
const auto hi = node.get_primitive<uint64_t>(off + 8);
ImGui::Text("%16lX-%16lX", hi, lo);
break;
}
case f32:
{
ImGui::Text("%f", node.get_primitive<float>(off));
break;
}
case f64:
{
ImGui::Text("%f", node.get_primitive<double>(off));
break;
}
case ptr:
{
// TODO: ptr size
ImGui::Text("%lX", node.get_primitive<uint64_t>(off));
if (tree_open)
{
// TODO: make this func be able to not print the "outer" line
// and just accept a bool to only draw the containing value
std::string path{};
if (type_id.sub_type != Type::i8)
{
// TODO: better check if this is really a string
path = "*";
}
// why cant i just path name_begin, name_end to string_view? :[
construct_expr_path(
path, expr_path,
std::string_view{name_begin,
static_cast<size_t>(name_end - name_begin)});
auto src_id = frontend.target->find_or_create_expr_path(
std::move(path), type_id.sub_type == Type::i8);
auto res_idx = frontend.target->data_idx_for_src_id(src_id);
if (res_idx)
{
const auto &node = *frontend.target->data_res_nodes[*res_idx];
if (type_id.sub_type == Type::i8 && node.success)
{
// TODO: make span whole row (see https://github.com/ocornut/imgui/issues/3565)
assert(node.type_id.type == Type::custom);
assert(node.vec_data().back() == '\0');
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(1);
ImGui::TextWrapped("\"%s\"", node.vec_data().data());
} else if (node.success && node.children.size() == 1)
{
auto data_idx = node.children[0];
const auto &data_node =
*frontend.target->data_res_nodes[data_idx];
expr_path.push_back(ExprPathPart{
.ident = std::string_view{name_begin, static_cast<size_t>(
name_end - name_begin)},
.deref = true});
this->draw_value(frontend, data_node.type_id,
std::string_view{"<pointee>"}, data_idx, 0,
expr_path);
expr_path.pop_back();
}
}
ImGui::TreePop();
}
break;
}
case _enum:
{
// TODO: robustness
uint64_t val;
const auto byte_size = frontend.target->types[type_id.idx].byte_size;
if (byte_size == 8)
{
val = node.get_primitive<uint64_t>(off);
} else if (byte_size == 4)
{
val = node.get_primitive<uint32_t>(off);
} else if (byte_size == 2)
{
val = node.get_primitive<uint16_t>(off);
} else if (byte_size == 1)
{
val = node.get_primitive<uint8_t>(off);
} else
{
assert(0);
val = 0;
}
const auto &members = frontend.target->types[type_id.idx].member_vec();
auto printed = false;
for (const auto &member : members)
{
if (val == member.enum_val)
{
ImGui::Text("%s", member.name.c_str());
printed = true;
break;
}
}
if (!printed)
{
ImGui::Text("<invalid: %lu>", val);
}
break;
}
}
}
}
void WatchWindow::construct_expr_path(std::string &out,
const std::vector<ExprPathPart> &path,
std::string_view tail)
{
for (const auto &entry : path)
{
// FIXME: BIGGEST HACK in cinema history
if (entry.ident == "<pointee>")
{
continue;
}
out += entry.ident;
if (entry.deref)
{
out += "->";
} else if (!entry.array)
{
out += '.';
}
}
out += tail;
}
void Window::handle_source_updated(Target &target, size_t id)
{
switch (this->type)
{
using enum WindowType;
case disassembly:
std::get<DisasmWindow>(this->data).handle_source_updated(target, id);
break;
case source:
std::get<SourceWindow>(this->data).handle_source_updated(target, id);
break;
default: break;
}
}
void DisasmWindow::handle_source_updated(Target &target, size_t id)
{
if (id == this->ip_src_id)
{
auto result = target.data_node_for_src_id(id);
if (!result || !result->success
|| result->type_id.type != data::type_info::Type::u64)
{
this->ip_unsuccessful = true;
return;
}
this->ip_unsuccessful = false;
this->ip_changed = true;
this->ip = std::get<uint64_t>(result->data);
printf("IP changed to %lX\n", this->ip);
return;
}
if (id != this->disas_src_id)
{
return;
}
auto result = target.data_node_for_src_id(id);
if (!result || !result->success
|| result->type_id.type != data::type_info::Type::custom)
{
this->disas_unsuccessful = true;
this->insts.clear();
return;
}
this->disas_unsuccessful = false;
this->insts.clear();
// parse insts
// struct Inst {
// uint64_t addr;
// uint8_t mnem_len;
// uint8_t op_len;
// uint8_t comment_len;
// uint8_t inst_len;
// char mnem[];
// char op[];
// char comment[];
// };
char buf[256];
size_t idx = 0;
uint8_t max_mnem_len = 0;
uint8_t max_op_len = 0;
const auto &data = result->vec_data();
while (idx < data.size())
{
if (data.size() - idx < 12)
{
break;
}
uint64_t addr = *reinterpret_cast<const uint64_t *>(&data[idx]);
uint8_t mnem_len = *reinterpret_cast<const uint8_t *>(&data[idx + 8]);
uint8_t op_len = *reinterpret_cast<const uint8_t *>(&data[idx + 9]);
uint8_t comment_len = *reinterpret_cast<const uint8_t *>(&data[idx + 10]);
// dc about inst_len rn
idx += 12;
if (data.size() - idx < mnem_len + op_len + comment_len)
{
break;
}
if (comment_len)
{
std::snprintf(buf, sizeof(buf), "%.*s%%*c%.*s%%.*c ; %.*s", mnem_len,
&data[idx], op_len, &data[idx + mnem_len], comment_len,
&data[idx + mnem_len + op_len]);
} else
{
std::snprintf(buf, sizeof(buf), "%.*s%%*c%.*s%%.*c", mnem_len, &data[idx],
op_len, &data[idx + mnem_len]);
}
idx += mnem_len + op_len + comment_len;
if (mnem_len > max_mnem_len)
{
max_mnem_len = mnem_len;
}
if (op_len > max_op_len)
{
max_op_len = op_len;
}
insts.push_back(Instruction{.fmt_str = buf,
.addr = addr,
.mnem_len = mnem_len,
.op_len = op_len,
.comm_len = comment_len});
}
this->max_mnem_len = max_mnem_len;
this->max_op_len = max_op_len;
}
void SourceWindow::handle_source_updated(Target &target, size_t id)
{
if (id == this->ip_src_id)
{
// should not need to care
return;
}
if (id != this->line_entry_src_id)
{
return;
}
auto result = target.data_node_for_src_id(id);
if (!result || !result->success
|| result->type_id.type != data::type_info::Type::custom)
{
this->lines.clear();
this->file_data.clear();
this->file_name.clear();
this->line = 0;
return;
}
// parse line entry
// struct LineEntry {
// uint32_t line_no;
// uint32_t name_len;
// char file_name[];
// };
const auto &data = result->vec_data();
if (data.size() < 8)
{
this->lines.clear();
this->file_data.clear();
this->file_name.clear();
this->line = 0;
return;
}
const auto line = *reinterpret_cast<const uint32_t *>(data.data());
const auto name_len = *reinterpret_cast<const uint32_t *>(data.data() + 4);
if (data.size() < 8 + name_len)
{
this->lines.clear();
this->file_data.clear();
this->file_name.clear();
this->line = 0;
return;
}
const auto file_view =
std::string_view{reinterpret_cast<const char *>(data.data() + 8), name_len};
printf("New LE: %.*s:%u", static_cast<int>(file_view.size()),
file_view.data(), line);
if (this->file_name != file_view)
{
// force reload
this->lines.clear();
this->file_data.clear();
this->file_name = file_view;
this->line_changed = true;
}
if (this->line != line)
{
this->line = line;
this->line_changed = true;
}
}